diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1bfdd24 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "conventionalCommits.scopes": [ + "database" + ] +} \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore index 91a800a..b1e5177 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -22,7 +22,7 @@ bin/ nb-configuration.xml # Visual Studio Code -.vscode +.vscode/ .factorypath # OSX diff --git a/backend/src/main/java/org/acme/TimetableResource.java b/backend/src/main/java/org/acme/TimetableResource.java index 61d86e0..4d15599 100644 --- a/backend/src/main/java/org/acme/TimetableResource.java +++ b/backend/src/main/java/org/acme/TimetableResource.java @@ -2,6 +2,7 @@ import ai.timefold.solver.core.api.solver.SolverManager; import jakarta.inject.Inject; +import jakarta.transaction.Transactional; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; @@ -42,8 +43,16 @@ public Timetable handleRequest(Timetable problem) throws ExecutionException, Int return solution; } + @Path("/view") @GET @Produces(MediaType.APPLICATION_JSON) + public List view() { + return Timetable.listAll(); + } + + @GET + @Transactional + @Produces(MediaType.APPLICATION_JSON) public Timetable solveExample() throws ExecutionException, InterruptedException { Student a = new Student("a"); @@ -60,12 +69,14 @@ public Timetable solveExample() throws ExecutionException, InterruptedException Room r2 = new Room("Room2", 4, false); Room r3 = new Room("Room3", 4, false); + Unit u1 = new Unit(1, "1", Duration.ofHours(2), List.of(a, b), true); + Unit u2 = new Unit(2, "2", Duration.ofHours(2), List.of(a, c, d, e), true); + Unit u3 = new Unit(3, "3", Duration.ofHours(2), List.of(f, g, h, i), false); + Unit u4 = new Unit(4, "4", Duration.ofHours(2), List.of(a, b), false); + var problem = new Timetable( List.of( - new Unit(1, "1", Duration.ofHours(2), List.of(a, b), true), - new Unit(2, "2", Duration.ofHours(2), List.of(a, c, d, e), true), - new Unit(3, "3", Duration.ofHours(2), List.of(f, g, h, i), false), - new Unit(4, "4", Duration.ofHours(2), List.of(a, b), false) + 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)) ), @@ -87,8 +98,25 @@ public Timetable solveExample() throws ExecutionException, InterruptedException 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 + */ Timetable solution = solverManager.solve("job 1", problem).getFinalBestSolution(); + solution.persist(); + // saves the solution timetable and all related entities to database + return solution; } diff --git a/backend/src/main/java/org/acme/schooltimetabling/domain/Campus.java b/backend/src/main/java/org/acme/domain/Campus.java similarity index 73% rename from backend/src/main/java/org/acme/schooltimetabling/domain/Campus.java rename to backend/src/main/java/org/acme/domain/Campus.java index d4c49cc..a08f6e1 100644 --- a/backend/src/main/java/org/acme/schooltimetabling/domain/Campus.java +++ b/backend/src/main/java/org/acme/domain/Campus.java @@ -1,4 +1,4 @@ -package org.acme.schooltimetabling.domain; +package org.acme.domain; // import java.util.List; import jakarta.persistence.*; @@ -9,11 +9,9 @@ public class Campus extends PanacheEntity { public String name; - // empty constructor public Campus() { } - // constructor with name input public Campus(String name) { this.name = name; } diff --git a/backend/src/main/java/org/acme/schooltimetabling/domain/CampusResource.java b/backend/src/main/java/org/acme/domain/CampusResource.java similarity index 94% rename from backend/src/main/java/org/acme/schooltimetabling/domain/CampusResource.java rename to backend/src/main/java/org/acme/domain/CampusResource.java index 7b380c9..f93e55e 100644 --- a/backend/src/main/java/org/acme/schooltimetabling/domain/CampusResource.java +++ b/backend/src/main/java/org/acme/domain/CampusResource.java @@ -1,4 +1,4 @@ -package org.acme.schooltimetabling.domain; +package org.acme.domain; import java.util.List; diff --git a/backend/src/main/java/org/acme/domain/Room.java b/backend/src/main/java/org/acme/domain/Room.java index 57c7bbd..4ed0f00 100644 --- a/backend/src/main/java/org/acme/domain/Room.java +++ b/backend/src/main/java/org/acme/domain/Room.java @@ -1,18 +1,53 @@ package org.acme.domain; +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonManagedReference; + import ai.timefold.solver.core.api.domain.lookup.PlanningId; +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.OneToMany; /** * Represents a room. * * @author Jet Edge */ -public class Room { +@Entity +public class Room extends PanacheEntity { @PlanningId - private String id; + public String roomCode; + + public String buildingId; + + public int capacity; + + public boolean isLab; - private int capacity; - private boolean isLab; + /** + * A list of units that are taught in a Room + */ + @JsonIgnoreProperties("room") + @OneToMany(mappedBy = "room", orphanRemoval = false) + @JsonManagedReference + @JsonIgnore + public List units = new ArrayList(); + + /** + * A list of timetables that the Room is a part of + */ + @JsonIgnoreProperties("rooms") + @ManyToMany(mappedBy = "rooms", fetch = FetchType.LAZY, cascade = {CascadeType.ALL}) + @JsonManagedReference + @JsonIgnore + public List timetables = new ArrayList(); public Room() { } @@ -25,17 +60,17 @@ public Room() { * @param isLab Whether the room is a laboratory. */ public Room(String id, int capacity, boolean isLab) { - this.id = id; + this.roomCode = id; this.capacity = capacity; this.isLab = isLab; } - public String getId() { - return id; + public String getRoomCode() { + return roomCode; } - public void setId(String id) { - this.id = id; + public void setRoomCode(String id) { + this.roomCode = id; } public int getCapacity() { diff --git a/backend/src/main/java/org/acme/domain/RoomResource.java b/backend/src/main/java/org/acme/domain/RoomResource.java new file mode 100644 index 0000000..c86a1a9 --- /dev/null +++ b/backend/src/main/java/org/acme/domain/RoomResource.java @@ -0,0 +1,31 @@ +package org.acme.domain; + +import java.util.List; + +import jakarta.transaction.Transactional; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +@Path("/rooms") +public class RoomResource { + + @GET + @Produces(MediaType.APPLICATION_JSON) + public List list() { + return Room.listAll(); + } + + @POST + @Transactional + @Consumes(MediaType.APPLICATION_JSON) + public Response createCampus(Room room) { + room.persist(); + return Response.status(Response.Status.CREATED).entity(room).build(); + } + +} diff --git a/backend/src/main/java/org/acme/domain/Student.java b/backend/src/main/java/org/acme/domain/Student.java index 42093dd..e4f10f7 100644 --- a/backend/src/main/java/org/acme/domain/Student.java +++ b/backend/src/main/java/org/acme/domain/Student.java @@ -1,17 +1,43 @@ package org.acme.domain; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonManagedReference; + +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; + /** * Represents a student. * * @author Jet Edge */ -public class Student { +@Entity +public class Student extends PanacheEntity{ // String studentID; - String name; + public String name; + + @JsonIgnoreProperties("students") + @ManyToMany(fetch = FetchType.LAZY, cascade = {CascadeType.ALL}) + @JoinTable( + name = "student_unit", + joinColumns = @JoinColumn(name = "student_id"), + inverseJoinColumns = @JoinColumn(name = "unit_id") + ) + @JsonManagedReference + @JsonIgnore + public List units = new ArrayList(); public Student() { } diff --git a/backend/src/main/java/org/acme/domain/StudentResource.java b/backend/src/main/java/org/acme/domain/StudentResource.java new file mode 100644 index 0000000..996d015 --- /dev/null +++ b/backend/src/main/java/org/acme/domain/StudentResource.java @@ -0,0 +1,29 @@ +package org.acme.domain; + +import java.util.List; + +import jakarta.transaction.Transactional; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +@Path("/students") +public class StudentResource { + @GET + @Produces(MediaType.APPLICATION_JSON) + public List list() { + return Student.listAll(); + } + + @POST + @Transactional + @Consumes(MediaType.APPLICATION_JSON) + public Response createCampus(Student student) { + student.persist(); + return Response.status(Response.Status.CREATED).entity(student).build(); + } +} diff --git a/backend/src/main/java/org/acme/domain/Timetable.java b/backend/src/main/java/org/acme/domain/Timetable.java index b6f9826..cacbc33 100644 --- a/backend/src/main/java/org/acme/domain/Timetable.java +++ b/backend/src/main/java/org/acme/domain/Timetable.java @@ -6,34 +6,78 @@ import ai.timefold.solver.core.api.domain.solution.ProblemFactCollectionProperty; import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider; import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import jakarta.persistence.CascadeType; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.Transient; import java.time.DayOfWeek; import java.time.LocalTime; import java.util.ArrayList; import java.util.List; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonManagedReference; + /** * Represents a timetable, the solution from the program. * * @author Jet Edge */ +@Entity @PlanningSolution -public class Timetable { - +public class Timetable extends PanacheEntity { + @ElementCollection @ValueRangeProvider - private List daysOfWeek; + public List daysOfWeek; + + @ElementCollection @ValueRangeProvider - private List startTimes; + public List startTimes; + /* + * Rooms can belong to multiple timetables because timetables are generated + * on a per-campus basis, and although each room can only belong to one + * campus, the user may choose to generate multiple timetables for each + * campus, hence the many-to-many relationship + */ + @JsonIgnoreProperties("timetables") + @ManyToMany(fetch = FetchType.LAZY, cascade = {CascadeType.ALL}) + @JoinTable( + name = "room_timetable", + joinColumns = @JoinColumn(name = "timetable_id"), + inverseJoinColumns = @JoinColumn(name = "room_id") + ) + @JsonManagedReference @ProblemFactCollectionProperty + @JsonIgnore @ValueRangeProvider - private List rooms; + public List rooms; + /* + * Units can belong to multiple timetables because again, timetables are + * generated on a per-campus basis, but each unit can be taught across + * multiple campuses, so may appear in multiple timetables + */ + @JsonIgnoreProperties("timetables") + @ManyToMany(fetch = FetchType.LAZY, cascade = {CascadeType.ALL}) + @JoinTable( + name = "unit_timetable", + joinColumns = @JoinColumn(name = "timetable_id"), + inverseJoinColumns = @JoinColumn(name = "unit_id") + ) + @JsonManagedReference @PlanningEntityCollectionProperty - private List units; + public List units; @PlanningScore - private HardSoftScore score; + public HardSoftScore score; public Timetable() { @@ -48,6 +92,7 @@ public Timetable() { public Timetable(List units, List startTimes) { this.units = units; this.startTimes = startTimes; + this.setUnitTimetable(); } /** @@ -61,6 +106,8 @@ public Timetable(List units, List startTimes, List rooms) this.units = units; this.startTimes = startTimes; this.rooms = rooms; + this.setUnitTimetable(); + this.setRoomTimetable(); } /** @@ -118,6 +165,18 @@ public void setScore(HardSoftScore score) { this.score = score; } + public void setUnitTimetable() { + for (Unit unit : this.units) { + unit.timetables.add(this); + } + } + + public void setRoomTimetable() { + for (Room room : this.rooms) { + room.timetables.add(this); + } + } + /** * Identify conflicting units having common students at the same time. * diff --git a/backend/src/main/java/org/acme/domain/Unit.java b/backend/src/main/java/org/acme/domain/Unit.java index c6b955b..677a2e6 100644 --- a/backend/src/main/java/org/acme/domain/Unit.java +++ b/backend/src/main/java/org/acme/domain/Unit.java @@ -3,34 +3,77 @@ import ai.timefold.solver.core.api.domain.entity.PlanningEntity; import ai.timefold.solver.core.api.domain.lookup.PlanningId; import ai.timefold.solver.core.api.domain.variable.PlanningVariable; +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.ManyToOne; import java.time.DayOfWeek; import java.time.Duration; import java.time.LocalTime; +import java.util.ArrayList; import java.util.List; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonManagedReference; + /** * Represents a unit. * * @author Jet Edge */ +@Entity @PlanningEntity -public class Unit { +public class Unit extends PanacheEntity { + + // TODO: change unit to be the owner, rather than the student being owner + @JsonIgnoreProperties("units") + @ManyToMany(mappedBy = "units", fetch = FetchType.LAZY, cascade = {CascadeType.ALL}) + @JsonManagedReference + public List students; - private List students; @PlanningId - private int unitID; - private String name; - private Duration duration; + public int unitID; + + public String name; + + public Duration duration; + @PlanningVariable - private DayOfWeek dayOfWeek; + public DayOfWeek dayOfWeek; + @PlanningVariable - private LocalTime startTime; + public LocalTime startTime; + + /* + * currently each unit only has 1 'slot' on the timetable, so it can only + * be associated with one room, but in the final product, we would most + * likely have to change this to a many-to-many relationship + * i.e. list of Rooms, because we might want to separate lecture/tutorial + * etc. + */ + @JsonIgnoreProperties("units") + @ManyToOne(cascade = {CascadeType.ALL}) + @JoinColumn(name = "room_id") + @JsonManagedReference @PlanningVariable - private Room room; + public Room room; - private boolean wantsLab; + public boolean wantsLab; + + /* + * The timetables that the Unit object belongs to + */ + @JsonIgnoreProperties("units") + @ManyToMany(mappedBy = "units", fetch = FetchType.LAZY, cascade = {CascadeType.ALL}) + @JsonManagedReference + @JsonIgnore + public List timetables = new ArrayList(); public Unit() { } @@ -48,6 +91,7 @@ public Unit(int unitID, String name, Duration duration, List students) this.name = name; this.duration = duration; this.students = students; + this.setStudentsUnits(); } /** @@ -65,6 +109,7 @@ public Unit(int unitID, String name, Duration duration, List students, this.duration = duration; this.students = students; this.wantsLab = wantsLab; + this.setStudentsUnits(); } /** @@ -129,6 +174,7 @@ public void setStartTime(LocalTime startTime) { } public LocalTime getEnd() { + if (startTime == null) return null; return startTime.plus(duration); } @@ -138,6 +184,17 @@ public List getStudents() { public void setStudents(List students) { this.students = students; + this.setStudentsUnits();; + } + + /** + * This is to ensure that many-to-many relationship can be properly setup + * in the database + */ + public void setStudentsUnits() { + for (Student student : this.students) { + student.units.add(this); + } } /** @@ -155,6 +212,7 @@ public Room getRoom() { public void setRoom(Room room) { this.room = room; + this.room.units.add(this); } public boolean isWantsLab() { @@ -164,4 +222,5 @@ public boolean isWantsLab() { public void setWantsLab(boolean wantsLab) { this.wantsLab = wantsLab; } + } \ No newline at end of file diff --git a/backend/src/main/java/org/acme/domain/UnitResource.java b/backend/src/main/java/org/acme/domain/UnitResource.java new file mode 100644 index 0000000..8455d46 --- /dev/null +++ b/backend/src/main/java/org/acme/domain/UnitResource.java @@ -0,0 +1,29 @@ +package org.acme.domain; + +import java.util.List; + +import jakarta.transaction.Transactional; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +@Path("/units") +public class UnitResource { + @GET + @Produces(MediaType.APPLICATION_JSON) + public List list() { + return Unit.listAll(); + } + + @POST + @Transactional + @Consumes(MediaType.APPLICATION_JSON) + public Response createCampus(Unit unit) { + unit.persist(); + return Response.status(Response.Status.CREATED).entity(unit).build(); + } +} diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 43e49f3..7d26f06 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -13,4 +13,4 @@ quarkus.datasource.password = ${QUARKUS_DATASOURCE_PASSWORD} quarkus.datasource.jdbc.url = ${QUARKUS_DATASOURCE_JDBC_URL} # drop and create the database at startup (use `update` to only update the schema) -quarkus.hibernate-orm.database.generation = update \ No newline at end of file +quarkus.hibernate-orm.database.generation = create-drop \ No newline at end of file