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
21 changes: 15 additions & 6 deletions canvigator.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def _run_course_task(task, course, canv_config, n_days, cloud_questions):
cd.prepClassDigest(course, days=n_days, cloud_questions=cloud_questions)


def _run_quiz_task(task, quiz, dry_run, tag, reply_window_days):
def _run_quiz_task(task, quiz, dry_run, tag, reply_window_days, send_all=False):
"""Dispatch a quiz-level task to the appropriate method."""
if task == 'get-quiz-questions':
quiz.getQuizQuestions(tag=tag)
Expand All @@ -98,11 +98,11 @@ def _run_quiz_task(task, quiz, dry_run, tag, reply_window_days):
elif task == 'assess-replies':
quiz.assessFollowUpReplies(reply_window_days=reply_window_days)
elif task == 'send-quiz-reminder':
quiz.sendQuizReminders(dry_run=dry_run)
quiz.sendQuizReminders(dry_run=dry_run, send_all=send_all)
elif task == 'send-follow-up-question':
quiz.sendFollowUpQuestions(dry_run=dry_run)
quiz.sendFollowUpQuestions(dry_run=dry_run, send_all=send_all)
elif task == 'send-follow-up-assessments':
quiz.sendFollowUpAssessments(dry_run=dry_run)
quiz.sendFollowUpAssessments(dry_run=dry_run, send_all=send_all)
elif task == 'create-pairs':
quiz.openPresentCSV()
quiz.generateDistanceMatrix(only_present=True)
Expand Down Expand Up @@ -132,6 +132,7 @@ def _run_quiz_task(task, quiz, dry_run, tag, reply_window_days):
'-g': '--auto-grade',
'-n': '--days',
'-q': '--cloud-questions',
'-s': '--send-all',
'-h': '--help',
}
args = [_SHORT_TO_LONG.get(a, a) for a in sys.argv[1:]]
Expand All @@ -148,6 +149,14 @@ def _run_quiz_task(task, quiz, dry_run, tag, reply_window_days):
if dry_run:
args.remove('--dry-run')

send_all = '--send-all' in args
if send_all:
args.remove('--send-all')

if dry_run and send_all:
print("Error: --send-all/-s and --dry-run/-d are mutually exclusive.")
sys.exit(1)

tag = '--tag' in args
if tag:
args.remove('--tag')
Expand Down Expand Up @@ -346,7 +355,7 @@ def _run_quiz_task(task, quiz, dry_run, tag, reply_window_days):
course.getAllQuizzesAndSubmissions()

elif task == 'send-quiz-reminder' and all_quizzes_flag:
course.sendAllQuizReminders(dry_run=dry_run)
course.sendAllQuizReminders(dry_run=dry_run, send_all=send_all)

elif task in ('create-media-recording-assignment', 'get-media-recordings', 'analyze-media-recordings'):
_run_assignment_task(task, course, canvas, canv_config, dry_run, auto_grade_flag)
Expand Down Expand Up @@ -383,6 +392,6 @@ def _run_quiz_task(task, quiz, dry_run, tag, reply_window_days):
print(f"\nSelected quiz: {quiz_choice.title}")
skip = task in ('get-quiz-questions', 'assess-replies', 'send-follow-up-assessments')
quiz = cq.CanvigatorQuiz(canvas, course, quiz_choice, canv_config, verbose=False, skip_student_data=skip)
_run_quiz_task(task, quiz, dry_run, tag, reply_window_days)
_run_quiz_task(task, quiz, dry_run, tag, reply_window_days, send_all=send_all)

print("\n*** Done ***\n")
15 changes: 13 additions & 2 deletions canvigator_course.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,26 @@ def getAllQuizzesAndSubmissions(self):
else:
print(" Skipping (unpublished or insufficient submissions)")

def sendAllQuizReminders(self, dry_run=False):
def sendAllQuizReminders(self, dry_run=False, send_all=False):
"""Send one consolidated reminder per student covering every published, future-due quiz they're behind on.

With ``send_all=True``, the per-student ``[y/N]`` prompt is bypassed,
but the task refuses unless a ``--dry-run`` of the same task ran in
the last 10 minutes (verified via
``canvigator_quiz._hasRecentDryRunManifest`` against the course-level
``course_reminder_sent_dryrun_*.csv`` manifest).

Iterates every quiz passing ``cu.is_quiz_open_for_reminder`` (published
with a ``due_at`` strictly in the future), classifies each enrolled
student per quiz (no attempt / imperfect / page-blur on perfect), and
composes a single message per student listing all eligible quizzes.
Skips quizzes lacking a ``*_questions_w_tags_*.csv`` with a warning.
"""
if send_all and not cq._hasRecentDryRunManifest(self.config.data_path, "course_reminder_sent"):
raise RuntimeError(
"--send-all requires a fresh --dry-run of 'send-quiz-reminder --all' "
"for this course within the last 10 minutes; none found."
)
all_quizzes = list(self.canvas_course.get_quizzes())
eligible = [q for q in all_quizzes if cu.is_quiz_open_for_reminder(q)]

Expand Down Expand Up @@ -150,7 +161,7 @@ def sendAllQuizReminders(self, dry_run=False):
cq._sendOrPreviewMessages(messages, subject_str, header_label)
_saveCourseReminderManifest(messages, student_state, subject_str, dry_run, self.config.data_path)
else:
sent_messages = cq._interactiveSend(self.canvas, messages, subject_str, header_label)
sent_messages = cq._interactiveSend(self.canvas, messages, subject_str, header_label, send_all=send_all)
if sent_messages:
_saveCourseReminderManifest(sent_messages, student_state, subject_str, dry_run, self.config.data_path)

Expand Down
9 changes: 8 additions & 1 deletion canvigator_digest.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,14 @@ def _summarizeFollowupThemes(quiz_blocks, client, model):
out = {}
for entry in quiz_blocks:
for question_id, rows in entry['rows_by_question'].items():
mode = (rows[0].get('question_mode') or 'explain').lower()
raw_mode = rows[0].get('question_mode')
# NaN is truthy in Python — guard explicitly so a hand-edited CSV
# row with an empty question_mode falls back to 'explain' instead
# of crashing on `.lower()`.
if raw_mode is None or pd.isna(raw_mode) or not str(raw_mode).strip():
mode = 'explain'
else:
mode = str(raw_mode).strip().lower()
prompt = _buildFollowupThemePrompt(
entry['quiz_name'], question_id, rows, mode,
)
Expand Down
17 changes: 14 additions & 3 deletions canvigator_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@
"Route the discussion-question step to cloud Gemini 3 with a redacted "
"prompt (tag names + integer counts only — no transcripts or themes).",
),
'--send-all': (
'-s', None,
"Skip the per-student [y/N] confirmation and send every message. "
"Requires a matching --dry-run run on the same task within the last "
"10 minutes (otherwise refuses). Mutually exclusive with --dry-run.",
),
}


Expand Down Expand Up @@ -480,10 +486,11 @@
"With --all: data/<course>/course_reminder_sent[_dryrun]_YYYYMMDD"
".csv (one row per student listing every quiz reminded about).",
],
'flags': ['--dry-run', '--all'],
'flags': ['--dry-run', '--all', '--send-all'],
'examples': [
"python canvigator.py --crn 12345 send-quiz-reminder",
"python canvigator.py --crn 12345 --all --dry-run send-quiz-reminder",
"python canvigator.py --crn 12345 --send-all send-quiz-reminder",
],
'run_before': ['get-quiz-questions', 'get-quiz-submission-events'],
'run_after': ['generate-follow-up-questions', 'send-follow-up-question'],
Expand Down Expand Up @@ -522,10 +529,11 @@
"(manifest with conversation_id, question_id, sent_at, "
"question_mode columns).",
],
'flags': ['--dry-run'],
'flags': ['--dry-run', '--send-all'],
'examples': [
"python canvigator.py --crn 12345 --dry-run send-follow-up-question",
"python canvigator.py --crn 12345 send-follow-up-question",
"python canvigator.py --crn 12345 --send-all send-follow-up-question",
],
'run_before': ['generate-follow-up-questions'],
'run_after': ['assess-replies', 'send-follow-up-assessments'],
Expand Down Expand Up @@ -599,11 +607,13 @@
"data/<course>/quiz<id>_followup_assessments.csv (rewritten in "
"place with sent_assessment=1 and updated sent_at on success).",
],
'flags': ['--dry-run'],
'flags': ['--dry-run', '--send-all'],
'examples': [
"python canvigator.py --crn 12345 --dry-run "
"send-follow-up-assessments",
"python canvigator.py --crn 12345 send-follow-up-assessments",
"python canvigator.py --crn 12345 --send-all "
"send-follow-up-assessments",
],
'run_before': ['assess-replies'],
'run_after': [],
Expand Down Expand Up @@ -871,6 +881,7 @@ def print_global_help(task_groups):
for flag in (
'--dry-run', '--tag', '--all', '--crn', '--months',
'--reply-window-days', '--auto-grade', '--days', '--cloud-questions',
'--send-all',
):
label = _flag_label(flag)
_, _, text = FLAG_DESCRIPTIONS[flag]
Expand Down
Loading
Loading