From 45fabcc4dad18bf538a1431ae0b7daa50e605d76 Mon Sep 17 00:00:00 2001 From: sparsetable Date: Thu, 5 Feb 2026 06:23:10 +0000 Subject: [PATCH 1/5] Fix ID types, use InternalModel, rename some models --- campus/model/timetable.py | 46 ++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/campus/model/timetable.py b/campus/model/timetable.py index 5fd8331e..6854758f 100644 --- a/campus/model/timetable.py +++ b/campus/model/timetable.py @@ -26,7 +26,7 @@ from campus.common import schema from campus.common.utils import uid -from .base import Model +from .base import Model, InternalModel from . import constraints # NOTE: Assumes reusing the same object is not an issue @@ -38,7 +38,7 @@ ## They will be relevant every for every allocation. ## @dataclass(eq=False, kw_only=True) -class WeekDay(Model): +class WeekDay(InternalModel): """ Describes a day in a repeating timetable. Assumption: This will stay constant across all allocations. @@ -47,13 +47,13 @@ class WeekDay(Model): label (String): (cosmetic purposes: 'Mon A', 'Tue A', ... 'Mon B', 'Tue B', ..., 'Sat', 'Sun') index (Integer): index 0 is earliest (eg. Mon A), followed by Tues A, etc. """ - id: schema.CampusID = unique_field + id: schema.Integer label: schema.String index: schema.Integer @dataclass(eq=False, kw_only=True) -class TimeSlot(Model): +class TimeSlot(InternalModel): """ Timeslot which repeats across all `WeekDay`s Assumption: This will stay constant across all allocations. @@ -64,9 +64,7 @@ class TimeSlot(Model): end_time (DateTime): ISO8601 index (Integer): index 0 is earliest slot (eg. 0730), followed by 0800 at idx 1, etc. """ - id: schema.CampusID = field(default_factory=( - lambda: uid.generate_category_uid("timetable", length=8) - )) + id: schema.Integer label: schema.String start_time: schema.DateTime = unique_field # ISO8601 end_time: schema.DateTime = unique_field # ISO8601 @@ -74,7 +72,7 @@ class TimeSlot(Model): @dataclass(eq=False, kw_only=True) -class Venue(Model): +class TimetableVenue(InternalModel): """ Describes a single venue. We have a set of venues that remain across years, except additions. @@ -83,14 +81,12 @@ class Venue(Model): Fields: label (String): (e.g. '03-39', 'i-Space 1', ..., follow XML when possible) """ - id: schema.CampusID = field(default_factory=( - lambda: uid.generate_category_uid("timetable", length=8) - )) + id: schema.Integer label: schema.String @dataclass(eq=False, kw_only=True) -class VenueTimeSlot(Model): +class VenueTimeSlot(InternalModel): """ Imagine each venue has its own timetable. This is one timeslot on such a timetable, representing an intersection of @@ -102,14 +98,12 @@ class VenueTimeSlot(Model): Fields: weekday_id (CampusID): FK referencing a WeekDay.id timeslot_id (CampusID): FK referencing a TimeSlot.id - venue_id (CampusID): FK referencing a Venue.id + venue_id (CampusID): FK referencing a TimetableVenue.id """ - id: schema.CampusID = field(default_factory=( - lambda: uid.generate_category_uid("timetable", length=8) - )) - weekday_id: schema.CampusID - timeslot_id: schema.CampusID - venue_id: schema.CampusID + id: schema.Integer + weekday_id: schema.Integer + timeslot_id: schema.Integer + ttvenue_id: schema.Integer __constraints__ = constraints.Unique("weekday_id", "timeslot_id", "venue_id") ## Sections that are only relevant to a specific allocation. ## @@ -129,14 +123,14 @@ class LessonGroup(Model): label (String): Label brought over from xml, like 2527-COM """ id: schema.CampusID = field(default_factory=( - lambda: uid.generate_category_uid("timetable", length=8) + lambda: uid.generate_category_uid("timetable-group", length=8) )) filename: schema.String label: schema.String @dataclass(eq=False, kw_only=True) -class LessonGroupMember(Model): +class LessonGroupMember(InternalModel): """ Represents a single member of a LessonGroup. For example, 2510-Math will have: @@ -151,9 +145,7 @@ class LessonGroupMember(Model): ade_participant (String): XML id (aka TTCode or teacher_id). We have a unique mapping of these IDs to nyjc email etc., for each allocation. """ - id: schema.CampusID = field(default_factory=( - lambda: uid.generate_category_uid("timetable", length=8) - )) + id: schema.Integer filename: schema.String lessongroup_id: schema.CampusID ade_participant: schema.String @@ -161,7 +153,7 @@ class LessonGroupMember(Model): @dataclass(eq=False, kw_only=True) -class Timetable(Model): +class TimetableEntry(Model): """ A timetable represents a single lesson for a LessonGroup at some VenueTimeSlot. @@ -172,9 +164,9 @@ class Timetable(Model): venuetimeslot_id (CampusID): FK referencing a VenueTimeSlot.id """ id: schema.CampusID = field(default_factory=( - lambda: uid.generate_category_uid("timetable", length=8) + lambda: uid.generate_category_uid("timetable-entry", length=16) )) filename: schema.String lessongroup_id: schema.CampusID - venuetimeslot_id: schema.CampusID + venuetimeslot_id: schema.Integer __constraints__ = constraints.Unique("lessongroup_id", "venuetimeslot_id", "filename") From b906d5ac93c4974db3d544157531b6079e5f8810 Mon Sep 17 00:00:00 2001 From: sparsetable Date: Thu, 5 Feb 2026 06:27:32 +0000 Subject: [PATCH 2/5] Add Timetable model --- campus/model/timetable.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/campus/model/timetable.py b/campus/model/timetable.py index 6854758f..e09811de 100644 --- a/campus/model/timetable.py +++ b/campus/model/timetable.py @@ -119,13 +119,13 @@ class LessonGroup(Model): directly from the XML. It seems we cannot seperate class and subject. Fields: - filename (String): Allocation xml filename which this entry is relevant to + timetable_id (CampusID): FK referencing the timetable this lessongroup is relevant to label (String): Label brought over from xml, like 2527-COM """ id: schema.CampusID = field(default_factory=( lambda: uid.generate_category_uid("timetable-group", length=8) )) - filename: schema.String + timetable_id: schema.CampusID label: schema.String @@ -140,7 +140,7 @@ class LessonGroupMember(InternalModel): 2510-Chem will have its own set of entries, even if the participant is duplicated. Fields: - filename (String): Allocation xml filename which this entry is relevant to + timetable_id (CampusID): FK referencing the timetable this lessongroup is relevant to lessongroup_id (CampusID): FK referencing a LessonGroup.id ade_participant (String): XML id (aka TTCode or teacher_id). We have a unique mapping of these IDs to nyjc email etc., for each allocation. @@ -155,11 +155,11 @@ class LessonGroupMember(InternalModel): @dataclass(eq=False, kw_only=True) class TimetableEntry(Model): """ - A timetable represents a single lesson for a LessonGroup + A timetable entry represents a single lesson for a LessonGroup at some VenueTimeSlot. Fields: - filename (String): Allocation xml filename which this entry is relevant to + timetable_id (CampusID): FK referencing the timetable this lessongroup is relevant to lessongroup_id (CampusID): FK referencing a LessonGroup.id venuetimeslot_id (CampusID): FK referencing a VenueTimeSlot.id """ @@ -170,3 +170,22 @@ class TimetableEntry(Model): lessongroup_id: schema.CampusID venuetimeslot_id: schema.Integer __constraints__ = constraints.Unique("lessongroup_id", "venuetimeslot_id", "filename") + +## Represents an allocation itself ## +class Timetable(Model): + """ + The timetable represents an allocation. + It refers to a new set of classes, people and class timings + imported with each new timetable XML. + + Fields: + filename (String): The XML filename it was imported from + start (DateTime): 00:00 on the first day the timetable comes into effect + end_date (DateTime): 00:00 on the day the last day the timetable is effective + """ + id: schema.CampusID = field(default_factory=( + lambda: uid.generate_category_uid("timetable", length=8) + )) + filename: schema.String + start_date: schema.DateTime # 00:00 on the day it begins + end_date: schema.DateTime # 00:00 on the day it begins \ No newline at end of file From fbd92245bd987da52c94bbc45de639536fb9ab67 Mon Sep 17 00:00:00 2001 From: sparsetable Date: Thu, 5 Feb 2026 06:37:02 +0000 Subject: [PATCH 3/5] Clean up --- campus/model/timetable.py | 43 ++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/campus/model/timetable.py b/campus/model/timetable.py index e09811de..3f2f1c93 100644 --- a/campus/model/timetable.py +++ b/campus/model/timetable.py @@ -15,9 +15,17 @@ imported with a new timetable. It refers to one of these: https://github.com/nyjc-computing/nyxchange-timetable-v2/blob/data-schema/tt_file/tt_xml_docs.md -This year's timetable and last year's timetable are two seperate allocations. +This year's timetable and last year's timetable are two separate allocations. The data for an allocation is stored in some .xml file, named `filename` +Internal models not returned through APIs: +- WeekDay, TimeSlot, VenueTimeSlot +- use Integer IDs for their own id, and are referenced as such + +Models that can be returned through APIs: +- LessonGroup, TimetableEntry, Timetable +- use CampusID (UUID) for their own id, and are referenced as such + TODO: Update doc link after migration """ @@ -96,19 +104,19 @@ class VenueTimeSlot(InternalModel): Must be automatically generated for each Venue for all WeekDay, TimeSlot. Fields: - weekday_id (CampusID): FK referencing a WeekDay.id - timeslot_id (CampusID): FK referencing a TimeSlot.id - venue_id (CampusID): FK referencing a TimetableVenue.id + weekday_id (Integer): FK referencing a WeekDay.id + timeslot_id (Integer): FK referencing a TimeSlot.id + venue_id (Integer): FK referencing a TimetableVenue.id """ id: schema.Integer weekday_id: schema.Integer timeslot_id: schema.Integer - ttvenue_id: schema.Integer + venue_id: schema.Integer __constraints__ = constraints.Unique("weekday_id", "timeslot_id", "venue_id") ## Sections that are only relevant to a specific allocation. ## ## New entries are created for each allocation. ## -## Which allocation the entry is relevant to is . ## +## Which allocation the entry is relevant to is in timetable_id. ## @dataclass(eq=False, kw_only=True) class LessonGroup(Model): @@ -116,7 +124,7 @@ class LessonGroup(Model): This represents a specific subject taught to a class. Eg. Chem, taught to 2510. (eg. stored as '2510-CM') These are labelled as an inconsistently formatted string, imported - directly from the XML. It seems we cannot seperate class and subject. + directly from the XML. It seems we cannot separate class and subject. Fields: timetable_id (CampusID): FK referencing the timetable this lessongroup is relevant to @@ -127,6 +135,7 @@ class LessonGroup(Model): )) timetable_id: schema.CampusID label: schema.String + __constraints__ = constraints.Unique("timetable_id", "label") @dataclass(eq=False, kw_only=True) @@ -146,10 +155,10 @@ class LessonGroupMember(InternalModel): to nyjc email etc., for each allocation. """ id: schema.Integer - filename: schema.String + timetable_id: schema.CampusID lessongroup_id: schema.CampusID ade_participant: schema.String - __constraints__ = constraints.Unique("lessongroup_id", "ade_participant", "filename") + __constraints__ = constraints.Unique("lessongroup_id", "ade_participant", "timetable_id") @dataclass(eq=False, kw_only=True) @@ -161,17 +170,18 @@ class TimetableEntry(Model): Fields: timetable_id (CampusID): FK referencing the timetable this lessongroup is relevant to lessongroup_id (CampusID): FK referencing a LessonGroup.id - venuetimeslot_id (CampusID): FK referencing a VenueTimeSlot.id + venuetimeslot_id (Integer): FK referencing a VenueTimeSlot.id """ id: schema.CampusID = field(default_factory=( lambda: uid.generate_category_uid("timetable-entry", length=16) )) - filename: schema.String + timetable_id: schema.CampusID lessongroup_id: schema.CampusID venuetimeslot_id: schema.Integer - __constraints__ = constraints.Unique("lessongroup_id", "venuetimeslot_id", "filename") + __constraints__ = constraints.Unique("lessongroup_id", "venuetimeslot_id", "timetable_id") ## Represents an allocation itself ## +@dataclass(eq=False, kw_only=True) class Timetable(Model): """ The timetable represents an allocation. @@ -180,12 +190,13 @@ class Timetable(Model): Fields: filename (String): The XML filename it was imported from - start (DateTime): 00:00 on the first day the timetable comes into effect - end_date (DateTime): 00:00 on the day the last day the timetable is effective + start_date (DateTime): 00:00 on the first day the timetable comes into effect + end_date (DateTime): 00:00 on the last day the timetable is effective """ id: schema.CampusID = field(default_factory=( lambda: uid.generate_category_uid("timetable", length=8) )) filename: schema.String - start_date: schema.DateTime # 00:00 on the day it begins - end_date: schema.DateTime # 00:00 on the day it begins \ No newline at end of file + start_date: schema.DateTime + end_date: schema.DateTime + __constraints__ = constraints.Unique("filename") \ No newline at end of file From c77e7b5ad1b1c5cbf4b0cc6f74b851097ccfae3f Mon Sep 17 00:00:00 2001 From: sparsetable Date: Thu, 5 Feb 2026 06:47:57 +0000 Subject: [PATCH 4/5] Add to init --- campus/model/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/campus/model/__init__.py b/campus/model/__init__.py index 3a81c584..e329a2fe 100644 --- a/campus/model/__init__.py +++ b/campus/model/__init__.py @@ -35,6 +35,12 @@ "User", "UserCredentials", "Vault", + "WeekDay", + "TimeSlot", + "VenueTimeSlot", + "LessonGroup", + "TimetableEntry", + "Timetable", ] from .assignment import Assignment, ClassroomLink, Question @@ -49,3 +55,4 @@ from .submission import Feedback, Response, Submission from .user import User from .vault import Vault +from .timetable import WeekDay, TimeSlot, VenueTimeSlot, LessonGroup, TimetableEntry, Timetable From dabfb8848091588f2710b84982514c1342dd7b25 Mon Sep 17 00:00:00 2001 From: sparsetable Date: Thu, 5 Feb 2026 06:50:41 +0000 Subject: [PATCH 5/5] Update resource to pacify typechecker --- campus/api/resources/timetable.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/campus/api/resources/timetable.py b/campus/api/resources/timetable.py index f97de491..fd3db2ce 100644 --- a/campus/api/resources/timetable.py +++ b/campus/api/resources/timetable.py @@ -12,15 +12,14 @@ timetable_storage = campus.storage.get_collection("timetables") -def _from_record(record: dict) -> campus.model.Timetable: - """Convert storage record to Timetable model.""" - return campus.model.Timetable( +def _from_record(record: dict) -> campus.model.TimetableEntry: + """Convert storage record to TimetableEntry model.""" + return campus.model.TimetableEntry( id=schema.CampusID(record["id"]), - filename=record["filename"], + timetable_id=record["timetable_id"], lessongroup_id=record["lessongroup_id"], venuetimeslot_id=record["venuetimeslot_id"], - created_at=schema.DateTime(record["created_at"]) if record.get("created_at") else None, - updated_at=schema.DateTime(record["updated_at"]) if record.get("updated_at") else None, + created_at=schema.DateTime(record["created_at"]) ) class TimetablesResource: @@ -34,7 +33,7 @@ def init_storage() -> None: def __getitem__(self, timetable_id: schema.CampusID) -> "TimetableResource": return TimetableResource(timetable_id) - def list(self, **filters: typing.Any) -> list[campus.model.Timetable]: + def list(self, **filters: typing.Any) -> list[campus.model.TimetableEntry]: """List timetables matching filters.""" try: records = timetable_storage.get_matching(filters) @@ -42,17 +41,15 @@ def list(self, **filters: typing.Any) -> list[campus.model.Timetable]: raise api_errors.InternalError.from_exception(e) from e return [_from_record(record) for record in records] - def new(self, **fields: typing.Any) -> campus.model.Timetable: + def new(self, **fields: typing.Any) -> campus.model.TimetableEntry: """Create new timetable.""" - timetable = campus.model.Timetable( - id = schema.CampusID( - uid.generate_category_uid("timetable", length=8) - ), - filename=fields["filename"], + timetable = campus.model.TimetableEntry( + # id generation is handled by the dataclass default factory lessongroup_id = fields["lessongroup_id"], venuetimeslot_id = fields["venuetimeslot_id"], - created_at=schema.DateTime.utcnow() + created_at=schema.DateTime.utcnow(), + timetable_id=fields["timetable_id"] ) try: @@ -68,7 +65,7 @@ class TimetableResource: def __init__(self, timetable_id: schema.CampusID): self.timetable_id = timetable_id - def get(self) -> campus.model.Timetable: + def get(self) -> campus.model.TimetableEntry: """Get the timetable.""" try: record = timetable_storage.get_by_id(self.timetable_id)