Phase 4: student views, opt-outs, and class schedule
Student page with a standing summary, a class-comparison histogram (zero-dependency HTML/CSS) with a plain-language fewer/same/more sentence, opt-out management with withdrawal of future dates, and a call history that respects the per-course assessment-visibility setting and never shows pending or skipped calls. Opt-outs validate against a new per-course class-day schedule (range generator plus individual add/remove for holidays), since Canvas has no structured meeting-day data; courses without a schedule fall back to a free date picker. Dev mode seeds a Tue/Thu pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -90,6 +90,19 @@ class Enrollment(Base):
|
||||
student: Mapped[Student] = relationship(back_populates="enrollments")
|
||||
|
||||
|
||||
class ClassDay(Base):
|
||||
"""A day the class actually meets. When a course has these, the
|
||||
student opt-out form only offers real class days; without them it
|
||||
falls back to a free date picker."""
|
||||
|
||||
__tablename__ = "class_days"
|
||||
__table_args__ = (UniqueConstraint("course_id", "date"),)
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
course_id: Mapped[int] = mapped_column(ForeignKey("courses.id"))
|
||||
date: Mapped[datetime.date] = mapped_column(Date)
|
||||
|
||||
|
||||
class OptOut(Base):
|
||||
__tablename__ = "optouts"
|
||||
__table_args__ = (UniqueConstraint("course_id", "student_id", "date"),)
|
||||
@@ -132,6 +145,31 @@ def answered_call_counts(session, course_id):
|
||||
return dict(rows)
|
||||
|
||||
|
||||
def class_days(session, course_id, start=None, end=None):
|
||||
q = select(ClassDay).where(ClassDay.course_id == course_id)
|
||||
if start is not None:
|
||||
q = q.where(ClassDay.date >= start)
|
||||
if end is not None:
|
||||
q = q.where(ClassDay.date <= end)
|
||||
return session.execute(q.order_by(ClassDay.date)).scalars().all()
|
||||
|
||||
|
||||
def is_class_day(session, course_id, on_date):
|
||||
"""True if on_date is a listed class day, or if the course has no
|
||||
class days defined at all (no schedule means no restriction)."""
|
||||
if not session.execute(
|
||||
select(ClassDay.id).where(ClassDay.course_id == course_id).limit(1)
|
||||
).first():
|
||||
return True
|
||||
return bool(
|
||||
session.execute(
|
||||
select(ClassDay.id).where(
|
||||
ClassDay.course_id == course_id, ClassDay.date == on_date
|
||||
)
|
||||
).first()
|
||||
)
|
||||
|
||||
|
||||
def students_present(session, course_id, on_date):
|
||||
"""Active student-role enrollees minus those opted out for the date."""
|
||||
opted_out = select(OptOut.student_id).where(
|
||||
|
||||
Reference in New Issue
Block a user