From c7180ecbd94e54587be2875f29a493d04da6205a Mon Sep 17 00:00:00 2001 From: pooriya Date: Fri, 25 Jul 2025 07:07:24 +0330 Subject: [PATCH 01/12] the ignor file be updated --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index e8fece7..07fe838 100644 --- a/.gitignore +++ b/.gitignore @@ -205,3 +205,7 @@ cython_debug/ marimo/_static/ marimo/_lsp/ __marimo__/ + + +# docs of local picture +docs \ No newline at end of file From bbdd60c7e73096c0322c04843f27376f14c2966e Mon Sep 17 00:00:00 2001 From: pooriya Date: Fri, 25 Jul 2025 07:57:39 +0330 Subject: [PATCH 02/12] the model structuer be updated --- syncortexGA/models/timetable_model.py | 142 +++++++++++++++----------- 1 file changed, 80 insertions(+), 62 deletions(-) diff --git a/syncortexGA/models/timetable_model.py b/syncortexGA/models/timetable_model.py index c8db99e..3eccf5b 100644 --- a/syncortexGA/models/timetable_model.py +++ b/syncortexGA/models/timetable_model.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel, model_validator, StringConstraints +from pydantic import BaseModel, model_validator, StringConstraints, ConfigDict from typing import List, Optional, Literal, Annotated # Accepts only time strings in 24-hour format like "08:00", "23:59" @@ -39,15 +39,31 @@ class AlternatingSessionPattern(BaseModel): paired_course_id: int +class LabSessionPattern(BaseModel): + """ + Represents a lab session pattern that occurs weekly at a fixed time. + This is used for lab courses that do not have a session pattern. + """ + + slot: TimeSlot + + @model_validator(mode="after") + def check_single_slot(cls, model): + if not model.slot: + raise ValueError("LabSessionPattern must have exactly 1 slot") + return model + + class SessionPattern(BaseModel): """ Wraps either a fixed or alternating session pattern. Only one of the patterns must be set based on the type. """ - type: Literal["fixed", "alternating"] + type: Literal["fixed", "alternating", "lab"] fixed_pattern: Optional[FixedSessionPattern] = None alternating_pattern: Optional[AlternatingSessionPattern] = None + lab_pattern: Optional[LabSessionPattern] = None @model_validator(mode="after") def validate_only_one_pattern(cls, model): @@ -59,56 +75,24 @@ def validate_only_one_pattern(cls, model): raise ValueError( "For type='alternating', " "only alternating_pattern must be set" ) + elif model.type == "lab": + if ( + not model.lab_pattern + or model.fixed_pattern + or model.alternating_pattern + ): + raise ValueError("For type='lab', only lab_pattern must be set") return model -# ========== Course ========== -class Course(BaseModel): - """ - Represents a course that may or may not include a lab. - Lab courses must define a lab slot and cannot have a session pattern. - Non-lab courses must define a session pattern and cannot have a lab slot. - """ +# ========== StudentGroup ========== +class StudentGroup(BaseModel): + """Represents a group of students (e.g., a class or cohort).""" id: int - name: str - code: str - instructor_id: int - has_lab: bool = False - lab_slot: Optional[TimeSlot] = None - lab_room_id: Optional[int] = None - session_pattern: Optional[SessionPattern] = None - - @model_validator(mode="after") - def validate_course(cls, model): - if model.has_lab: - if not model.lab_slot: - raise ValueError("Lab slot must be defined for lab courses") - if model.session_pattern: - raise ValueError("Lab courses must not have session pattern") - else: - if not model.session_pattern: - raise ValueError("Non-lab courses must have a session pattern") - if model.lab_slot: - raise ValueError("Non-lab courses must not have lab slot") - return model - - -class CourseOffering(BaseModel): - """ - Represents the offering of a course by an instructor for a specific subgroup. - Allows multiple instructors to offer the same course to different subgroups, - and a single instructor to teach the same course in multiple subgroups. - - Attributes: - course_id (int): Identifier of the course being offered. - instructor_id (int): Identifier of the instructor teaching the course. - sub_group (int): Sub-group number within the course offering. Default is 1. - """ - - course_id: int - instructor_id: int - sub_group: int = 1 # Default subgroup is 1 + name: str # e.g., "CS-401" + major: str # e.g., "Computer Engineering" + entry_year: int # e.g., 2022 # ========== Student ========== @@ -117,17 +101,7 @@ class Student(BaseModel): full_name: str student_number: str # e.g., "401234567" - group_id: int # Refers to StudentGroup.id - - -# ========== StudentGroup ========== -class StudentGroup(BaseModel): - """Represents a group of students (e.g., a class or cohort).""" - - id: int - name: str # e.g., "CS-401" - major: str # e.g., "Computer Engineering" - entry_year: int # e.g., 2022 + group_id: StudentGroup # Refers to StudentGroup.id # ========== Instructor ========== @@ -150,17 +124,61 @@ class Room(BaseModel): is_lab: bool = False +# ========== Course ========== +class Course(BaseModel): + """ + Represents a course that may or may not include a lab. + Lab courses must define a lab slot and cannot have a session pattern. + Non-lab courses must define a session pattern and cannot have a lab slot. + """ + + id: int + name: str + code: str + # has_lab: bool = False + # lab_room_id: Optional[int] = None + session_pattern: Optional[SessionPattern] = None + + +class CourseOffering(BaseModel): + """ + Represents the offering of a course by an instructor for a specific subgroup. + Allows multiple instructors to offer the same course to different subgroups, + and a single instructor to teach the same course in multiple subgroups. + + Attributes: + course_id (int): Identifier of the course being offered. + instructor_id (int): Identifier of the instructor teaching the course. + sub_group (int): Sub-group number within the course offering. Default is 1. + """ + + course_id: Course + instructor_id: Instructor + sub_group: int = 1 # Default subgroup is 1 + + # ========== Scheduled Session & Timetable ========== class ScheduledSession(BaseModel): """Represents a scheduled class session (theory or lab).""" - course_id: int - group_id: int - instructor_id: int - room_id: int + course_id: Course + group_id: StudentGroup + instructor_id: Instructor + room_id: Room slot: TimeSlot type: Literal["theory", "lab"] + @model_validator(mode="after") + def validate_slot(cls, model): + if model.type == "lab" and not model.course_id.session_pattern: + raise ValueError("Lab sessions must have a lab session pattern defined") + if model.type == "theory" and not model.course_id.session_pattern: + raise ValueError("Theory sessions must have a session pattern defined") + if model.type not in ("theory", "lab"): + raise ValueError("Session type must be either 'theory' or 'lab'") + return model + + class Timetable(BaseModel): """Represents the final schedule as a list of sessions.""" From 95caca82252544a8743ca0cd17910a380bdd7c9c Mon Sep 17 00:00:00 2001 From: pooriya Date: Fri, 25 Jul 2025 07:57:54 +0330 Subject: [PATCH 03/12] new test case be set for models --- tests/unit/test_models.py | 459 ++++++++++++++++++-------------------- 1 file changed, 219 insertions(+), 240 deletions(-) diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py index f2d2a9c..2d20d73 100644 --- a/tests/unit/test_models.py +++ b/tests/unit/test_models.py @@ -4,23 +4,122 @@ TimeSlot, FixedSessionPattern, AlternatingSessionPattern, + LabSessionPattern, SessionPattern, - Course, - CourseOffering, - Student, StudentGroup, + Student, Instructor, Room, + Course, + CourseOffering, ScheduledSession, Timetable, ) -# ====== TimeSlot Tests ====== +# ==== Helper functions to create reusable sample data ==== + + +def sample_timeslot(day="Monday", start="08:00", end="10:00"): + return TimeSlot(day=day, start=start, end=end) + + +def sample_student_group(): + return StudentGroup( + id=1, name="CS-401", major="Computer Engineering", entry_year=2022 + ) + + +def sample_instructor(): + return Instructor( + id=1, + full_name="Dr. Smith", + available_slots=[sample_timeslot()], + preferred_slots=[sample_timeslot("Tuesday", "10:00", "12:00")], + ) + + +def sample_room(): + return Room(id=1, name="Room 101", capacity=40, is_lab=False) + + +def sample_fixed_session_pattern(): + return FixedSessionPattern( + slots=[ + sample_timeslot("Saturday", "08:00", "10:00"), + sample_timeslot("Monday", "10:00", "12:00"), + ] + ) + + +def sample_alternating_session_pattern(): + return AlternatingSessionPattern( + fixed_slot=sample_timeslot("Sunday", "08:00", "10:00"), + alternating_slot=sample_timeslot("Tuesday", "10:00", "12:00"), + alternating_mode="odd", + paired_course_id=123, + ) + + +def sample_lab_session_pattern(): + return LabSessionPattern(slot=sample_timeslot("Wednesday", "14:00", "16:00")) + + +def sample_session_pattern_fixed(): + return SessionPattern(type="fixed", fixed_pattern=sample_fixed_session_pattern()) + + +def sample_session_pattern_alternating(): + return SessionPattern( + type="alternating", alternating_pattern=sample_alternating_session_pattern() + ) + + +def sample_session_pattern_lab(): + return SessionPattern(type="lab", lab_pattern=sample_lab_session_pattern()) +def sample_course_with_pattern(session_pattern=None): + return Course( + id=1, + name="Advanced Mathematics", + code="MATH401", + session_pattern=session_pattern, + ) + + +def sample_course_without_pattern(): + return Course(id=2, name="Philosophy", code="PHIL101", session_pattern=None) + + +def sample_course_offering(course=None, instructor=None, sub_group=1): + return CourseOffering( + course_id=course, instructor_id=instructor, sub_group=sub_group + ) + + +def sample_scheduled_session(): + course = sample_course_with_pattern(sample_session_pattern_fixed()) + group = sample_student_group() + instructor = sample_instructor() + room = sample_room() + slot = sample_timeslot() + return ScheduledSession( + course_id=course, + group_id=group, + instructor_id=instructor, + room_id=room, + slot=slot, + type="theory", + ) + + +# ==== Tests ==== + + +# ----- TimeSlot ----- def test_timeslot_valid(): - ts = TimeSlot(day="Monday", start="08:00", end="10:00") + ts = sample_timeslot() assert ts.day == "Monday" assert ts.start == "08:00" assert ts.end == "10:00" @@ -28,31 +127,23 @@ def test_timeslot_valid(): def test_timeslot_invalid_day(): with pytest.raises(ValidationError): - TimeSlot(day="Friday", start="08:00", end="10:00") + TimeSlot(day="Friday", start="08:00", end="10:00") # Friday not allowed def test_timeslot_invalid_time_format(): with pytest.raises(ValidationError): - TimeSlot(day="Monday", start="8am", end="10am") - - -# ====== FixedSessionPattern Tests ====== + TimeSlot(day="Monday", start="8am", end="10:00") +# ----- FixedSessionPattern ----- def test_fixed_session_pattern_valid(): - slots = [ - TimeSlot(day="Saturday", start="08:00", end="10:00"), - TimeSlot(day="Monday", start="10:00", end="12:00"), - ] - pattern = FixedSessionPattern(slots=slots) + pattern = sample_fixed_session_pattern() assert len(pattern.slots) == 2 def test_fixed_session_pattern_invalid_count(): with pytest.raises(ValidationError): - FixedSessionPattern( - slots=[TimeSlot(day="Saturday", start="08:00", end="10:00")] - ) + FixedSessionPattern(slots=[sample_timeslot()]) def test_fixed_session_pattern_empty(): @@ -60,52 +151,52 @@ def test_fixed_session_pattern_empty(): FixedSessionPattern(slots=[]) -# ====== AlternatingSessionPattern Tests ====== - - +# ----- AlternatingSessionPattern ----- def test_alternating_session_pattern_valid(): - pattern = AlternatingSessionPattern( - fixed_slot=TimeSlot(day="Sunday", start="08:00", end="10:00"), - alternating_slot=TimeSlot(day="Tuesday", start="10:00", end="12:00"), - alternating_mode="odd", - paired_course_id=123, - ) - assert pattern.alternating_mode == "odd" + pattern = sample_alternating_session_pattern() + assert pattern.alternating_mode in ("odd", "even") def test_alternating_session_pattern_invalid_mode(): with pytest.raises(ValidationError): AlternatingSessionPattern( - fixed_slot=TimeSlot(day="Sunday", start="08:00", end="10:00"), - alternating_slot=TimeSlot(day="Tuesday", start="10:00", end="12:00"), - alternating_mode="week3", - paired_course_id=123, + fixed_slot=sample_timeslot(), + alternating_slot=sample_timeslot(), + alternating_mode="weekly", + paired_course_id=1, ) -# ====== SessionPattern Tests ====== +# ----- LabSessionPattern ----- +def test_lab_session_pattern_valid(): + pattern = sample_lab_session_pattern() + assert pattern.slot.day == "Wednesday" + + +def test_lab_session_pattern_invalid_missing_slot(): + with pytest.raises(ValidationError): + LabSessionPattern(slot=None) +# ----- SessionPattern ----- def test_session_pattern_fixed_valid(): - fixed = FixedSessionPattern( - slots=[ - TimeSlot(day="Saturday", start="08:00", end="10:00"), - TimeSlot(day="Monday", start="10:00", end="12:00"), - ] - ) - sp = SessionPattern(type="fixed", fixed_pattern=fixed) + sp = sample_session_pattern_fixed() assert sp.type == "fixed" + assert sp.fixed_pattern is not None + assert sp.alternating_pattern is None + assert sp.lab_pattern is None def test_session_pattern_alternating_valid(): - alt = AlternatingSessionPattern( - fixed_slot=TimeSlot(day="Sunday", start="08:00", end="10:00"), - alternating_slot=TimeSlot(day="Tuesday", start="10:00", end="12:00"), - alternating_mode="even", - paired_course_id=99, - ) - sp = SessionPattern(type="alternating", alternating_pattern=alt) + sp = sample_session_pattern_alternating() assert sp.type == "alternating" + assert sp.alternating_pattern is not None + + +def test_session_pattern_lab_valid(): + sp = sample_session_pattern_lab() + assert sp.type == "lab" + assert sp.lab_pattern is not None def test_session_pattern_invalid_type(): @@ -113,216 +204,134 @@ def test_session_pattern_invalid_type(): SessionPattern(type="random") -def test_session_pattern_mismatch_fixed(): - fixed = FixedSessionPattern( - slots=[ - TimeSlot(day="Saturday", start="08:00", end="10:00"), - TimeSlot(day="Monday", start="10:00", end="12:00"), - ] - ) - alt = AlternatingSessionPattern( - fixed_slot=fixed.slots[0], - alternating_slot=fixed.slots[1], - alternating_mode="odd", - paired_course_id=5, - ) - with pytest.raises(ValidationError): - SessionPattern(type="fixed", fixed_pattern=fixed, alternating_pattern=alt) +def test_session_pattern_mismatched_patterns(): + fixed = sample_fixed_session_pattern() + alt = sample_alternating_session_pattern() + lab = sample_lab_session_pattern() + # fixed type but alternating pattern set + with pytest.raises(ValidationError): + SessionPattern( + type="fixed", fixed_pattern=fixed, alternating_pattern=alt, lab_pattern=lab + ) -def test_session_pattern_mismatch_alternating(): - fixed = FixedSessionPattern( - slots=[ - TimeSlot(day="Saturday", start="08:00", end="10:00"), - TimeSlot(day="Monday", start="10:00", end="12:00"), - ] - ) + # alternating type but fixed pattern set with pytest.raises(ValidationError): SessionPattern(type="alternating", fixed_pattern=fixed) - -# ====== Course Tests ====== - - -def test_course_lab_valid(): - course = Course( - id=1, - name="Physics Lab", - code="PHY101", - instructor_id=10, - has_lab=True, - lab_slot=TimeSlot(day="Wednesday", start="14:00", end="16:00"), - lab_room_id=3, - lab_instructor_id=15, - ) - assert course.has_lab is True - - -def test_course_lab_missing_lab_slot(): + # lab type but fixed pattern set with pytest.raises(ValidationError): - Course( - id=2, name="Chemistry Lab", code="CHEM101", instructor_id=11, has_lab=True - ) - + SessionPattern(type="lab", fixed_pattern=fixed) -def test_course_lab_with_session_pattern(): - lab_slot = TimeSlot(day="Wednesday", start="14:00", end="16:00") - session_pattern = SessionPattern( - type="fixed", - fixed_pattern=FixedSessionPattern( - slots=[ - TimeSlot(day="Monday", start="08:00", end="10:00"), - TimeSlot(day="Thursday", start="08:00", end="10:00"), - ] - ), - ) + # lab type but alternating pattern set with pytest.raises(ValidationError): - Course( - id=3, - name="Bio Lab", - code="BIO101", - instructor_id=12, - has_lab=True, - lab_slot=lab_slot, - session_pattern=session_pattern, - ) + SessionPattern(type="lab", alternating_pattern=alt) -def test_course_non_lab_valid(): - session_pattern = SessionPattern( - type="fixed", - fixed_pattern=FixedSessionPattern( - slots=[ - TimeSlot(day="Monday", start="08:00", end="10:00"), - TimeSlot(day="Thursday", start="08:00", end="10:00"), - ] - ), - ) - course = Course( - id=4, - name="Math", - code="MATH101", - instructor_id=13, - has_lab=False, - session_pattern=session_pattern, - ) - assert course.has_lab is False +# ----- StudentGroup ----- +def test_student_group_valid(): + group = sample_student_group() + assert group.name == "CS-401" -def test_course_non_lab_missing_session_pattern(): - with pytest.raises(ValidationError): - Course(id=5, name="History", code="HIST101", instructor_id=14, has_lab=False) +# ----- Student ----- +def test_student_valid(): + group = sample_student_group() + student = Student( + full_name="Alice Smith", student_number="401234567", group_id=group + ) + assert student.group_id.id == group.id -def test_course_non_lab_with_lab_slot(): +def test_student_missing_fields(): with pytest.raises(ValidationError): - Course( - id=6, - name="English", - code="ENG101", - instructor_id=15, - has_lab=False, - lab_slot=TimeSlot(day="Wednesday", start="14:00", end="16:00"), - ) - - -# ====== Student Tests ====== + Student(full_name="Bob") -def test_student_valid(): - student = Student(full_name="Alice Smith", student_number="401234567", group_id=100) - assert student.student_number == "401234567" +# ----- Instructor ----- +def test_instructor_valid(): + instructor = sample_instructor() + assert instructor.full_name == "Dr. Smith" + assert len(instructor.available_slots) == 1 -def test_student_missing_field(): - with pytest.raises(ValidationError): - Student(full_name="Bob") +def test_instructor_optional_preferred_slots(): + inst = Instructor(id=2, full_name="Jane Doe", available_slots=[sample_timeslot()]) + assert inst.preferred_slots is None -# ====== StudentGroup Tests ====== +# ----- Room ----- +def test_room_valid(): + room = sample_room() + assert room.capacity == 40 + assert room.is_lab is False -def test_student_group_valid(): - group = StudentGroup( - id=1, name="CS-401", major="Computer Engineering", entry_year=2023 - ) - assert group.name == "CS-401" +def test_room_lab_flag_true(): + room = Room(id=10, name="Lab 1", capacity=20, is_lab=True) + assert room.is_lab is True -# ====== Instructor Tests ====== +# ----- Course ----- +def test_course_with_valid_session_pattern(): + course = sample_course_with_pattern(sample_session_pattern_fixed()) + assert course.session_pattern is not None -def test_instructor_valid(): - instructor = Instructor( - id=1, - full_name="Dr. John", - available_slots=[TimeSlot(day="Monday", start="08:00", end="12:00")], - preferred_slots=[TimeSlot(day="Monday", start="10:00", end="12:00")], - ) - assert instructor.full_name == "Dr. John" +def test_course_without_session_pattern(): + course = sample_course_without_pattern() + assert course.session_pattern is None -def test_instructor_no_preferred(): - instructor = Instructor( - id=2, - full_name="Dr. Jane", - available_slots=[TimeSlot(day="Tuesday", start="08:00", end="10:00")], - ) - assert instructor.preferred_slots is None +# ----- CourseOffering ----- +def test_course_offering_valid_default_subgroup(): + course = sample_course_with_pattern(sample_session_pattern_fixed()) + instructor = sample_instructor() + offering = sample_course_offering(course=course, instructor=instructor) + assert offering.sub_group == 1 -# ====== Room Tests ====== +def test_course_offering_valid_custom_subgroup(): + course = sample_course_with_pattern(sample_session_pattern_fixed()) + instructor = sample_instructor() + offering = sample_course_offering(course=course, instructor=instructor, sub_group=3) + assert offering.sub_group == 3 -def test_room_valid(): - room = Room(id=1, name="Room 101", capacity=30, is_lab=False) - assert room.capacity == 30 +def test_course_offering_missing_course_id(): + instructor = sample_instructor() + with pytest.raises(ValidationError): + CourseOffering(instructor_id=instructor) -def test_room_lab_flag(): - room = Room(id=2, name="Lab 1", capacity=20, is_lab=True) - assert room.is_lab is True +def test_course_offering_missing_instructor_id(): + course = sample_course_with_pattern(sample_session_pattern_fixed()) + with pytest.raises(ValidationError): + CourseOffering(course_id=course) -# ====== ScheduledSession Tests ====== +def test_course_offering_invalid_subgroup_type(): + course = sample_course_with_pattern(sample_session_pattern_fixed()) + instructor = sample_instructor() + with pytest.raises(ValidationError): + CourseOffering(course_id=course, instructor_id=instructor, sub_group="two") +# ----- ScheduledSession ----- def test_scheduled_session_valid(): - session = ScheduledSession( - course_id=1, - group_id=10, - instructor_id=5, - room_id=3, - slot=TimeSlot(day="Thursday", start="10:00", end="12:00"), - type="theory", - ) - assert session.type == "theory" + session = sample_scheduled_session() + assert session.type in ("theory", "lab") def test_scheduled_session_invalid_type(): + sess = sample_scheduled_session() with pytest.raises(ValidationError): - ScheduledSession( - course_id=1, - group_id=10, - instructor_id=5, - room_id=3, - slot=TimeSlot(day="Thursday", start="10:00", end="12:00"), - type="exam", - ) - - -# ====== Timetable Tests ====== + sess.type = "exam" # invalid +# ----- Timetable ----- def test_timetable_valid(): - session = ScheduledSession( - course_id=1, - group_id=10, - instructor_id=5, - room_id=3, - slot=TimeSlot(day="Thursday", start="10:00", end="12:00"), - type="lab", - ) + session = sample_scheduled_session() timetable = Timetable(sessions=[session]) assert len(timetable.sessions) == 1 @@ -330,33 +339,3 @@ def test_timetable_valid(): def test_timetable_empty(): timetable = Timetable(sessions=[]) assert timetable.sessions == [] - - -def test_course_offering_valid_defaults(): - offering = CourseOffering(course_id=10, instructor_id=5) - assert offering.course_id == 10 - assert offering.instructor_id == 5 - assert offering.sub_group == 1 # Default value - - -def test_course_offering_valid_with_subgroup(): - offering = CourseOffering(course_id=20, instructor_id=7, sub_group=3) - assert offering.course_id == 20 - assert offering.instructor_id == 7 - assert offering.sub_group == 3 - - -def test_course_offering_invalid_missing_course_id(): - with pytest.raises(ValidationError): - CourseOffering(instructor_id=1) - - -def test_course_offering_invalid_missing_instructor_id(): - with pytest.raises(ValidationError): - CourseOffering(course_id=1) - - -def test_course_offering_invalid_sub_group_type(): - # sub_group should be int - with pytest.raises(ValidationError): - CourseOffering(course_id=1, instructor_id=1, sub_group="two") From 661817ea49e4626570d707cbb7e06bb169fe8df4 Mon Sep 17 00:00:00 2001 From: pooriya Date: Mon, 28 Jul 2025 04:53:12 +0330 Subject: [PATCH 04/12] the final model structuer be set --- syncortexGA/models/timetable_model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/syncortexGA/models/timetable_model.py b/syncortexGA/models/timetable_model.py index 3eccf5b..28691ed 100644 --- a/syncortexGA/models/timetable_model.py +++ b/syncortexGA/models/timetable_model.py @@ -172,9 +172,9 @@ class ScheduledSession(BaseModel): def validate_slot(cls, model): if model.type == "lab" and not model.course_id.session_pattern: raise ValueError("Lab sessions must have a lab session pattern defined") - if model.type == "theory" and not model.course_id.session_pattern: + elif model.type == "theory" and not model.course_id.session_pattern: raise ValueError("Theory sessions must have a session pattern defined") - if model.type not in ("theory", "lab"): + elif model.type not in ("theory", "lab"): raise ValueError("Session type must be either 'theory' or 'lab'") return model From 1063924ffb25e5c49d19cb2d51540f424ab28725 Mon Sep 17 00:00:00 2001 From: pooriya Date: Mon, 28 Jul 2025 04:53:28 +0330 Subject: [PATCH 05/12] the test of models be updated --- tests/unit/test_models.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py index 2d20d73..7255b52 100644 --- a/tests/unit/test_models.py +++ b/tests/unit/test_models.py @@ -324,9 +324,20 @@ def test_scheduled_session_valid(): def test_scheduled_session_invalid_type(): - sess = sample_scheduled_session() + course = sample_course_with_pattern(sample_session_pattern_fixed()) + group = sample_student_group() + instructor = sample_instructor() + room = sample_room() + slot = sample_timeslot() with pytest.raises(ValidationError): - sess.type = "exam" # invalid + sess = ScheduledSession( + course_id=course, + group_id=group, + instructor_id=instructor, + room_id=room, + slot=slot, + type="exam", + ) # ----- Timetable ----- From 58f5e6599dcafec8b078eaa9f6e6235f538a3de3 Mon Sep 17 00:00:00 2001 From: pooriya Date: Mon, 28 Jul 2025 04:58:00 +0330 Subject: [PATCH 06/12] the code be clean format --- syncortexGA/models/timetable_model.py | 1 - 1 file changed, 1 deletion(-) diff --git a/syncortexGA/models/timetable_model.py b/syncortexGA/models/timetable_model.py index 28691ed..0a6cc8c 100644 --- a/syncortexGA/models/timetable_model.py +++ b/syncortexGA/models/timetable_model.py @@ -179,7 +179,6 @@ def validate_slot(cls, model): return model - class Timetable(BaseModel): """Represents the final schedule as a list of sessions.""" From 0da787c975637a4ec0f1c9d121d6e9d767e3c4d7 Mon Sep 17 00:00:00 2001 From: pooriya Date: Sat, 2 Aug 2025 02:55:56 +0330 Subject: [PATCH 07/12] the test file names be changed --- tests/unit/{test_constraints.py => test_hard_constraints.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/unit/{test_constraints.py => test_hard_constraints.py} (100%) diff --git a/tests/unit/test_constraints.py b/tests/unit/test_hard_constraints.py similarity index 100% rename from tests/unit/test_constraints.py rename to tests/unit/test_hard_constraints.py From 793ca5b0f945e67b44de7614eb3c90cc8be25026 Mon Sep 17 00:00:00 2001 From: pooriya Date: Sat, 2 Aug 2025 02:56:17 +0330 Subject: [PATCH 08/12] the model validation be updated --- syncortexGA/models/timetable_model.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/syncortexGA/models/timetable_model.py b/syncortexGA/models/timetable_model.py index 0a6cc8c..2a727c4 100644 --- a/syncortexGA/models/timetable_model.py +++ b/syncortexGA/models/timetable_model.py @@ -176,6 +176,11 @@ def validate_slot(cls, model): raise ValueError("Theory sessions must have a session pattern defined") elif model.type not in ("theory", "lab"): raise ValueError("Session type must be either 'theory' or 'lab'") + + if model.type == "lab" != model.course_id.session_pattern.type: + raise ValueError("The Type of Pattern must match the session type") + if model.type == "theory" and model.course_id.session_pattern.type == "lab": + raise ValueError("Theory sessions cannot have a lab session pattern") return model From 82202864e0c68664f0c27a81d6a6581ad05fc2d2 Mon Sep 17 00:00:00 2001 From: pooriya Date: Sat, 2 Aug 2025 02:57:31 +0330 Subject: [PATCH 09/12] the first hard constraintion be implement with H1 name --- syncortexGA/constraints/hard_constraints.py | 27 +++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/syncortexGA/constraints/hard_constraints.py b/syncortexGA/constraints/hard_constraints.py index e69de29..a7322ff 100644 --- a/syncortexGA/constraints/hard_constraints.py +++ b/syncortexGA/constraints/hard_constraints.py @@ -0,0 +1,27 @@ +from syncortexGA.models.timetable_model import Timetable + + +def check_h1_instructor_conflicts(timetable: Timetable) -> bool: + """ + Ensures no instructor is scheduled for more than one session at the same time + and only in their available time slots. + """ + seen = {} # key: (instructor_id, day, start, end), value: count + + for session in timetable.sessions: + instructor = session.instructor_id + slot = session.slot + + # 1. Check if the session is in instructor's available slots + if slot not in instructor.available_slots: + return False + + # 2. Check if this slot already assigned to this instructor + key = (instructor.id, slot.day, slot.start, slot.end) + if key in seen: + return ( + False # Conflict detected: instructor already has session in this slot + ) + seen[key] = True + + return True From 1205e029f6400d3a44d10bd0e050d31b5db2e8a9 Mon Sep 17 00:00:00 2001 From: pooriya Date: Sat, 2 Aug 2025 02:57:44 +0330 Subject: [PATCH 10/12] the test for H1 be set --- tests/unit/test_hard_constraints.py | 178 ++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) diff --git a/tests/unit/test_hard_constraints.py b/tests/unit/test_hard_constraints.py index e69de29..5dd5e1e 100644 --- a/tests/unit/test_hard_constraints.py +++ b/tests/unit/test_hard_constraints.py @@ -0,0 +1,178 @@ +import pytest +from syncortexGA.models.timetable_model import ( + TimeSlot, + Instructor, + StudentGroup, + Room, + Course, + SessionPattern, + FixedSessionPattern, + LabSessionPattern, + ScheduledSession, + Timetable, +) +from syncortexGA.constraints.hard_constraints import check_h1_instructor_conflicts + + +# ---------- Common Fixtures ---------- + + +@pytest.fixture +def common_setup(): + """ + Creates a standard setup of group, room, instructor, course, and time slots + used in multiple tests. + """ + group = StudentGroup(id=1, name="CS-A", major="CS", entry_year=2023) + + room = Room(id=1, name="Room 101", capacity=40, is_lab=False) + + slot1 = TimeSlot(day="Saturday", start="08:00", end="10:00") + slot2 = TimeSlot(day="Saturday", start="10:00", end="12:00") + slot3 = TimeSlot(day="Sunday", start="08:00", end="10:00") # Not available + + instructor = Instructor( + id=1, + full_name="Dr. Smith", + available_slots=[slot1, slot2], # Only slot1 and slot2 are allowed + preferred_slots=None, + ) + + course = Course( + id=1, + name="Algorithms", + code="CS101", + session_pattern=SessionPattern( + type="fixed", fixed_pattern=FixedSessionPattern(slots=[slot1, slot2]) + ), + ) + + return group, room, instructor, course, slot1, slot2, slot3 + + +# ---------- Test Cases for H1 ---------- + + +def test_h1_valid_schedule(common_setup): + """ + Test: Instructor has two valid sessions with no slot conflict. + Expect: H1 passes (returns True) + """ + group, room, instructor, course, slot1, slot2, _ = common_setup + + sessions = [ + ScheduledSession( + course_id=course, + group_id=group, + instructor_id=instructor, + room_id=room, + slot=slot1, + type="theory", + ), + ScheduledSession( + course_id=course, + group_id=group, + instructor_id=instructor, + room_id=room, + slot=slot2, + type="theory", + ), + ] + + timetable = Timetable(sessions=sessions) + assert check_h1_instructor_conflicts(timetable) is True + + +def test_h1_invalid_due_to_slot_not_in_available(common_setup): + """ + Test: Instructor is scheduled in a slot not listed in available_slots. + Expect: H1 fails (returns False) + """ + group, room, instructor, course, _, _, slot3 = common_setup # slot3 is invalid + + session = ScheduledSession( + course_id=course, + group_id=group, + instructor_id=instructor, + room_id=room, + slot=slot3, + type="theory", + ) + + timetable = Timetable(sessions=[session]) + assert check_h1_instructor_conflicts(timetable) is False + + +def test_h1_invalid_due_to_slot_conflict(common_setup): + """ + Test: Instructor has two sessions scheduled in the exact same time slot. + Expect: H1 fails (returns False) + """ + group, room, instructor, course, slot1, _, _ = common_setup + + sessions = [ + ScheduledSession( + course_id=course, + group_id=group, + instructor_id=instructor, + room_id=room, + slot=slot1, + type="theory", + ), + ScheduledSession( + course_id=course, + group_id=group, + instructor_id=instructor, + room_id=room, + slot=slot1, + type="theory", + ), # Conflict + ] + + timetable = Timetable(sessions=sessions) + assert check_h1_instructor_conflicts(timetable) is False + + +def test_h1_multiple_instructors_no_conflict(): + """ + Test: Two instructors teaching different sessions at the same time. + Expect: H1 passes (returns True) — conflict is per instructor, not globally + """ + slot = TimeSlot(day="Monday", start="10:00", end="12:00") + + instructor1 = Instructor(id=1, full_name="A", available_slots=[slot]) + instructor2 = Instructor(id=2, full_name="B", available_slots=[slot]) + + group = StudentGroup(id=1, name="G1", major="CS", entry_year=2023) + room = Room(id=1, name="Room A", capacity=30, is_lab=False) + + course = Course( + id=1, + name="CS", + code="CS101", + session_pattern=SessionPattern( + type="lab", lab_pattern=LabSessionPattern(slot=slot) + ), + ) + + sessions = [ + ScheduledSession( + course_id=course, + group_id=group, + instructor_id=instructor1, + room_id=room, + slot=slot, + type="lab", + ), + ScheduledSession( + course_id=course, + group_id=group, + instructor_id=instructor2, + room_id=room, + slot=slot, + type="lab", + ), + ] + + timetable = Timetable(sessions=sessions) + assert check_h1_instructor_conflicts(timetable) is True From c70bce0e95f535c37686bd74843cb9191d19e809 Mon Sep 17 00:00:00 2001 From: pooriya Date: Sat, 2 Aug 2025 03:00:24 +0330 Subject: [PATCH 11/12] the code be clean for flake8 --- tests/unit/test_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py index 7255b52..7dcc1fa 100644 --- a/tests/unit/test_models.py +++ b/tests/unit/test_models.py @@ -330,7 +330,7 @@ def test_scheduled_session_invalid_type(): room = sample_room() slot = sample_timeslot() with pytest.raises(ValidationError): - sess = ScheduledSession( + ScheduledSession( course_id=course, group_id=group, instructor_id=instructor, From 4508c234e6f6e963eb7dd92a2f39ce8f56c5166a Mon Sep 17 00:00:00 2001 From: pooriya Date: Sat, 2 Aug 2025 03:00:58 +0330 Subject: [PATCH 12/12] the workflow be updated --- .github/workflows/main.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d17db44..6633398 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,8 +1,6 @@ name: CI on: - push: - branches: [ main ] pull_request: branches: [ main ]