Skip to content

Commit

Permalink
Merge branch 'dev' into CSCEXAM-1391
Browse files Browse the repository at this point in the history
  • Loading branch information
VirmasaloA authored Jan 8, 2025
2 parents 2e705c7 + 8d35deb commit 6eed667
Show file tree
Hide file tree
Showing 16 changed files with 248 additions and 45 deletions.
47 changes: 36 additions & 11 deletions app/controllers/admin/ReportController.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import javax.inject.Inject;
import miscellaneous.excel.ExcelBuilder;
import models.enrolment.ExamEnrolment;
import models.enrolment.Reservation;
import models.exam.Course;
import models.exam.Exam;
import models.facility.ExamRoom;
Expand Down Expand Up @@ -228,21 +229,46 @@ public Result getPublishedExams(Optional<String> dept, Optional<String> start, O
@Restrict({ @Group("ADMIN") })
public Result getReservations(Optional<String> dept, Optional<String> start, Optional<String> end) {
ExpressionList<ExamEnrolment> query = DB.find(ExamEnrolment.class).where();
query =
applyFilters(
query,
"exam.course",
"reservation.startAt",
dept.orElse(null),
start.orElse(null),
end.orElse(null)
);
query = applyFilters(
query,
"exam.course",
"reservation.startAt",
dept.orElse(null),
start.orElse(null),
end.orElse(null)
);
Set<ExamEnrolment> enrolments = query.findSet();
long noShows = enrolments.stream().filter(ExamEnrolment::isNoShow).count();
long appearances = enrolments.size() - noShows;
return ok(Json.newObject().put("noShows", noShows).put("appearances", appearances));
}

@Restrict({ @Group("ADMIN") })
public Result getIopReservations(Optional<String> dept, Optional<String> start, Optional<String> end) {
ExpressionList<Reservation> query = DB.find(Reservation.class)
.fetch("externalReservation")
.fetch("enrolment")
.where()
.or()
.isNotNull("externalRef")
.isNotNull("externalReservation.orgName")
.endOr();
query = applyFilters(
query,
"examEnrolment.exam.course",
"startAt",
dept.orElse(null),
start.orElse(null),
end.orElse(null)
);
Set<Reservation> reservations = query
.findSet()
.stream()
.filter(r -> r.getExternalOrgName() != null || (r.getExternalReservation() != null))
.collect(Collectors.toSet());
return ok(reservations);
}

@Restrict({ @Group("ADMIN") })
public Result getResponses(Optional<String> dept, Optional<String> start, Optional<String> end) {
ExpressionList<Exam> query = DB.find(Exam.class).where().isNotNull("parent").isNotNull("course");
Expand Down Expand Up @@ -272,8 +298,7 @@ public Result getResponses(Optional<String> dept, Optional<String> start, Option
)
)
.count();
JsonNode node = Json
.newObject()
JsonNode node = Json.newObject()
.put("aborted", aborted)
.put("assessed", assessed)
.put("unAssessed", unAssessed);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ public Result provideReservation(Http.Request request) {
DateTime start = ISODateTimeFormat.dateTimeParser().parseDateTime(node.get("start").asText());
DateTime end = ISODateTimeFormat.dateTimeParser().parseDateTime(node.get("end").asText());
String userEppn = node.get("user").asText();
String orgRef = node.get("orgRef").asText();
String orgName = node.get("orgName").asText();
if (start.isBeforeNow() || end.isBefore(start)) {
return badRequest("invalid dates");
}
Expand All @@ -133,6 +135,8 @@ public Result provideReservation(Http.Request request) {
reservation.setStartAt(start);
reservation.setMachine(machine.get());
reservation.setExternalUserRef(userEppn);
reservation.setExternalOrgRef(orgRef);
reservation.setExternalOrgName(orgName);
reservation.save();
PathProperties pp = PathProperties.parse("(*, machine(*, room(*, mailAddress(*))))");

Expand Down
10 changes: 5 additions & 5 deletions app/impl/ExternalCourseHandlerImpl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ package impl
import io.ebean.DB
import miscellaneous.config.ConfigReader
import miscellaneous.scala.DbApiHelper
import models._
import models.*
import models.exam.{Course, Grade, GradeScale}
import models.facility.Organisation
import models.user.User
Expand All @@ -18,15 +18,15 @@ import play.api.Logging
import play.api.libs.json.{JsValue, Json}
import play.api.libs.ws.{WSClient, WSResponse}
import play.mvc.Http
import validators.ExternalCourseValidator.{CourseUnitInfo, GradeScale => ExtGradeScale}
import validators.ExternalCourseValidator.{CourseUnitInfo, GradeScale as ExtGradeScale}

import java.net._
import java.net.*
import java.nio.charset.StandardCharsets
import java.text.SimpleDateFormat
import javax.inject.Inject
import scala.collection.immutable.TreeSet
import scala.concurrent.{ExecutionContext, Future}
import scala.jdk.CollectionConverters._
import scala.jdk.CollectionConverters.*

class ExternalCourseHandlerImpl @Inject (
private val wsClient: WSClient,
Expand Down Expand Up @@ -229,7 +229,7 @@ class ExternalCourseHandlerImpl @Inject (
val path = configReader.getString(configPath.getOrElse(""))
if (!path.contains(COURSE_CODE_PLACEHOLDER))
throw new RuntimeException("exam.integration.courseUnitInfo.url is malformed")
val url = path.replace(COURSE_CODE_PLACEHOLDER, courseCode)
val url = path.replace(COURSE_CODE_PLACEHOLDER, URLEncoder.encode(courseCode, StandardCharsets.UTF_8))
URI.create(url).toURL

private def parseUrl(user: User) =
Expand Down
26 changes: 22 additions & 4 deletions app/models/enrolment/Reservation.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ public class Reservation extends GeneratedIdentityModel implements Comparable<Re
private String externalRef;

private String externalUserRef;
private String externalOrgRef;
private String externalOrgName;

@OneToOne(cascade = CascadeType.ALL)
private ExternalReservation externalReservation;
Expand Down Expand Up @@ -123,10 +125,6 @@ public void setExternalRef(String externalRef) {
this.externalRef = externalRef;
}

public String getExternalUserRef() {
return externalUserRef;
}

public ExternalReservation getExternalReservation() {
return externalReservation;
}
Expand All @@ -135,10 +133,30 @@ public void setExternalReservation(ExternalReservation externalReservation) {
this.externalReservation = externalReservation;
}

public String getExternalUserRef() {
return externalUserRef;
}

public void setExternalUserRef(String externalUserRef) {
this.externalUserRef = externalUserRef;
}

public String getExternalOrgRef() {
return externalOrgRef;
}

public void setExternalOrgRef(String externalOrgRef) {
this.externalOrgRef = externalOrgRef;
}

public String getExternalOrgName() {
return externalOrgName;
}

public void setExternalOrgName(String externalOrgName) {
this.externalOrgName = externalOrgName;
}

public Interval toInterval() {
return new Interval(startAt, endAt);
}
Expand Down
11 changes: 11 additions & 0 deletions conf/evolutions/default/135.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- SPDX-FileCopyrightText: 2024 The members of the EXAM Consortium
--
-- SPDX-License-Identifier: EUPL-1.2

# --- !Ups
ALTER TABLE reservation ADD external_org_ref character varying(32);
ALTER TABLE reservation ADD external_org_name character varying(255);

# --- !Downs
ALTER TABLE reservation DROP external_org_ref;
ALTER TABLE reservation DROP external_org_name;
1 change: 1 addition & 0 deletions conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,7 @@ GET /app/reports/participations
GET /app/reports/departments controllers.admin.ReportController.listDepartments
GET /app/reports/exams controllers.admin.ReportController.getPublishedExams(dept: java.util.Optional[String], start: java.util.Optional[String], end: java.util.Optional[String])
GET /app/reports/reservations controllers.admin.ReportController.getReservations(dept: java.util.Optional[String], start: java.util.Optional[String], end: java.util.Optional[String])
GET /app/reports/reservations/iop controllers.admin.ReportController.getIopReservations(dept: java.util.Optional[String], start: java.util.Optional[String], end: java.util.Optional[String])
GET /app/reports/responses controllers.admin.ReportController.getResponses(dept: java.util.Optional[String], start: java.util.Optional[String], end: java.util.Optional[String])
POST /app/reports/questionreport/:id controllers.admin.ReportController.exportExamQuestionScoresAsExcel(id: Long, request: Request)

Expand Down
2 changes: 2 additions & 0 deletions test/controllers/iop/ExternalCalendarInterfaceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,8 @@ public void testProvideReservation() {
.put("start", ISODateTimeFormat.dateTime().print(start))
.put("end", ISODateTimeFormat.dateTime().print(end))
.put("user", "[email protected]")
.put("orgRef", "1234")
.put("orgName", "1234")
);
assertThat(result.status()).isEqualTo(201);
Reservation reservation = DB.find(Reservation.class).where().eq("externalRef", RESERVATION_REF).findOne();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import { StatisticsService } from 'src/app/administrative/statistics/statistics.
<strong>{{ 'i18n_most_popular_exams' | translate }}</strong>
</div>
</div>
<div class="row">
@if (exams.length > 0) {
@if (exams.length > 0) {
<div class="row">
<div class="col-12">
<table class="table table-striped table-sm">
<thead>
Expand All @@ -46,16 +46,14 @@ import { StatisticsService } from 'src/app/administrative/statistics/statistics.
<strong>{{ 'i18n_total' | translate }}</strong>
</td>
<td>
@if (exams) {
<strong>{{ totalExams }}</strong>
}
<strong>{{ totalExams }}</strong>
</td>
</tr>
</tfoot>
</table>
</div>
}
</div>
</div>
}
`,
selector: 'xm-exam-statistics',
standalone: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// SPDX-FileCopyrightText: 2024 The members of the EXAM Consortium
//
// SPDX-License-Identifier: EUPL-1.2

import { KeyValuePipe } from '@angular/common';
import type { OnInit } from '@angular/core';
import { Component, Input } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import { groupBy } from 'ramda';
import { QueryParams } from 'src/app/administrative/administrative.model';
import { StatisticsService } from 'src/app/administrative/statistics/statistics.service';
import { Reservation } from 'src/app/reservation/reservation.model';

@Component({
template: `
<div class="row my-2">
<div class="col-12">
<button class="btn btn-primary" (click)="listReservations()">
{{ 'i18n_search' | translate }}
</button>
</div>
</div>
@if (grouped) {
<div class="row">
<div class="col-12">
<table class="table table-striped table-sm">
<thead class="table-light">
<tr>
<th>{{ 'i18n_faculty_name' | translate }}</th>
<th>{{ 'i18n_outbound_reservations' | translate }}</th>
<th>
{{ 'i18n_outbound_reservations' | translate }} -
{{ 'i18n_unused_reservation' | translate }}
</th>
<th>{{ 'i18n_inbound_reservations' | translate }}</th>
<th>
{{ 'i18n_inbound_reservations' | translate }} -
{{ 'i18n_unused_reservation' | translate }}
</th>
</tr>
</thead>
<tbody>
@for (rg of grouped | keyvalue; track rg) {
<tr>
<td>{{ rg.key }}</td>
<td>{{ outgoingTo(rg.key) }}</td>
<td>{{ outgoingNoShowsTo(rg.key) }}</td>
<td>{{ incomingFrom(rg.key) }}</td>
<td>{{ incomingNoShowsFrom(rg.key) }}</td>
</tr>
}
</tbody>
<tfoot class="table-light">
<tr>
<td>
<strong>{{ 'i18n_total' | translate }}</strong>
</td>
<td>
<strong>{{ totalOutgoing() }}</strong>
</td>
<td>
<strong>{{ totalOutgoingNoShows() }}</strong>
</td>
<td>
<strong>{{ totalIncoming() }}</strong>
</td>
<td>
<strong>{{ totalIncomingNoShows() }}</strong>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
}
`,
selector: 'xm-iop-reservation-statistics',
standalone: true,
imports: [KeyValuePipe, TranslateModule],
})
export class IopReservationStatisticsComponent implements OnInit {
@Input() queryParams: QueryParams = {};

reservations: Reservation[] = [];
grouped!: Record<string, Reservation[]>;

constructor(private Statistics: StatisticsService) {}

ngOnInit() {
this.listReservations();
}

listReservations = () =>
this.Statistics.listIopReservations$(this.queryParams).subscribe((resp) => {
const byOrg = groupBy((r: Reservation) => r.externalOrgName || r.externalReservation?.orgName || '');
this.grouped = byOrg(resp) as Record<string, Reservation[]>;
});

incomingFrom = (org: keyof typeof this.grouped): number =>
this.grouped[org].filter((r) => r.externalOrgRef && !r.enrolment?.noShow).length;
incomingNoShowsFrom = (org: keyof typeof this.grouped): number =>
this.grouped[org].filter((r) => r.externalOrgRef && r.enrolment?.noShow === true).length;
outgoingTo = (org: keyof typeof this.grouped): number =>
this.grouped[org].filter((r) => r.externalReservation?.orgName && !r.enrolment?.noShow).length;
outgoingNoShowsTo = (org: keyof typeof this.grouped): number =>
this.grouped[org].filter((r) => r.externalReservation?.orgName && r.enrolment?.noShow === true).length;
totalIncoming = () =>
Object.keys(this.grouped)
.map((k) => this.incomingFrom(k))
.reduce((a, b) => a + b);
totalOutgoing = () =>
Object.keys(this.grouped)
.map((k) => this.outgoingTo(k))
.reduce((a, b) => a + b);
totalIncomingNoShows = () =>
Object.keys(this.grouped)
.map((k) => this.incomingNoShowsFrom(k))
.reduce((a, b) => a + b);
totalOutgoingNoShows = () =>
Object.keys(this.grouped)
.map((k) => this.outgoingNoShowsTo(k))
.reduce((a, b) => a + b);
}
13 changes: 7 additions & 6 deletions ui/src/app/administrative/statistics/statistics.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
<li [ngbNavItem]="'RESERVATIONS'">
<a ngbNavLink>{{ 'i18n_reservations' | translate }}</a>
</li>
<li [ngbNavItem]="'IOP_RESERVATIONS'">
<a ngbNavLink>{{ 'i18n_iop_reservations' | translate }}</a>
</li>
</ul>
</div>
</div>
Expand Down Expand Up @@ -60,12 +63,7 @@
>
{{ 'i18n_choose' | translate }}&nbsp;<span class="caret"></span>
</button>
<div
ngbDropdownMenu
style="padding-left: 0; min-width: 17em"
role="menu"
aria-labelledby="dropDownMenu1"
>
<div ngbDropdownMenu role="menu" aria-labelledby="dropDownMenu1">
<div role="presentation" class="input-group">
<input
[ngModel]="limitations.department"
Expand Down Expand Up @@ -112,6 +110,9 @@
@if (view === 'RESERVATIONS') {
<xm-reservation-statistics [queryParams]="queryParams"> </xm-reservation-statistics>
}
@if (view === 'IOP_RESERVATIONS') {
<xm-iop-reservation-statistics [queryParams]="queryParams"> </xm-iop-reservation-statistics>
}
@if (view === 'RESPONSES') {
<xm-response-statistics [queryParams]="queryParams"></xm-response-statistics>
}
Expand Down
Loading

0 comments on commit 6eed667

Please sign in to comment.