Phase 5: reporting, exports, pronouns, and roster freshness
Instructor participation report: per-student histograms, outcome mix by class day, and a sortable table including the fairness ratio (answered calls over questions present for, with opt-out days out of the denominator), plus CSV exports of students, calls, and opt-outs. Assessment scales are now per-course data: ordered levels with labels and points out of 100 (defaults carry the old R grading values), with calls referencing levels by id so renames follow through to history. Renaming, re-pointing, reordering, and adding levels are always allowed; deleting a level in use by recorded calls is blocked. Pronouns and course term dates come from Canvas custom variable substitutions, at launch and roster-wide via rlid-scoped NRPS; the student page notes that names/pronouns are Canvas-sourced. Rosters can also be refreshed outside launches: a "Sync roster now" button and a sync-rosters CLI command for an hourly cron job, skipping ended courses. Alembic now runs SQLite-compatible batch migrations with a constraint naming convention. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -32,8 +32,16 @@ STATUS_SKIPPED = "skipped"
|
||||
STATUS_PENDING = "pending"
|
||||
CALL_STATUSES = (STATUS_ANSWERED, STATUS_MISSING, STATUS_SKIPPED, STATUS_PENDING)
|
||||
|
||||
# Default assessment vocabulary, carried over from the manual system.
|
||||
ASSESSMENTS = ("GOOD", "SATISFACTORY", "POOR", "NO MEANINGFUL ANSWER")
|
||||
# Default assessment vocabulary and points, carried over from the
|
||||
# manual system: compute_final_case_grades.R scored GOOD=100,
|
||||
# SATISFACTORY and POOR one and two UW-4.0-scale steps down
|
||||
# (100 - n*50/(4-0.7)), and NO MEANINGFUL ANSWER zero.
|
||||
DEFAULT_ASSESSMENT_LEVELS = (
|
||||
("GOOD", 100.0),
|
||||
("SATISFACTORY", 84.85),
|
||||
("POOR", 69.7),
|
||||
("NO MEANINGFUL ANSWER", 0.0),
|
||||
)
|
||||
|
||||
SELECTION_WEIGHTED = "weighted"
|
||||
SELECTION_CYCLE = "cycle"
|
||||
@@ -47,6 +55,18 @@ class Course(Base):
|
||||
lti_deployment_id: Mapped[str | None] = mapped_column(String(255))
|
||||
title: Mapped[str | None] = mapped_column(String(255))
|
||||
|
||||
# Enough platform detail to call NRPS outside a launch, for the
|
||||
# on-demand roster refresh. Captured/updated on each launch.
|
||||
lti_issuer: Mapped[str | None] = mapped_column(String(255))
|
||||
lti_client_id: Mapped[str | None] = mapped_column(String(255))
|
||||
nrps_url: Mapped[str | None] = mapped_column(String(1024))
|
||||
|
||||
# Course term dates from Canvas ($Canvas.course.startAt/endAt
|
||||
# custom parameters), used to bound date pickers. Nullable: not
|
||||
# every course has them set in Canvas.
|
||||
start_date: Mapped[datetime.date | None] = mapped_column(Date)
|
||||
end_date: Mapped[datetime.date | None] = mapped_column(Date)
|
||||
|
||||
# Per-course settings.
|
||||
weight_factor: Mapped[float] = mapped_column(Float, default=2.0)
|
||||
selection_mode: Mapped[str] = mapped_column(
|
||||
@@ -70,6 +90,7 @@ class Student(Base):
|
||||
name: Mapped[str | None] = mapped_column(String(255))
|
||||
sortable_name: Mapped[str | None] = mapped_column(String(255))
|
||||
email: Mapped[str | None] = mapped_column(String(255))
|
||||
pronouns: Mapped[str | None] = mapped_column(String(64))
|
||||
avatar_url: Mapped[str | None] = mapped_column(String(1024))
|
||||
|
||||
enrollments: Mapped[list["Enrollment"]] = relationship(back_populates="student")
|
||||
@@ -90,6 +111,50 @@ class Enrollment(Base):
|
||||
student: Mapped[Student] = relationship(back_populates="enrollments")
|
||||
|
||||
|
||||
class AssessmentLevel(Base):
|
||||
"""One rung of a course's assessment scale: a label and its points
|
||||
out of 100. Calls reference levels by id, so renaming a level
|
||||
renames it everywhere, past calls included."""
|
||||
|
||||
__tablename__ = "assessment_levels"
|
||||
__table_args__ = (UniqueConstraint("course_id", "label"),)
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
course_id: Mapped[int] = mapped_column(ForeignKey("courses.id"))
|
||||
label: Mapped[str] = mapped_column(String(64))
|
||||
points: Mapped[float] = mapped_column(Float)
|
||||
position: Mapped[int] = mapped_column(default=0)
|
||||
|
||||
|
||||
def ensure_default_levels(session, course):
|
||||
existing = session.execute(
|
||||
select(AssessmentLevel.id)
|
||||
.where(AssessmentLevel.course_id == course.id)
|
||||
.limit(1)
|
||||
).first()
|
||||
if existing:
|
||||
return
|
||||
for i, (label, points) in enumerate(DEFAULT_ASSESSMENT_LEVELS):
|
||||
session.add(
|
||||
AssessmentLevel(
|
||||
course_id=course.id, label=label, points=points, position=i
|
||||
)
|
||||
)
|
||||
session.flush()
|
||||
|
||||
|
||||
def assessment_levels(session, course_id):
|
||||
return (
|
||||
session.execute(
|
||||
select(AssessmentLevel)
|
||||
.where(AssessmentLevel.course_id == course_id)
|
||||
.order_by(AssessmentLevel.position, AssessmentLevel.id)
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
@@ -124,13 +189,20 @@ class Call(Base):
|
||||
student_id: Mapped[int] = mapped_column(ForeignKey("students.id"))
|
||||
session_date: Mapped[datetime.date] = mapped_column(Date)
|
||||
status: Mapped[str] = mapped_column(String(16), default=STATUS_PENDING)
|
||||
assessment: Mapped[str | None] = mapped_column(String(32))
|
||||
assessment_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("assessment_levels.id")
|
||||
)
|
||||
note: Mapped[str | None] = mapped_column(Text)
|
||||
created_at: Mapped[datetime.datetime] = mapped_column(
|
||||
DateTime, default=utcnow
|
||||
)
|
||||
|
||||
student: Mapped[Student] = relationship()
|
||||
level: Mapped["AssessmentLevel | None"] = relationship()
|
||||
|
||||
@property
|
||||
def assessment(self):
|
||||
return self.level.label if self.level else None
|
||||
|
||||
|
||||
def answered_call_counts(session, course_id):
|
||||
|
||||
Reference in New Issue
Block a user