Skip to content

Commit

Permalink
Merge pull request #33 from hotungkhanh/kan-80/timetable-update
Browse files Browse the repository at this point in the history
KAN-80 feat(timetables): implement backend logic for timetable update
  • Loading branch information
FlyingPufferFish authored Oct 10, 2024
2 parents 07bd394 + d521329 commit a49578e
Show file tree
Hide file tree
Showing 43 changed files with 1,614 additions and 1,183 deletions.
2 changes: 0 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,6 @@ jobs:
QUARKUS_DATASOURCE_PASSWORD: ${{secrets.QUARKUS_DATASOURCE_PASSWORD}}
QUARKUS_DATASOURCE_JDBC_URL: ${{secrets.QUARKUS_DATASOURCE_JDBC_URL}}

FRONTEND_USERNAME: ${{secrets.FRONTEND_USERNAME}}
FRONTEND_PASSWORD: ${{secrets.FRONTEND_PASSWORD}}

deploy:
needs: build
Expand Down
124 changes: 124 additions & 0 deletions backend/og-pom.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.acme</groupId>
<artifactId>backend</artifactId>
<version>1.0.0-SNAPSHOT</version>

<properties>
<compiler-plugin.version>3.13.0</compiler-plugin.version>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
<quarkus.platform.version>3.13.3</quarkus.platform.version>
<skipITs>true</skipITs>
<surefire-plugin.version>3.2.5</surefire-plugin.version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>${quarkus.platform.group-id}</groupId>
<artifactId>${quarkus.platform.artifact-id}</artifactId>
<version>${quarkus.platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>${quarkus.platform.group-id}</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>${quarkus.platform.version}</version>
<extensions>true</extensions>
<executions>
<execution>
<goals>
<goal>build</goal>
<goal>generate-code</goal>
<goal>generate-code-tests</goal>
<goal>native-image-agent</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>${compiler-plugin.version}</version>
<configuration>
<parameters>true</parameters>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<configuration>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
<configuration>
<systemPropertyVariables>
<native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>

<profiles>
<profile>
<id>native</id>
<activation>
<property>
<name>native</name>
</property>
</activation>
<properties>
<skipITs>false</skipITs>
<quarkus.native.enabled>true</quarkus.native.enabled>
</properties>
</profile>
</profiles>
</project>
6 changes: 5 additions & 1 deletion backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,11 @@
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security-jpa</artifactId>
<artifactId>quarkus-smallrye-metrics</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-narayana-jta</artifactId>
</dependency>
</dependencies>

Expand Down
230 changes: 230 additions & 0 deletions backend/src/main/java/org/acme/TimetableResource.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
package org.acme;

import ai.timefold.solver.core.api.solver.SolverManager;
import io.smallrye.mutiny.Uni;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;

import org.acme.domain.Room;
import org.acme.domain.Student;
import org.acme.domain.Timetable;
import org.acme.domain.Unit;

import java.time.DayOfWeek;
import java.time.Duration;
import java.time.LocalTime;
import java.util.List;
import java.util.concurrent.ExecutionException;

import java.util.UUID;

/**
* Entry to the timetabling program.
* Receives a timetabling problem and outputs the solution
* with the best optimised scores according to the provided constraints.
*
* @author Jet Edge
*/
@Path("/timetabling")
public class TimetableResource {

@Inject
SolverManager<Timetable, String> solverManager;

@POST
@Transactional
public Timetable handleRequest(Timetable problem) throws ExecutionException, InterruptedException {
UUID uuid = UUID.randomUUID();
String uuidAsString = uuid.toString();

System.out.println("Your UUID is: " + uuidAsString);
String name = "Job" + uuidAsString;

findByCampusAndDelete(problem.campusName);

// generate solution timetable with TimeFold Solver
Timetable solution = solverManager.solve(name, problem).getFinalBestSolution();

// store the solution timetable to the database
solution.persist();

return solution;
}

@Path("/view")
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<Timetable> view() {
return Timetable.listAll();
}


/**
* Takes a list of Units as input to updates exisiting timetable once users
* make drag-and-drop modifications in frontend
*
* @param updatedUnits List of all Unit updates
* @return A Response object indicating status
*/
@Path("/update")
@PUT
@Transactional
@Consumes(MediaType.APPLICATION_JSON)
public Response timetableUpdate(List<Unit> updatedUnits) {
List<Unit> dbUnits = Unit.listAll();
for (Unit updatedUnit : updatedUnits) {
if (unitUpdate(updatedUnit, dbUnits) == null) {
return Response.serverError().build();
}
}
return Response.ok().build();
}


/**
* Update the time/location of a unit
*
* @param updatedUnit Unit object with updated time/location
* @param dbUnits List of all Unit objects in the database
* @return The updated Unit, null otherwise
*/
@PUT
@Transactional
@Consumes(MediaType.APPLICATION_JSON)
public Unit unitUpdate(Unit updatedUnit, List<Unit> dbUnits) {

// find existing Unit obj in db by unitId
for (Unit unit : dbUnits) {
if (updatedUnit.unitId == unit.unitId) {
assert(unit.isPersistent());
// update day of week
unit.dayOfWeek = updatedUnit.dayOfWeek;
// update startTime
unit.startTime = updatedUnit.startTime;
// if room is different
if (unit.room.roomCode != updatedUnit.room.roomCode
|| !unit.room.buildingId.equals(updatedUnit.room.buildingId)) {
// update room
Room newRoom;
if ((newRoom = findRoom(updatedUnit.room)) == null) {
throw new WebApplicationException("Room with building ID " + updatedUnit.room.buildingId + ", and room code " + updatedUnit.room.roomCode + " not found", 404);
}
unit.room = newRoom;
}
return unit;
}
}
return null;
}

/**
* Find existing Room object inside the database with
* desired buildingId and roomCode
*
* @param inputRoom Room object with buildingId and roomCode of desired Room
* @return Room object meeting the input criteria, null otherwise
*/
public Room findRoom(Room inputRoom) {
List<Room> rooms = Room.listAll();
for (Room room : rooms) {
if (room.roomCode.equals(inputRoom.roomCode)
&& room.buildingId.equals(inputRoom.buildingId)) {
assert(room.isPersistent());
return room;
}
}
return null;
}

public void findByCampusAndDelete(String campusName) {
List<Timetable> timetables = Timetable.listAll();
for (Timetable timetable : timetables) {
if (campusName.equals(timetable.campusName)) {
timetable.delete();
}
}
}

@GET
@Transactional
@Produces(MediaType.APPLICATION_JSON)
public Timetable solveExample() throws ExecutionException, InterruptedException {

Student a = new Student("a");
Student b = new Student("b");
Student c = new Student("c");
Student d = new Student("d");
Student e = new Student("e");
Student f = new Student("f");
Student g = new Student("g");
Student h = new Student("h");
Student i = new Student("i");

Room r1 = new Room("Room1", "building A", "campus A", 2, true);
Room r2 = new Room("Room2", "building B", "campus A", 4, false);
Room r3 = new Room("Room3", "building A", "campus B", 4, false);

Unit u1 = new Unit(1, "1", "Course A", Duration.ofHours(2), List.of(a, b), true);
Unit u2 = new Unit(2, "2", "Course A", Duration.ofHours(2), List.of(a, c, d, e), true);
Unit u3 = new Unit(3, "3", "Course B", Duration.ofHours(2), List.of(f, g, h, i), false);
Unit u4 = new Unit(4, "4", "Course C", Duration.ofHours(2), List.of(a, b), false);

var problem = new Timetable("Campus A",
List.of(
u1, u2, u3, u4
// new Unit(5, "5", Duration.ofHours(2), List.of(c, d, e)),
// new Unit(6, "6", Duration.ofHours(2), List.of(f, g, h, i))
),

List.of(
DayOfWeek.MONDAY,
DayOfWeek.TUESDAY,
DayOfWeek.WEDNESDAY
// DayOfWeek.THURSDAY,
// DayOfWeek.FRIDAY
),

List.of(
LocalTime.of(15, 0)
// LocalTime.of(17, 0)
// LocalTime.of(16,0),
// LocalTime.of(23,0)
),
List.of(r1, r2, r3)
);

/*
* During this solving phase, new Unit objects will be created with the
* alloted date and Room assignment.
*
* Currently, the 'old' Unit objects in the 'problem' variable and the
* 'new' Unit objects in the 'solution' variable are stored as different
* Units in the database due to our inability to control the behaviour
* of solverManager.solve
*
* i.e. after solving, there will be 2 copies of each Unit in the
* database, where the 'old' Unit has the list of students but no
* timetable assignment, while the 'new' Unit does not have the list
* of students enrolled, but does have the assigned date and room
*/

findByCampusAndDelete(problem.campusName);

Timetable solution = solverManager.solve("job 1", problem).getFinalBestSolution();

solution.persist();
// saves the solution timetable and all related entities to database

return solution;
}

}
Loading

0 comments on commit a49578e

Please sign in to comment.