Skip to content

Commit

Permalink
WIP: CSCEXAM-1364 Maintenance period visibility for external students
Browse files Browse the repository at this point in the history
- Also fix some problems regarding adding exception events
  • Loading branch information
lupari committed Oct 11, 2024
1 parent 8fde904 commit ce14c79
Show file tree
Hide file tree
Showing 19 changed files with 154 additions and 58 deletions.
File renamed without changes.
10 changes: 5 additions & 5 deletions app/controllers/exam/CourseController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class CourseController @Inject() (
): Future[Result] =
(filterType, criteria) match
case (Some("code"), Some(c)) =>
externalApi.getCoursesByCode(user, c).map(cs => Results.Ok(cs.toJson).asJson)
externalApi.getCoursesByCode(user, c).map(cs => Results.Ok(cs.asJson))
case (Some("name"), Some(x)) if x.length >= 2 =>
Future {
DB.find(classOf[Course])
Expand All @@ -52,13 +52,13 @@ class CourseController @Inject() (
.getCourseValidityDate(new DateTime(c.getStartDate))
.isBeforeNow
)
}.map(data => Results.Ok(data.toJson).asJson)
}.map(data => Results.Ok(data.asJson))
case (Some("name"), Some(_)) =>
throw new IllegalArgumentException("Too short criteria")
case _ =>
Future {
DB.find(classOf[Course]).where.isNotNull("name").orderBy("code").list
}.map(data => Ok(data.toJson).asJson)
}.map(data => Ok(data.asJson))

private def getUserCourses(
user: User,
Expand All @@ -76,7 +76,7 @@ class CourseController @Inject() (
if tagIds.getOrElse(Nil).nonEmpty then
query = query.in("exams.examSections.sectionQuestions.question.parent.tags.id", tagIds.get.asJava)
if ownerIds.getOrElse(Nil).nonEmpty then query = query.in("exams.examOwners.id", ownerIds.get.asJava)
Ok(query.orderBy("name desc").list.toJson).asJson
Ok(query.orderBy("name desc").list.asJson)

// Actions ->
def getCourses(filterType: Option[String], criteria: Option[String]): Action[AnyContent] =
Expand All @@ -87,7 +87,7 @@ class CourseController @Inject() (

def getCourse(id: Long): Action[AnyContent] =
Action.andThen(authorized(Seq(Role.Name.TEACHER, Role.Name.ADMIN))) { _ =>
Ok(DB.find(classOf[Course], id).toJson)
Ok(DB.find(classOf[Course], id).asJson)
}

def listUsersCourses(
Expand Down
68 changes: 52 additions & 16 deletions app/controllers/facility/MaintenancePeriodController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,56 +2,70 @@
//
// SPDX-License-Identifier: EUPL-1.2

package controllers
package controllers.facility

import io.ebean.DB
import miscellaneous.config.ConfigReader
import miscellaneous.scala.{DbApiHelper, JavaApiHelper}
import models.calendar.MaintenancePeriod
import models.user.Role
import org.joda.time.DateTime
import org.joda.time.format.ISODateTimeFormat
import play.api.libs.json.JsValue
import play.api.mvc._
import play.api.libs.json.*
import play.api.libs.ws.WSClient
import play.api.mvc.*
import security.scala.Auth.authorized
import security.scala.AuthExecutionContext
import system.AuditedAction

import javax.inject.Inject
import scala.concurrent.Future

class MaintenancePeriodController @Inject() (
val controllerComponents: ControllerComponents,
val audited: AuditedAction,
val configReader: ConfigReader,
val wsClient: WSClient,
implicit val ec: AuthExecutionContext
) extends BaseController
with DbApiHelper
with JavaApiHelper:

import play.api.libs.ws.JsonBodyWritables.*

def listMaintenancePeriods: Action[AnyContent] =
Action.andThen(authorized(Seq(Role.Name.STUDENT, Role.Name.TEACHER, Role.Name.ADMIN))) { _ =>
Ok(
DB.find(classOf[MaintenancePeriod])
.where()
.gt("endsAt", DateTime.now())
.list
.toJson
.asJson
)
}

def createMaintenancePeriod: Action[AnyContent] =
Action.andThen(authorized(Seq(Role.Name.ADMIN))).andThen(audited) { request =>
Action.andThen(authorized(Seq(Role.Name.ADMIN))).andThen(audited).async { request =>
request.body.asJson match
case Some(body) =>
parseBody(body) match
case (Some(s), Some(e), Some(d)) =>
val period = update(new MaintenancePeriod, s, e, d)
period.save()
Created(period.toJson)
case _ => BadRequest
case _ => BadRequest
val homeOrg = configReader.getHomeOrganisationRef
if homeOrg.isEmpty then Future.successful { Created(period.asJson) }
else
val periods = DB.find(classOf[MaintenancePeriod]).distinct.asJson
val payload = Json.obj("maintenancePeriods" -> periods)
createRequest(homeOrg)
.put(payload)
.map(resp => Created(period.asJson))
case _ => Future.successful { BadRequest("Bad payload") }
case _ => Future.successful { BadRequest("Bad payload") }
}

def updateMaintenancePeriod(id: Long): Action[AnyContent] =
Action.andThen(authorized(Seq(Role.Name.ADMIN))).andThen(audited) { request =>
Action.andThen(authorized(Seq(Role.Name.ADMIN))).andThen(audited) async { request =>
request.body.asJson match
case Some(body) =>
DB.find(classOf[MaintenancePeriod]).where().idEq(id).find match
Expand All @@ -60,21 +74,43 @@ class MaintenancePeriodController @Inject() (
case (Some(s), Some(e), Some(d)) =>
val period = update(mp, s, e, d)
period.update()
Ok
case _ => BadRequest
case _ => NotFound
case _ => BadRequest
val homeOrg = configReader.getHomeOrganisationRef
if homeOrg.isEmpty then Future.successful { Ok }
else
val periods = DB.find(classOf[MaintenancePeriod]).distinct.asJson
val payload = Json.obj("maintenancePeriods" -> periods)
createRequest(homeOrg)
.put(payload)
.map(resp => Created(period.asJson))
case _ => Future.successful { BadRequest }
case _ => Future.successful { NotFound }
case _ => Future.successful { BadRequest }
}

def removeMaintenancePeriod(id: Long): Action[AnyContent] =
Action.andThen(authorized(Seq(Role.Name.ADMIN))) { _ =>
Action.andThen(authorized(Seq(Role.Name.ADMIN))) async { _ =>
DB.find(classOf[MaintenancePeriod]).where().idEq(id).find match
case Some(mp) =>
mp.delete()
Ok
case _ => NotFound
val homeOrg = configReader.getHomeOrganisationRef
if homeOrg.isEmpty then
Future.successful {
Ok
}
else
val periods = DB.find(classOf[MaintenancePeriod]).distinct.asJson
val payload = Json.obj("maintenancePeriods" -> periods)
createRequest(homeOrg)
.put(payload)
.map(resp => Ok)
case _ => Future.successful { NotFound }
}

private def createRequest(homeOrg: String) =
wsClient
.url(s"${configReader.getIopHost}/api/organisations/$homeOrg")
.addHttpHeaders("Content-Type" -> "application/json")

private def parseBody(body: JsValue) =
def parseDate(d: String) = DateTime.parse(d, ISODateTimeFormat.dateTimeParser())
val start = (body \ "startsAt").asOpt[String].map(parseDate)
Expand Down
6 changes: 3 additions & 3 deletions app/controllers/facility/RoomController.java
Original file line number Diff line number Diff line change
Expand Up @@ -316,8 +316,8 @@ public Result updateExamStartingHours(Http.Request request) {
}

private ExceptionWorkingHours parse(JsonNode node) {
DateTime startDate = ISODateTimeFormat.dateTime().parseDateTime(node.get("startDate").asText());
DateTime endDate = ISODateTimeFormat.dateTime().parseDateTime(node.get("endDate").asText());
DateTime startDate = ISODateTimeFormat.dateTime().parseDateTime(node.get("start").asText());
DateTime endDate = ISODateTimeFormat.dateTime().parseDateTime(node.get("end").asText());
ExceptionWorkingHours hours = new ExceptionWorkingHours();
hours.setStartDate(startDate.toDate());
hours.setEndDate(endDate.toDate());
Expand All @@ -344,8 +344,8 @@ public Result addRoomExceptionHours(Http.Request request) {
exception.setRoom(room);
exception.save();
room.getCalendarExceptionEvents().add(exception);
asyncUpdateRemote(room);
}
asyncUpdateRemote(room);
}
return ok(Json.toJson(rooms.stream().flatMap(r -> r.getCalendarExceptionEvents().stream()).toList()));
}
Expand Down
3 changes: 2 additions & 1 deletion app/controllers/integration/ReservationAPIController.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.inject.Inject;
import miscellaneous.datetime.DateTimeHandler;
import models.enrolment.Reservation;
Expand Down Expand Up @@ -115,7 +116,7 @@ public Result getRoomOpeningHours(Long roomId, Optional<String> date) {
LocalDate end = new LocalDate(ee.getEndDate()).dayOfMonth().withMaximumValue();
return !start.isAfter(searchDate) && !end.isBefore(searchDate);
})
.toList()
.collect(Collectors.toSet())
);
return ok(room, pp);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import play.libs.ws.WSClient;
import play.libs.ws.WSRequest;
import play.libs.ws.WSResponse;
import play.mvc.Http;
import play.mvc.Result;

public class OrganisationController extends BaseController {
Expand All @@ -42,7 +43,7 @@ public CompletionStage<Result> listOrganisations() throws MalformedURLException

Function<WSResponse, Result> onSuccess = response -> {
JsonNode root = response.asJson();
if (response.getStatus() != 200) {
if (response.getStatus() != Http.Status.OK) {
return internalServerError(root.get("message").asText("Connection refused"));
}
if (root instanceof ArrayNode node) {
Expand Down
3 changes: 2 additions & 1 deletion app/impl/CalendarHandlerImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.google.common.collect.Lists;
import controllers.admin.SettingsController;
import controllers.iop.transfer.api.ExternalReservationHandler;
import exceptions.NotFoundException;
Expand Down Expand Up @@ -369,7 +370,7 @@ private static Collection<Interval> allSlots(
LocalDate date
) {
Collection<Interval> intervals = new ArrayList<>();
List<ExamStartingHour> startingHours = room.getExamStartingHours();
List<ExamStartingHour> startingHours = Lists.newArrayList(room.getExamStartingHours());
if (startingHours.isEmpty()) {
// Default to 1 hour slots that start at the hour
startingHours = createDefaultStartingHours(room.getLocalTimezone());
Expand Down
9 changes: 7 additions & 2 deletions app/miscellaneous/datetime/DateTimeHandlerImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import static org.joda.time.DateTimeConstants.MILLIS_PER_DAY;

import com.google.common.collect.Lists;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
Expand Down Expand Up @@ -265,10 +266,14 @@ public int getTimezoneOffset(DateTime date) {
public List<OpeningHours> getWorkingHoursForDate(LocalDate date, ExamRoom room) {
List<OpeningHours> workingHours = getDefaultWorkingHours(date, room);
List<Interval> extensionEvents = mergeSlots(
getExceptionEvents(room.getCalendarExceptionEvents(), date, RestrictionType.NON_RESTRICTIVE)
getExceptionEvents(
Lists.newArrayList(room.getCalendarExceptionEvents()),
date,
RestrictionType.NON_RESTRICTIVE
)
);
List<Interval> restrictionEvents = mergeSlots(
getExceptionEvents(room.getCalendarExceptionEvents(), date, RestrictionType.RESTRICTIVE)
getExceptionEvents(Lists.newArrayList(room.getCalendarExceptionEvents()), date, RestrictionType.RESTRICTIVE)
);
List<OpeningHours> availableHours = new ArrayList<>();
if (!extensionEvents.isEmpty()) {
Expand Down
9 changes: 6 additions & 3 deletions app/miscellaneous/scala/DbApiHelper.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

package miscellaneous.scala

import io.ebean.{ExpressionList, Model}
import io.ebean.{ExpressionList, Model, Query}

import scala.jdk.CollectionConverters._
import scala.jdk.OptionConverters._
import scala.jdk.CollectionConverters.*
import scala.jdk.OptionConverters.*

trait DbApiHelper:
extension [T <: Model](el: ExpressionList[T])
Expand All @@ -19,3 +19,6 @@ trait DbApiHelper:
// Apparently we can end up having Some(null) if not mapped like this.
// As a result we get None as expected.
extension [T](o: Option[T]) def nonNull: Option[T] = o.flatMap(Option(_))

extension [T <: Model](q: Query[T])
def distinct: Set[T] = q.findSet().asScala.toSet
15 changes: 10 additions & 5 deletions app/miscellaneous/scala/JavaApiHelper.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@

package miscellaneous.scala

import com.fasterxml.jackson.databind.ObjectMapper
import io.ebean.Model
import play.api.mvc.Result
import play.libs.{Json => JavaJson}
import play.api.libs.json.jackson.PlayJsonMapperModule
import play.api.libs.json.{JsValue, JsonConfig}
import play.libs.Json as JavaJson

trait JavaApiHelper:

extension [T <: Model](model: T) def toJson: String = JavaJson.toJson(model).toString
private val jsonSettings = JsonConfig.settings
private val mapper = new ObjectMapper().registerModule(new PlayJsonMapperModule(jsonSettings))

extension [T <: Model](model: Iterable[T]) def toJson: String = JavaJson.toJson(model).toString
extension [T <: Model](model: T)
def asJson: JsValue = mapper.readValue(JavaJson.toJson(model).toString, classOf[JsValue])

extension (result: Result) def asJson: Result = result.as("application/json")
extension [T <: Model](model: Iterable[T])
def asJson: JsValue = mapper.readValue(JavaJson.toJson(model).toString, classOf[JsValue])
13 changes: 13 additions & 0 deletions app/models/calendar/DefaultWorkingHours.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Temporal;
import jakarta.persistence.TemporalType;
import java.util.Objects;
import miscellaneous.datetime.DateTimeAdapter;
import models.base.GeneratedIdentityModel;
import models.facility.ExamRoom;
Expand Down Expand Up @@ -87,4 +88,16 @@ private Interval toInterval() {
return new Interval(startTime.withDate(LocalDate.now()), endTime.withDate(LocalDate.now()));
}
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof DefaultWorkingHours that)) return false;
return Objects.equals(id, that.id);
}

@Override
public int hashCode() {
return Objects.hashCode(id);
}
}
Loading

0 comments on commit ce14c79

Please sign in to comment.