Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 12 additions & 15 deletions campus/api/resources/timetable.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -34,25 +33,23 @@ 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)
except campus.storage.errors.StorageError as e:
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:
Expand All @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions campus/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@
"User",
"UserCredentials",
"Vault",
"WeekDay",
"TimeSlot",
"VenueTimeSlot",
"LessonGroup",
"TimetableEntry",
"Timetable",
]

from .assignment import Assignment, ClassroomLink, Question
Expand All @@ -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
104 changes: 63 additions & 41 deletions campus/model/timetable.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""

Expand All @@ -26,7 +34,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
Expand All @@ -38,7 +46,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.
Expand All @@ -47,13 +55,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.
Expand All @@ -64,17 +72,15 @@ 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
index: schema.Integer = unique_field


@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.
Expand All @@ -83,14 +89,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
Expand All @@ -100,43 +104,42 @@ class VenueTimeSlot(Model):
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 Venue.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.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
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):
"""
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:
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", length=8)
lambda: uid.generate_category_uid("timetable-group", length=8)
))
filename: schema.String
timetable_id: schema.CampusID
label: schema.String
__constraints__ = constraints.Unique("timetable_id", "label")


@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:
Expand All @@ -146,35 +149,54 @@ class LessonGroupMember(Model):
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.
"""
id: schema.CampusID = field(default_factory=(
lambda: uid.generate_category_uid("timetable", length=8)
))
filename: schema.String
id: schema.Integer
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)
class Timetable(Model):
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
venuetimeslot_id (Integer): FK referencing a VenueTimeSlot.id
"""
id: schema.CampusID = field(default_factory=(
lambda: uid.generate_category_uid("timetable-entry", length=16)
))
timetable_id: schema.CampusID
lessongroup_id: schema.CampusID
venuetimeslot_id: schema.Integer
__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.
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_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
lessongroup_id: schema.CampusID
venuetimeslot_id: schema.CampusID
__constraints__ = constraints.Unique("lessongroup_id", "venuetimeslot_id", "filename")
start_date: schema.DateTime
end_date: schema.DateTime
__constraints__ = constraints.Unique("filename")