-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #33 from hotungkhanh/kan-80/timetable-update
KAN-80 feat(timetables): implement backend logic for timetable update
- Loading branch information
Showing
43 changed files
with
1,614 additions
and
1,183 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
|
||
} |
Oops, something went wrong.