Skip to content

Commit

Permalink
Merge pull request #102 from filips123/improvements-and-fixes
Browse files Browse the repository at this point in the history
Implement various improvements and fixes
  • Loading branch information
filips123 authored Sep 2, 2024
2 parents 76d5baf + b3c2eea commit d39de83
Show file tree
Hide file tree
Showing 22 changed files with 471 additions and 318 deletions.
63 changes: 41 additions & 22 deletions API/gimvicurnik/blueprints/menus.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

from .base import BaseHandler
from ..database import LunchMenu, Session, SnackMenu
from ..utils.dates import get_weekdays

if typing.TYPE_CHECKING:
import datetime
from typing import Any
from flask import Blueprint
from ..config import Config

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

@classmethod
def routes(cls, bp: Blueprint, config: Config) -> None:
@bp.route("/menus/date/<date:date>")
def get_menus(date: datetime.date) -> dict[str, dict[str, str] | None]:
# We need type "any" here, otherwise mypy complains
# See: https://github.com/python/mypy/issues/15101
snack: Any = Session.query(SnackMenu).filter(SnackMenu.date == date).first()
lunch: Any = Session.query(LunchMenu).filter(LunchMenu.date == date).first()

if snack:
snack = {
"normal": snack.normal,
"poultry": snack.poultry,
"vegetarian": snack.vegetarian,
"fruitvegetable": snack.fruitvegetable,
}
def _serialize_snack_menu(snack: SnackMenu) -> dict[str, str | None]:
return {
"normal": snack.normal,
"poultry": snack.poultry,
"vegetarian": snack.vegetarian,
"fruitvegetable": snack.fruitvegetable,
}

if lunch:
lunch = {
"normal": lunch.normal,
"vegetarian": lunch.vegetarian,
}
def _serialize_lunch_menu(lunch: LunchMenu) -> dict[str, str | None]:
return {
"until": lunch.until.isoformat("minutes") if lunch.until else None,
"normal": lunch.normal,
"vegetarian": lunch.vegetarian,
}

@bp.route("/menus/date/<date:date>")
def get_date_menus(date: datetime.date) -> dict[str, str | dict[str, str | None] | None]:
snack = Session.query(SnackMenu).filter(SnackMenu.date == date).first()
lunch = Session.query(LunchMenu).filter(LunchMenu.date == date).first()

return {
"snack": snack,
"lunch": lunch,
"date": date.isoformat(),
"snack": _serialize_snack_menu(snack) if snack else None,
"lunch": _serialize_lunch_menu(lunch) if lunch else None,
}

@bp.route("/menus/week/<date:date>")
def get_week_menus(date: datetime.date) -> list[dict[str, str | dict[str, str | None] | None]]:
weekdays = get_weekdays(date)

snacks = Session.query(SnackMenu).filter(SnackMenu.date.in_(weekdays)).all()
lunches = Session.query(LunchMenu).filter(LunchMenu.date.in_(weekdays)).all()

snacks = {snack.date: _serialize_snack_menu(snack) for snack in snacks}
lunches = {lunch.date: _serialize_lunch_menu(lunch) for lunch in lunches}

return [
{
"date": date.isoformat(),
"snack": snacks.get(date),
"lunch": lunches.get(date),
}
for date in weekdays
]
102 changes: 70 additions & 32 deletions API/gimvicurnik/blueprints/schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from .base import BaseHandler
from ..database import Class, LunchSchedule, Session
from ..utils.dates import get_weekdays

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

@classmethod
def routes(cls, bp: Blueprint, config: Config) -> None:
def _serialize_schedule(schedule: LunchSchedule, class_: str) -> dict[str, Any]:
return {
"class": class_,
"date": schedule.date.isoformat(),
"time": schedule.time.isoformat("minutes") if schedule.time else None,
"location": schedule.location,
"notes": schedule.notes,
}

def _fetch_schedules_for_date(
date: datetime.date,
classes: list[str] | None = None,
) -> list[dict[str, Any]]:
"""Fetch lunch schedules for a specific date."""

query = (
Session.query(LunchSchedule, Class.name)
.join(Class)
.filter(LunchSchedule.date == date)
.order_by(LunchSchedule.time, LunchSchedule.class_)
)

if classes:
query = query.filter(Class.name.in_(classes))

return [_serialize_schedule(model[0], model[1]) for model in query]

def _fetch_schedules_for_week(
weekdays: list[datetime.date],
classes: list[str] | None = None,
) -> dict[datetime.date, list[dict[str, Any]]]:
"""Fetch lunch schedules for a specific week."""

query = (
Session.query(LunchSchedule, Class.name)
.join(Class)
.filter(LunchSchedule.date.in_(weekdays))
.order_by(LunchSchedule.time, LunchSchedule.class_)
)

if classes:
query = query.filter(Class.name.in_(classes))

schedules: dict[datetime.date, list[dict[str, Any]]] = {day: [] for day in weekdays}

for schedule, class_ in query.all():
schedules[schedule.date].append(_serialize_schedule(schedule, class_))

return schedules

@bp.route("/schedule/date/<date:date>")
def get_lunch_schedule(date: datetime.date) -> list[dict[str, Any]]:
return [
{
"class": model[1],
"date": model[0].date.strftime("%Y-%m-%d"),
"time": model[0].time.strftime("%H:%M") if model[0].time else None,
"location": model[0].location,
"notes": model[0].notes,
}
for model in (
Session.query(LunchSchedule, Class.name)
.join(Class)
.filter(LunchSchedule.date == date)
.order_by(LunchSchedule.time, LunchSchedule.class_)
)
]
def get_date_schedule(date: datetime.date) -> list[dict[str, Any]]:
return _fetch_schedules_for_date(date)

@bp.route("/schedule/date/<date:date>/classes/<list:classes>")
def get_lunch_schedule_for_classes(date: datetime.date, classes: list[str]) -> list[dict[str, Any]]:
return [
{
"class": model[1],
"date": model[0].date.strftime("%Y-%m-%d"),
"time": model[0].time.strftime("%H:%M") if model[0].time else None,
"location": model[0].location,
"notes": model[0].notes,
}
for model in (
Session.query(LunchSchedule, Class.name)
.join(Class)
.filter(LunchSchedule.date == date, Class.name.in_(classes))
.order_by(LunchSchedule.time, LunchSchedule.class_)
)
]
def get_date_schedule_for_classes(date: datetime.date, classes: list[str]) -> list[dict[str, Any]]:
return _fetch_schedules_for_date(date, classes)

@bp.route("/schedule/week/<date:date>")
def get_week_schedule(date: datetime.date) -> list[list[dict[str, Any]]]:
weekdays = get_weekdays(date)
schedules = _fetch_schedules_for_week(weekdays)
return list(schedules.values())

@bp.route("/schedule/week/<date:date>/classes/<list:classes>")
def get_week_schedule_for_classes(
date: datetime.date,
classes: list[str],
) -> list[list[dict[str, Any]]]:
weekdays = get_weekdays(date)
schedules = _fetch_schedules_for_week(weekdays, classes)
return list(schedules.values())
59 changes: 51 additions & 8 deletions API/gimvicurnik/blueprints/substitutions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import typing

from ..utils.dates import get_weekdays
from .base import BaseHandler
from ..database import Class, Classroom, Entity, Teacher

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

@classmethod
def routes(cls, bp: Blueprint, config: Config) -> None:
def _fetch_week_substitutions(
date: datetime.date,
entity: type[Entity],
names: list[str],
) -> list[list[dict[str, Any]]]:
"""Fetch substitutions for a week containing the given date."""

weekdays = get_weekdays(date)
substitutions = entity.get_substitutions(weekdays, names)

grouped: dict[str, list[dict[str, Any]]] = {day.isoformat(): [] for day in weekdays}

for substitution in substitutions:
grouped[substitution["date"]].append(substitution)

return list(grouped.values())

@bp.route("/substitutions/date/<date:date>")
def get_substitutions(date: datetime.date) -> list[dict[str, Any]]:
return list(Entity.get_substitutions(date))
def get_date_substitutions(date: datetime.date) -> list[dict[str, Any]]:
return list(Entity.get_substitutions([date]))

@bp.route("/substitutions/date/<date:date>/classes/<list:classes>")
def get_substitutions_for_classes(
def get_date_substitutions_for_classes(
date: datetime.date,
classes: list[str],
) -> list[dict[str, Any]]:
return list(Class.get_substitutions(date, classes))
return list(Class.get_substitutions([date], classes))

@bp.route("/substitutions/date/<date:date>/teachers/<list:teachers>")
def get_substitutions_for_teachers(
def get_date_substitutions_for_teachers(
date: datetime.date,
teachers: list[str],
) -> list[dict[str, Any]]:
return list(Teacher.get_substitutions(date, teachers))
return list(Teacher.get_substitutions([date], teachers))

@bp.route("/substitutions/date/<date:date>/classrooms/<list:classrooms>")
def get_substitutions_for_classrooms(
def get_date_substitutions_for_classrooms(
date: datetime.date,
classrooms: list[str],
) -> list[dict[str, Any]]:
return list(Classroom.get_substitutions(date, classrooms))
return list(Classroom.get_substitutions([date], classrooms))

@bp.route("/substitutions/week/<date:date>")
def get_week_substitutions(date: datetime.date) -> list[list[dict[str, Any]]]:
return _fetch_week_substitutions(date, Entity, [])

@bp.route("/substitutions/week/<date:date>/classes/<list:classes>")
def get_week_substitutions_for_classes(
date: datetime.date,
classes: list[str],
) -> list[list[dict[str, Any]]]:
return _fetch_week_substitutions(date, Class, classes)

@bp.route("/substitutions/week/<date:date>/teachers/<list:teachers>")
def get_week_substitutions_for_teachers(
date: datetime.date,
teachers: list[str],
) -> list[list[dict[str, Any]]]:
return _fetch_week_substitutions(date, Teacher, teachers)

@bp.route("/substitutions/week/<date:date>/classrooms/<list:classrooms>")
def get_week_substitutions_for_classrooms(
date: datetime.date,
classrooms: list[str],
) -> list[list[dict[str, Any]]]:
return _fetch_week_substitutions(date, Classroom, classrooms)
10 changes: 6 additions & 4 deletions API/gimvicurnik/database/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ def get_lessons(
@classmethod
def get_substitutions(
cls,
date: date_ | None = None,
dates: list[date_] | None = None,
names: list[str] | None = None,
) -> Iterator[dict[str, Any]]:
original_teacher = aliased(Teacher)
Expand All @@ -143,8 +143,8 @@ def get_substitutions(
)
# fmt: on

if date:
query = query.filter(Substitution.date == date)
if dates:
query = query.filter(Substitution.date.in_(dates))

if names:
if cls.__tablename__ == "classes":
Expand All @@ -156,7 +156,7 @@ def get_substitutions(

for model in query:
yield {
"date": model[0].date.strftime("%Y-%m-%d"),
"date": model[0].date.isoformat(),
"day": model[0].day,
"time": model[0].time,
"subject": model[0].subject,
Expand Down Expand Up @@ -287,5 +287,7 @@ class LunchMenu(Base):
id: Mapped[intpk]
date: Mapped[date_] = mapped_column(unique=True, index=True)

until: Mapped[time_ | None]

normal: Mapped[text | None]
vegetarian: Mapped[text | None]
18 changes: 11 additions & 7 deletions API/gimvicurnik/updaters/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,6 @@ def handle_document(self, document: DocumentInfo, span: Span) -> None:
extractable = self.document_needs_extraction(document)

action = "skipped"
effective = None
content = None
crashed = False
new_hash = None
Expand Down Expand Up @@ -234,12 +233,17 @@ def handle_document(self, document: DocumentInfo, span: Span) -> None:

return

# Get the document's effective date using the subclassed method
# This may return none for documents without an effective date
# If this fails, we can't do anything other than to skip the document
effective = self.get_document_effective(document)

if parsable:
# Get the document's effective date using subclassed method
# If this fails, we can't do anything other than to skip the document
effective = self.get_document_effective(document)
# If there is no date, we can't do anything other than to skip the document
if not effective:
raise ValueError("Missing effective date for a parsable document")

# Parse the document using subclassed method and handle any errors
# Parse the document using the subclassed method and handle any errors
# If this fails, we store the record but mark it for later parsing
try:
self.parse_document(document, stream, effective)
Expand Down Expand Up @@ -272,9 +276,9 @@ def handle_document(self, document: DocumentInfo, span: Span) -> None:
record.url = document.url
record.type = document.type
record.modified = modified
record.effective = effective

if parsable:
record.effective = effective
record.hash = new_hash
record.parsed = True

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

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

# noinspection PyMethodMayBeStatic
Expand Down
7 changes: 2 additions & 5 deletions API/gimvicurnik/updaters/eclassroom.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ def get_document_title(self, document: DocumentInfo) -> str:
return document.title

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

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

# This cannot happen because only substitutions and schedules are provided
raise KeyError("Unknown parsable document type from the e-classroom")

def document_needs_parsing(self, document: DocumentInfo) -> bool:
"""Return whether the document needs parsing."""

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

def document_needs_extraction(self, document: DocumentInfo) -> bool:
Expand Down
Loading

0 comments on commit d39de83

Please sign in to comment.