Skip to content

Commit d39de83

Browse files
authored
Merge pull request #102 from filips123/improvements-and-fixes
Implement various improvements and fixes
2 parents 76d5baf + b3c2eea commit d39de83

File tree

22 files changed

+471
-318
lines changed

22 files changed

+471
-318
lines changed

API/gimvicurnik/blueprints/menus.py

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44

55
from .base import BaseHandler
66
from ..database import LunchMenu, Session, SnackMenu
7+
from ..utils.dates import get_weekdays
78

89
if typing.TYPE_CHECKING:
910
import datetime
10-
from typing import Any
1111
from flask import Blueprint
1212
from ..config import Config
1313

@@ -17,28 +17,47 @@ class MenusHandler(BaseHandler):
1717

1818
@classmethod
1919
def routes(cls, bp: Blueprint, config: Config) -> None:
20-
@bp.route("/menus/date/<date:date>")
21-
def get_menus(date: datetime.date) -> dict[str, dict[str, str] | None]:
22-
# We need type "any" here, otherwise mypy complains
23-
# See: https://github.com/python/mypy/issues/15101
24-
snack: Any = Session.query(SnackMenu).filter(SnackMenu.date == date).first()
25-
lunch: Any = Session.query(LunchMenu).filter(LunchMenu.date == date).first()
26-
27-
if snack:
28-
snack = {
29-
"normal": snack.normal,
30-
"poultry": snack.poultry,
31-
"vegetarian": snack.vegetarian,
32-
"fruitvegetable": snack.fruitvegetable,
33-
}
20+
def _serialize_snack_menu(snack: SnackMenu) -> dict[str, str | None]:
21+
return {
22+
"normal": snack.normal,
23+
"poultry": snack.poultry,
24+
"vegetarian": snack.vegetarian,
25+
"fruitvegetable": snack.fruitvegetable,
26+
}
3427

35-
if lunch:
36-
lunch = {
37-
"normal": lunch.normal,
38-
"vegetarian": lunch.vegetarian,
39-
}
28+
def _serialize_lunch_menu(lunch: LunchMenu) -> dict[str, str | None]:
29+
return {
30+
"until": lunch.until.isoformat("minutes") if lunch.until else None,
31+
"normal": lunch.normal,
32+
"vegetarian": lunch.vegetarian,
33+
}
34+
35+
@bp.route("/menus/date/<date:date>")
36+
def get_date_menus(date: datetime.date) -> dict[str, str | dict[str, str | None] | None]:
37+
snack = Session.query(SnackMenu).filter(SnackMenu.date == date).first()
38+
lunch = Session.query(LunchMenu).filter(LunchMenu.date == date).first()
4039

4140
return {
42-
"snack": snack,
43-
"lunch": lunch,
41+
"date": date.isoformat(),
42+
"snack": _serialize_snack_menu(snack) if snack else None,
43+
"lunch": _serialize_lunch_menu(lunch) if lunch else None,
4444
}
45+
46+
@bp.route("/menus/week/<date:date>")
47+
def get_week_menus(date: datetime.date) -> list[dict[str, str | dict[str, str | None] | None]]:
48+
weekdays = get_weekdays(date)
49+
50+
snacks = Session.query(SnackMenu).filter(SnackMenu.date.in_(weekdays)).all()
51+
lunches = Session.query(LunchMenu).filter(LunchMenu.date.in_(weekdays)).all()
52+
53+
snacks = {snack.date: _serialize_snack_menu(snack) for snack in snacks}
54+
lunches = {lunch.date: _serialize_lunch_menu(lunch) for lunch in lunches}
55+
56+
return [
57+
{
58+
"date": date.isoformat(),
59+
"snack": snacks.get(date),
60+
"lunch": lunches.get(date),
61+
}
62+
for date in weekdays
63+
]

API/gimvicurnik/blueprints/schedule.py

Lines changed: 70 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from .base import BaseHandler
66
from ..database import Class, LunchSchedule, Session
7+
from ..utils.dates import get_weekdays
78

89
if typing.TYPE_CHECKING:
910
import datetime
@@ -17,38 +18,75 @@ class ScheduleHandler(BaseHandler):
1718

1819
@classmethod
1920
def routes(cls, bp: Blueprint, config: Config) -> None:
21+
def _serialize_schedule(schedule: LunchSchedule, class_: str) -> dict[str, Any]:
22+
return {
23+
"class": class_,
24+
"date": schedule.date.isoformat(),
25+
"time": schedule.time.isoformat("minutes") if schedule.time else None,
26+
"location": schedule.location,
27+
"notes": schedule.notes,
28+
}
29+
30+
def _fetch_schedules_for_date(
31+
date: datetime.date,
32+
classes: list[str] | None = None,
33+
) -> list[dict[str, Any]]:
34+
"""Fetch lunch schedules for a specific date."""
35+
36+
query = (
37+
Session.query(LunchSchedule, Class.name)
38+
.join(Class)
39+
.filter(LunchSchedule.date == date)
40+
.order_by(LunchSchedule.time, LunchSchedule.class_)
41+
)
42+
43+
if classes:
44+
query = query.filter(Class.name.in_(classes))
45+
46+
return [_serialize_schedule(model[0], model[1]) for model in query]
47+
48+
def _fetch_schedules_for_week(
49+
weekdays: list[datetime.date],
50+
classes: list[str] | None = None,
51+
) -> dict[datetime.date, list[dict[str, Any]]]:
52+
"""Fetch lunch schedules for a specific week."""
53+
54+
query = (
55+
Session.query(LunchSchedule, Class.name)
56+
.join(Class)
57+
.filter(LunchSchedule.date.in_(weekdays))
58+
.order_by(LunchSchedule.time, LunchSchedule.class_)
59+
)
60+
61+
if classes:
62+
query = query.filter(Class.name.in_(classes))
63+
64+
schedules: dict[datetime.date, list[dict[str, Any]]] = {day: [] for day in weekdays}
65+
66+
for schedule, class_ in query.all():
67+
schedules[schedule.date].append(_serialize_schedule(schedule, class_))
68+
69+
return schedules
70+
2071
@bp.route("/schedule/date/<date:date>")
21-
def get_lunch_schedule(date: datetime.date) -> list[dict[str, Any]]:
22-
return [
23-
{
24-
"class": model[1],
25-
"date": model[0].date.strftime("%Y-%m-%d"),
26-
"time": model[0].time.strftime("%H:%M") if model[0].time else None,
27-
"location": model[0].location,
28-
"notes": model[0].notes,
29-
}
30-
for model in (
31-
Session.query(LunchSchedule, Class.name)
32-
.join(Class)
33-
.filter(LunchSchedule.date == date)
34-
.order_by(LunchSchedule.time, LunchSchedule.class_)
35-
)
36-
]
72+
def get_date_schedule(date: datetime.date) -> list[dict[str, Any]]:
73+
return _fetch_schedules_for_date(date)
3774

3875
@bp.route("/schedule/date/<date:date>/classes/<list:classes>")
39-
def get_lunch_schedule_for_classes(date: datetime.date, classes: list[str]) -> list[dict[str, Any]]:
40-
return [
41-
{
42-
"class": model[1],
43-
"date": model[0].date.strftime("%Y-%m-%d"),
44-
"time": model[0].time.strftime("%H:%M") if model[0].time else None,
45-
"location": model[0].location,
46-
"notes": model[0].notes,
47-
}
48-
for model in (
49-
Session.query(LunchSchedule, Class.name)
50-
.join(Class)
51-
.filter(LunchSchedule.date == date, Class.name.in_(classes))
52-
.order_by(LunchSchedule.time, LunchSchedule.class_)
53-
)
54-
]
76+
def get_date_schedule_for_classes(date: datetime.date, classes: list[str]) -> list[dict[str, Any]]:
77+
return _fetch_schedules_for_date(date, classes)
78+
79+
@bp.route("/schedule/week/<date:date>")
80+
def get_week_schedule(date: datetime.date) -> list[list[dict[str, Any]]]:
81+
weekdays = get_weekdays(date)
82+
schedules = _fetch_schedules_for_week(weekdays)
83+
return list(schedules.values())
84+
85+
@bp.route("/schedule/week/<date:date>/classes/<list:classes>")
86+
def get_week_schedule_for_classes(
87+
date: datetime.date,
88+
classes: list[str],
89+
) -> list[list[dict[str, Any]]]:
90+
weekdays = get_weekdays(date)
91+
schedules = _fetch_schedules_for_week(weekdays, classes)
92+
return list(schedules.values())

API/gimvicurnik/blueprints/substitutions.py

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import typing
44

5+
from ..utils.dates import get_weekdays
56
from .base import BaseHandler
67
from ..database import Class, Classroom, Entity, Teacher
78

@@ -17,27 +18,69 @@ class SubstitutionsHandler(BaseHandler):
1718

1819
@classmethod
1920
def routes(cls, bp: Blueprint, config: Config) -> None:
21+
def _fetch_week_substitutions(
22+
date: datetime.date,
23+
entity: type[Entity],
24+
names: list[str],
25+
) -> list[list[dict[str, Any]]]:
26+
"""Fetch substitutions for a week containing the given date."""
27+
28+
weekdays = get_weekdays(date)
29+
substitutions = entity.get_substitutions(weekdays, names)
30+
31+
grouped: dict[str, list[dict[str, Any]]] = {day.isoformat(): [] for day in weekdays}
32+
33+
for substitution in substitutions:
34+
grouped[substitution["date"]].append(substitution)
35+
36+
return list(grouped.values())
37+
2038
@bp.route("/substitutions/date/<date:date>")
21-
def get_substitutions(date: datetime.date) -> list[dict[str, Any]]:
22-
return list(Entity.get_substitutions(date))
39+
def get_date_substitutions(date: datetime.date) -> list[dict[str, Any]]:
40+
return list(Entity.get_substitutions([date]))
2341

2442
@bp.route("/substitutions/date/<date:date>/classes/<list:classes>")
25-
def get_substitutions_for_classes(
43+
def get_date_substitutions_for_classes(
2644
date: datetime.date,
2745
classes: list[str],
2846
) -> list[dict[str, Any]]:
29-
return list(Class.get_substitutions(date, classes))
47+
return list(Class.get_substitutions([date], classes))
3048

3149
@bp.route("/substitutions/date/<date:date>/teachers/<list:teachers>")
32-
def get_substitutions_for_teachers(
50+
def get_date_substitutions_for_teachers(
3351
date: datetime.date,
3452
teachers: list[str],
3553
) -> list[dict[str, Any]]:
36-
return list(Teacher.get_substitutions(date, teachers))
54+
return list(Teacher.get_substitutions([date], teachers))
3755

3856
@bp.route("/substitutions/date/<date:date>/classrooms/<list:classrooms>")
39-
def get_substitutions_for_classrooms(
57+
def get_date_substitutions_for_classrooms(
4058
date: datetime.date,
4159
classrooms: list[str],
4260
) -> list[dict[str, Any]]:
43-
return list(Classroom.get_substitutions(date, classrooms))
61+
return list(Classroom.get_substitutions([date], classrooms))
62+
63+
@bp.route("/substitutions/week/<date:date>")
64+
def get_week_substitutions(date: datetime.date) -> list[list[dict[str, Any]]]:
65+
return _fetch_week_substitutions(date, Entity, [])
66+
67+
@bp.route("/substitutions/week/<date:date>/classes/<list:classes>")
68+
def get_week_substitutions_for_classes(
69+
date: datetime.date,
70+
classes: list[str],
71+
) -> list[list[dict[str, Any]]]:
72+
return _fetch_week_substitutions(date, Class, classes)
73+
74+
@bp.route("/substitutions/week/<date:date>/teachers/<list:teachers>")
75+
def get_week_substitutions_for_teachers(
76+
date: datetime.date,
77+
teachers: list[str],
78+
) -> list[list[dict[str, Any]]]:
79+
return _fetch_week_substitutions(date, Teacher, teachers)
80+
81+
@bp.route("/substitutions/week/<date:date>/classrooms/<list:classrooms>")
82+
def get_week_substitutions_for_classrooms(
83+
date: datetime.date,
84+
classrooms: list[str],
85+
) -> list[list[dict[str, Any]]]:
86+
return _fetch_week_substitutions(date, Classroom, classrooms)

API/gimvicurnik/database/__init__.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ def get_lessons(
122122
@classmethod
123123
def get_substitutions(
124124
cls,
125-
date: date_ | None = None,
125+
dates: list[date_] | None = None,
126126
names: list[str] | None = None,
127127
) -> Iterator[dict[str, Any]]:
128128
original_teacher = aliased(Teacher)
@@ -143,8 +143,8 @@ def get_substitutions(
143143
)
144144
# fmt: on
145145

146-
if date:
147-
query = query.filter(Substitution.date == date)
146+
if dates:
147+
query = query.filter(Substitution.date.in_(dates))
148148

149149
if names:
150150
if cls.__tablename__ == "classes":
@@ -156,7 +156,7 @@ def get_substitutions(
156156

157157
for model in query:
158158
yield {
159-
"date": model[0].date.strftime("%Y-%m-%d"),
159+
"date": model[0].date.isoformat(),
160160
"day": model[0].day,
161161
"time": model[0].time,
162162
"subject": model[0].subject,
@@ -287,5 +287,7 @@ class LunchMenu(Base):
287287
id: Mapped[intpk]
288288
date: Mapped[date_] = mapped_column(unique=True, index=True)
289289

290+
until: Mapped[time_ | None]
291+
290292
normal: Mapped[text | None]
291293
vegetarian: Mapped[text | None]

API/gimvicurnik/updaters/base.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,6 @@ def handle_document(self, document: DocumentInfo, span: Span) -> None:
184184
extractable = self.document_needs_extraction(document)
185185

186186
action = "skipped"
187-
effective = None
188187
content = None
189188
crashed = False
190189
new_hash = None
@@ -234,12 +233,17 @@ def handle_document(self, document: DocumentInfo, span: Span) -> None:
234233

235234
return
236235

236+
# Get the document's effective date using the subclassed method
237+
# This may return none for documents without an effective date
238+
# If this fails, we can't do anything other than to skip the document
239+
effective = self.get_document_effective(document)
240+
237241
if parsable:
238-
# Get the document's effective date using subclassed method
239-
# If this fails, we can't do anything other than to skip the document
240-
effective = self.get_document_effective(document)
242+
# If there is no date, we can't do anything other than to skip the document
243+
if not effective:
244+
raise ValueError("Missing effective date for a parsable document")
241245

242-
# Parse the document using subclassed method and handle any errors
246+
# Parse the document using the subclassed method and handle any errors
243247
# If this fails, we store the record but mark it for later parsing
244248
try:
245249
self.parse_document(document, stream, effective)
@@ -272,9 +276,9 @@ def handle_document(self, document: DocumentInfo, span: Span) -> None:
272276
record.url = document.url
273277
record.type = document.type
274278
record.modified = modified
279+
record.effective = effective
275280

276281
if parsable:
277-
record.effective = effective
278282
record.hash = new_hash
279283
record.parsed = True
280284

@@ -391,7 +395,7 @@ def get_document_title(self, document: DocumentInfo) -> str:
391395
"""Return the normalized document title. Must be set by subclasses."""
392396

393397
@abstractmethod
394-
def get_document_effective(self, document: DocumentInfo) -> datetime.date:
398+
def get_document_effective(self, document: DocumentInfo) -> datetime.date | None:
395399
"""Return the document effective date in a local timezone. Must be set by subclasses."""
396400

397401
# noinspection PyMethodMayBeStatic

API/gimvicurnik/updaters/eclassroom.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ def get_document_title(self, document: DocumentInfo) -> str:
251251
return document.title
252252

253253
@typing.no_type_check # Ignored because if regex fails, we cannot do anything
254-
def get_document_effective(self, document: DocumentInfo) -> date:
254+
def get_document_effective(self, document: DocumentInfo) -> date | None:
255255
"""Return the document effective date in a local timezone."""
256256

257257
if document.type == DocumentType.SUBSTITUTIONS:
@@ -263,9 +263,6 @@ def get_document_effective(self, document: DocumentInfo) -> date:
263263
search = re.search(r"(\d+) *\. *(\d+) *\. *(\d+)", title)
264264
return date(year=int(search.group(3)), month=int(search.group(2)), day=int(search.group(1)))
265265

266-
# This cannot happen because only substitutions and schedules are provided
267-
raise KeyError("Unknown parsable document type from the e-classroom")
268-
269266
def document_needs_parsing(self, document: DocumentInfo) -> bool:
270267
"""Return whether the document needs parsing."""
271268

@@ -305,7 +302,7 @@ def parse_document( # type: ignore[override]
305302
"Unknown lunch schedule document format: " + str(document.extension)
306303
)
307304
case _:
308-
# This cannot happen because only menus are provided by the API
305+
# This cannot happen because only these types are provided by the API
309306
raise KeyError("Unknown parsable document type from the e-classroom")
310307

311308
def document_needs_extraction(self, document: DocumentInfo) -> bool:

0 commit comments

Comments
 (0)