From bc9bf97c48646e950dd589480adafb8d37fc28c2 Mon Sep 17 00:00:00 2001 From: Rodolfo Lourenzutti Date: Sun, 26 Dec 2021 13:08:44 -0800 Subject: [PATCH 1/7] Add extra configuration --- scripts/rudaux_config_template.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/rudaux_config_template.py b/scripts/rudaux_config_template.py index 65ee773..6637bf9 100644 --- a/scripts/rudaux_config_template.py +++ b/scripts/rudaux_config_template.py @@ -4,6 +4,7 @@ c.canvas_domain = 'https://canvas.ubc.ca' c.canvas_id = '12345' #course number from the canvas URL c.canvas_token = '23487~sdfasdfga3847fga874fga8347fgaf' #canvas API token (this example was generated by the ISmashedMyKeyboard algorithm) +c.registration_deadline = '2021-01-23' # the last day to add/drop the course c.user_folder_root = '/tank/home/dsci100' #the root folder for users on *both* student and instructor jupyterhub servers c.student_local_assignment_folder = 'dsci-100/materials' # the name of the student repository and the subdirectory in the students repository where assignments are stored (if it is used) c.grading_image = 'yourdockeraccount/your-docker-image:v0.1' From 6342baee517950f11ca4f285fb6daa7eabfe1579 Mon Sep 17 00:00:00 2001 From: Rodolfo Lourenzutti Date: Thu, 30 Dec 2021 15:21:58 -0800 Subject: [PATCH 2/7] Add registration deadline --- rudaux/flows.py | 64 +++++---- scripts/rudaux_config_template.py | 217 ++++++++++++++++++++++++------ 2 files changed, 213 insertions(+), 68 deletions(-) diff --git a/rudaux/flows.py b/rudaux/flows.py index c343dd2..287daf6 100644 --- a/rudaux/flows.py +++ b/rudaux/flows.py @@ -1,17 +1,17 @@ -import sys, os +import sys +import os import prefect -from prefect import Flow, unmapped, task, flatten +from prefect import Flow, unmapped, task from prefect.engine import signals from prefect.schedules import IntervalSchedule -from prefect.executors import LocalExecutor, DaskExecutor, LocalDaskExecutor +from prefect.executors import LocalExecutor from prefect.backend import FlowView, FlowRunView from prefect.tasks.control_flow.filter import FilterTask from traitlets.config import Config from traitlets.config.loader import PyFileConfigLoader import pendulum as plm from requests.exceptions import ConnectionError -import logging -from subprocess import check_output, STDOUT, CalledProcessError +from subprocess import check_output, CalledProcessError import threading @@ -28,16 +28,17 @@ __PROJECT_NAME = "rudaux" + def _build_flows(args): print("Loading the rudaux_config.py file...") if not os.path.exists(os.path.join(args.directory, 'rudaux_config.py')): - sys.exit( - f""" - There is no rudaux_config.py in the directory {args.directory}, - and no course directory was specified on the command line. Please - specify a directory with a valid rudaux_config.py file. - """ - ) + sys.exit( + f""" + There is no rudaux_config.py in the directory {args.directory}, + and no course directory was specified on the command line. Please + specify a directory with a valid rudaux_config.py file. + """ + ) config = Config() config.merge(PyFileConfigLoader('rudaux_config.py', path=args.directory).load_config()) @@ -100,14 +101,13 @@ def register(args): except ConnectionError as e: print(e) sys.exit( - f""" - Could not connect to the prefect server. Is the server running? - Make sure to start the server before trying to register flows. - To start the prefect server, run the command: - - prefect server start + """ + Could not connect to the prefect server. Is the server running? + Make sure to start the server before trying to register flows. + To start the prefect server, run the command: - """ + prefect server start + """ ) flows = _build_flows(args) for flow in flows: @@ -155,16 +155,31 @@ def build_snapshot_flows(config, args): flows.append(flow) return flows + @task(checkpoint=False) def combine_dictionaries(dicts): - return {k : v for d in dicts for k, v in d.items()} + return {k: v for d in dicts for k, v in d.items()} -def build_autoext_flows(config, args): + +def build_autoext_flows(config): + """ + Build the flow for the auto-extension of assignments for students + who register late. + + Params + ------ + config: traitlets.config.loader.Config + a dictionary-like object containing the configurations + from rudaux_config.py + """ flows = [] for group in config.course_groups: for course_id in config.course_groups[group]: - with Flow(config.course_names[course_id]+"-autoext", terminal_state_handler = fail_handler_gen(config)) as flow: + with Flow(config.course_names[course_id] + "-autoext", + terminal_state_handler=fail_handler_gen(config)) as flow: + assignment_names = list(config.assignments[group].keys()) + # Obtain course/student/assignment/etc info from the course API course_info = api.get_course_info(config, course_id) assignments = api.get_assignments(config, course_id, assignment_names) @@ -177,8 +192,9 @@ def build_autoext_flows(config, args): # Fill in submission deadlines submission_sets = subm.build_submission_set.map(unmapped(config), submission_sets) - # Compute override updates - overrides = subm.get_latereg_overrides.map(unmapped(config.latereg_extension_days[group]), submission_sets) + # Compute override updates if registration deadline has not passed. + if plm.now().in_timezone(config.notify_timezone).timestamp() < plm.from_format(config.registration_deadline, 'YYYY-MM-DD').timestamp(): + overrides = subm.get_latereg_overrides.map(unmapped(config.latereg_extension_days[group]), submission_sets) # TODO: we would ideally do flatten(overrides) and then # api.update_override.map(unmapped(config), unmapped(course_id), flatten(overrides)) diff --git a/scripts/rudaux_config_template.py b/scripts/rudaux_config_template.py index 6637bf9..e4bb34d 100644 --- a/scripts/rudaux_config_template.py +++ b/scripts/rudaux_config_template.py @@ -1,46 +1,175 @@ -import rudaux - -c.name = 'dsci100' -c.canvas_domain = 'https://canvas.ubc.ca' -c.canvas_id = '12345' #course number from the canvas URL -c.canvas_token = '23487~sdfasdfga3847fga874fga8347fgaf' #canvas API token (this example was generated by the ISmashedMyKeyboard algorithm) -c.registration_deadline = '2021-01-23' # the last day to add/drop the course -c.user_folder_root = '/tank/home/dsci100' #the root folder for users on *both* student and instructor jupyterhub servers -c.student_local_assignment_folder = 'dsci-100/materials' # the name of the student repository and the subdirectory in the students repository where assignments are stored (if it is used) -c.grading_image = 'yourdockeraccount/your-docker-image:v0.1' -c.jupyterhub_host_root = 'your-student-jupyterhub.domain.com' -c.jupyterhub_config_dir = '/srv/jupyterhub/' #the folder where jupyterhub_config and zfs_homedir.sh is -c.latereg_extension_days = 7 #number of days to give extensions for late registrations (registration date + 7 days here) -c.instructor_user = 'your_username' #your username on the jupyterhub (you have to create this using dictauth) -c.instructor_repo_url = 'git@github.com:your-account/your-repo.git' #the git url for the course material -c.return_solution_threshold = 0.93 #the fraction of students whose assignments must be collected before you return solutions -c.student_folder_root = '/tank-student/home/dsci100' #the NFS mount point on the instructor jupyterhub server for /tank/home/dsci100 from student server -c.num_docker_threads = 4 #the number of CPU threads to use when grading, generating feedback, etc -c.docker_memory = '1g' #the amount of memory for each grading thread -c.earliest_solution_return_date = '2020-10-02 01:00:00' #the earliest date in the course to return any solutions for anything - -c.notify_days = ['Monday', 'Thursday'] #days of the week to send grading reminder emails to graders (emails are sent to instructor for any errors any day) -c.notification_type = rudaux.notification.SendMail #use this for local email server sending (no account required); use rudaux.notification.SMTP for remote smtp server -c.sendmail.address = 'dscibot@domain.com' #the "from" address for your notifications -c.sendmail.contact_info = { #contact info for you and graders -- format is 'your_jupyterhub_username' : {'name' : 'Your Nice Name', 'address' : 'your.email@email.com'} - 'your_username' : {'name' : 'Your Nice Name', 'address' : 'your.email@email.com'}, - 'a_ta_name' : {'name' : 'TA Nice Name', 'address' : 'ta.email@email.com'}, - 'a_ta_name' : {'name' : 'TA Nice Name', 'address' : 'ta.email@email.com'} +# the base domain for canvas at your institution +# e.g. at UBC, this is https://canvas.ubc.ca +c.canvas_domain = 'https://canvas.your-domain.com' + +# tells rudaux which courses are part of which groups +# group_name is a simple name for the group of canvas courses. +# e.g. +# "dsci100" : ["12345", "678910"] +# "stat201" : ["54323"] +c.course_groups = { + 'group_name': ['canvas_id_1', 'canvas_id_2'] +} + +# tells rudaux what to call each course when printing to logs +# canvas_id_1/2/etc are the same as above +# human_readable_name_1/2/etc is just a human-readable name for each section +# (e.g., human_readable_name_1 and 2 would be dsci100-001 and dsci100-004 for +# the two dsci100 sections) +# make sure you include names for every canvas ID above +# e.g. +# {"12345" : "dsci100-001", "678910" : "dsci100-004"} +c.course_names = { + 'canvas_id_1': 'human_readable_name_1', + 'canvas_id_2': 'human_readable_name_2', +} + +# tells rudaux the last day students have to add a course. +# at UBC we can check it here: +# https://students.ubc.ca/enrolment/registration/course-change-dates +c.registration_deadline = '2022-01-21' + +# gives rudaux the ability to read/write to canvas page +# canvas_id_1/2/etc are same as above +# instructor_token_1/2/etc are the Canvas API tokens for the instructor of each section +# make sure to include an instructor token for each canvas ID above +# i.e. if both sections have the same instructor, they'll have the same token +# e.g. {"12345" : "43456~34f8h948fha948haoweifha30948fha34f", "678910": "54698~34hs384he4fhsoeirfho348hfaoweifh"} +c.course_tokens = { + 'canvas_id_1': 'instructor_token_1', + 'canvas_id_2': 'instructor_token_2', } -#don't need this unless using notification_type = rudaux.notification.SMTP -#c.smtp.hostname = 'smtp.otherdomain.com:587' -#c.smtp.address = 'dsci100bot@otherdomain.com' -#c.smtp.username = 'dsci100bot' -#c.smtp.passwd = 'your_password' -#c.smtp.contact_info = { #contact info for you and graders -- format is 'your_jupyterhub_username' : {'name' : 'Your Nice Name', 'address' : 'your.email@email.com'} -# 'your_username' : {'name' : 'Your Nice Name', 'address' : 'your.email@email.com'}, -# 'a_ta_name' : {'name' : 'TA Nice Name', 'address' : 'ta.email@email.com'}, -# 'a_ta_name' : {'name' : 'TA Nice Name', 'address' : 'ta.email@email.com'} -#} - -c.graders = { #the list of graders for each jupyterhub assignment - 'worksheet_01' : ['your_username'], - 'worksheet_02' : ['your_username'], - 'tutorial_01' : ['ta_username', 'other_ta_username'], - 'tutorial_02' : ['ta_username', 'other_ta_username'] + +# tells rudaux which assignments to track and who is grading them +# group_name same as above +# assignment_name_1/2/3 are assignment names from Canvas +# grader_jhub_username_A/B/etc are the grading jupyterhub username of TAs / instructors / whoever is responsible for grading +# if an assignment is purely autograded, just use the instructor's jhub username (no manual grading required) +# if multiple graders are listed with an assignment, rudaux splits the grading up for them equally +c.assignments = { + 'group_name': { + 'assignment_name_1': ['grader_jhub_username_A'], + 'assignment_name_2': ['grader_jhub_username_B'], + 'assignment_name_3': ['grader_jhub_username_A', 'grader_jhub_username_B'] + } } + +# tells rudaux where to look for student work (usually a remote machine) +# canvas_id_1/2/etc are same canvas IDs as above +# hostname is the ssh hostname for the student machine +# port is the ssh port (usually just 22) +# admin_user is the linux user on the student machine to ssh into, should have admin privileges (needs to zfs snapshot) +# /usr/sbin/zfs is the path to the zfs exec, but change if needed +# /path/to/student/folder/directory is the dir with the student folders in it (e.g. /tank/home/dsci100/) +# make sure to include an entry below for each course (canvas_id_1/2/etc) +c.student_ssh = { + 'canvas_id_1' : {'hostname': 'student-jhub.com', + 'port': 22, + 'user' : 'admin_user', + 'zfs_path' : '/usr/sbin/zfs', + 'student_root' : '/path/to/student/folder/directory/'}, + 'canvas_id_2' : {'hostname': 'maybe-another-student-jhub.com', + 'port': 22, + 'user' : 'admin_user', + 'zfs_path' : '/usr/sbin/zfs', + 'student_root' : '/path/to/other/student/folder/directory'}, +} + +# number of late registration automatic extension days (applies for all courses in each group) +c.latereg_extension_days = { + 'group_name' : 7 +} + +# timezone to use for sending grading notifications for outstanding tasks (posting grades, manual grading) +c.notify_timezone = 'America/Vancouver' + +# days to send grading notifications (only a few days per week to avoid spamming the graders) +c.notify_days = ['Friday', 'Monday'] + +# email address to use for sending automated notifications +c.sendmail.address = 'rudaux@your-domain.com' + +# email addresses to send notifications to +c.sendmail.contact_info = { + 'grader_jhub_username_A' : {'name' : 'Bob', 'address' : 'bob@email.com'}, + 'grader_jhub_username_B' : {'name' : 'Alice', 'address' : 'alice@email.com'} +} + +# specify which user is the instructor (who to notify to post grades) +# template below is if the course instructor's username on the grading jhub is grader_jhub_username_A +c.instructor_user = 'grader_jhub_username_A' + +# specify where to find the jupyterhub config directory (rudaux manages grading jupyterhub accounts) +# you probably don't need to change this +c.jupyterhub_config_dir = '/srv/jupyterhub/' + +# specify the linux user and group to grant ownership for grading directories (the jupyterhub user) +# you probably don't need to change this +c.jupyterhub_user = 'jupyter' +c.jupyterhub_group = 'users' + +# specify the quota to give grading accounts +# default to 5 gigabytes, but you can make this larger/smaller as needed +c.user_quota = '5g' + +# specify where to create new grading folders (path to the directory where the jupyterhub user folders are stored) +c.user_root = '/path/to/jhub/user/folder/dir' + +# specify the names of submitted/feedback/autograded folders +# you probably don't need to change these +c.submissions_folder = 'submitted' +c.feedback_folder = 'feedback' +c.autograded_folder = 'autograded' + +# the path to the ZFS exec on the grading machine +# you probably don't need to change this +c.zfs_path = '/usr/sbin/zfs' + +# the ssh-access git repository URL (the root of this directory should be an nbgrader dir) +c.instructor_repo_url = 'git@github.com/your-instructor-repo.git' + +# NFS mount of student jhub user directory to instructor machine +# NOTE: I believe this is no longer used. But leaving it in for now and will make a github issue to remove it. +c.student_dataset_root = '/path-student/to/jhub/user/folder/directory' + +# the path in each student's JHub directory to look for their assignment folders +# e.g. if assignment_name_1's folder is located at dsci-100-student/materials/assignment_name_1/... within each student's jhub user folder (and similar for the other asgns), +# this would be "dsci-100-student/materials" +c.student_local_assignment_folder = 'dsci-100-student/materials' + +# the prefix to add to user folders (nbgrader breaks with student folders that are just numbers, so append student-##### to make it work) +# this can be essentially anything as long as it starts with a letter, so you probably don't need to change this +c.grading_student_folder_prefix = 'student-' + +# the fraction of deadlines that must be passed before solutions are returned (useful for early weeks in a semester with lots of auto late extensions) +c.return_solution_threshold = 0.93 + +# the earliest date to return solutions to anything regardless of the return thresholds +c.earliest_solution_return_date = '2021-09-26' + +# the amount of memory to give each autograding container +c.docker_memory = '2g' + +# the image to use for nbgrader operations +# the example below is for the 0.17.0 version of the DSCI100 image +c.docker_image = 'ubcdsci/r-dsci-grading:v0.17.0' + +# the name of the directory in the container to bind to the grader's jhub folder +c.docker_bind_folder = '/home/jupyter' + +# the minute and interval to run the autoextension/snapshot/grading flows +# e.g. below for snapshot would run at 1:02, 1:17, 1:32, ... +# similar behaviour for the other two +# typically grading flow runs much slower than the other two +c.snapshot_interval = 15 +c.snapshot_minute = 2 +c.autoext_interval = 60 +c.autoext_minute = 0 +c.grade_interval = 60*24 +c.grade_minute = 10 + +# whether to run the flow once or on the usual schedule (True = run once now, False = run on the normal schedule) +c.debug = True + + + From 19cc748eb6e9d606c1d55010585e1fc5c4f6407d Mon Sep 17 00:00:00 2001 From: Rodolfo Lourenzutti Date: Thu, 30 Dec 2021 16:23:15 -0800 Subject: [PATCH 3/7] Control override based on registration deadline --- rudaux/flows.py | 5 ++--- rudaux/submission.py | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/rudaux/flows.py b/rudaux/flows.py index 287daf6..41b3a6b 100644 --- a/rudaux/flows.py +++ b/rudaux/flows.py @@ -192,9 +192,8 @@ def build_autoext_flows(config): # Fill in submission deadlines submission_sets = subm.build_submission_set.map(unmapped(config), submission_sets) - # Compute override updates if registration deadline has not passed. - if plm.now().in_timezone(config.notify_timezone).timestamp() < plm.from_format(config.registration_deadline, 'YYYY-MM-DD').timestamp(): - overrides = subm.get_latereg_overrides.map(unmapped(config.latereg_extension_days[group]), submission_sets) + # Compute override updates + overrides = subm.get_latereg_overrides.map(unmapped(config.latereg_extension_days[group]), submission_sets, config) # TODO: we would ideally do flatten(overrides) and then # api.update_override.map(unmapped(config), unmapped(course_id), flatten(overrides)) diff --git a/rudaux/submission.py b/rudaux/submission.py index 99c30e3..fde993d 100644 --- a/rudaux/submission.py +++ b/rudaux/submission.py @@ -212,7 +212,7 @@ def generate_latereg_overrides_name(extension_days, subm_set, **kwargs): return 'lateregs-'+subm_set['__name__'] @task(checkpoint=False,task_run_name=generate_latereg_overrides_name) -def get_latereg_overrides(extension_days, subm_set): +def get_latereg_overrides(extension_days, subm_set, config): logger = get_logger() fmt = 'ddd YYYY-MM-DD HH:mm:ss' overrides = [] @@ -234,7 +234,7 @@ def get_latereg_overrides(extension_days, subm_set): to_remove = None to_create = None - if regdate > assignment['unlock_at']: + if regdate > assignment['unlock_at'] and assignment['due_at'] <= plm.from_format(config.registration_deadline, f'YYYY-MM-DD', tz=config.notify_timezone).add(days=extension_days): #the late registration due date latereg_date = regdate.add(days=extension_days).in_timezone(tz).end_of('day').set(microsecond=0) if latereg_date > subm['due_at']: From e9b6d4d63b34eeb722308303d75002e5281c1740 Mon Sep 17 00:00:00 2001 From: Rodolfo Lourenzutti Date: Thu, 30 Dec 2021 17:46:23 -0800 Subject: [PATCH 4/7] Unmappe config in get_latereg_overrides --- rudaux/flows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rudaux/flows.py b/rudaux/flows.py index 41b3a6b..88a865d 100644 --- a/rudaux/flows.py +++ b/rudaux/flows.py @@ -193,7 +193,7 @@ def build_autoext_flows(config): submission_sets = subm.build_submission_set.map(unmapped(config), submission_sets) # Compute override updates - overrides = subm.get_latereg_overrides.map(unmapped(config.latereg_extension_days[group]), submission_sets, config) + overrides = subm.get_latereg_overrides.map(unmapped(config.latereg_extension_days[group]), submission_sets, unmapped(config)) # TODO: we would ideally do flatten(overrides) and then # api.update_override.map(unmapped(config), unmapped(course_id), flatten(overrides)) From e53acbda94a38181b1ceea77b5743bf57614f7f8 Mon Sep 17 00:00:00 2001 From: Rodolfo Lourenzutti Date: Thu, 30 Dec 2021 17:52:30 -0800 Subject: [PATCH 5/7] Bring the 'args' parameter back --- rudaux/flows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rudaux/flows.py b/rudaux/flows.py index 88a865d..8d79b05 100644 --- a/rudaux/flows.py +++ b/rudaux/flows.py @@ -161,7 +161,7 @@ def combine_dictionaries(dicts): return {k: v for d in dicts for k, v in d.items()} -def build_autoext_flows(config): +def build_autoext_flows(config, args): """ Build the flow for the auto-extension of assignments for students who register late. From 69792e9e95b911a44a4f8abc088518f7abdc04e2 Mon Sep 17 00:00:00 2001 From: Rodolfo Lourenzutti Date: Thu, 30 Dec 2021 17:54:00 -0800 Subject: [PATCH 6/7] Change the rule for auto-extension from due_at + extension to unlock_at --- rudaux/submission.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rudaux/submission.py b/rudaux/submission.py index fde993d..11cf3b7 100644 --- a/rudaux/submission.py +++ b/rudaux/submission.py @@ -234,7 +234,7 @@ def get_latereg_overrides(extension_days, subm_set, config): to_remove = None to_create = None - if regdate > assignment['unlock_at'] and assignment['due_at'] <= plm.from_format(config.registration_deadline, f'YYYY-MM-DD', tz=config.notify_timezone).add(days=extension_days): + if regdate > assignment['unlock_at'] and assignment['unlock_at'] <= plm.from_format(config.registration_deadline, f'YYYY-MM-DD', tz=config.notify_timezone): #the late registration due date latereg_date = regdate.add(days=extension_days).in_timezone(tz).end_of('day').set(microsecond=0) if latereg_date > subm['due_at']: From 76a10bd24a1f5a913b9ff4b347a072f7c141d60b Mon Sep 17 00:00:00 2001 From: Rodolfo Lourenzutti Date: Thu, 30 Dec 2021 18:23:49 -0800 Subject: [PATCH 7/7] Remove args from build_*_flows --- rudaux/flows.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rudaux/flows.py b/rudaux/flows.py index 8d79b05..e6731e7 100644 --- a/rudaux/flows.py +++ b/rudaux/flows.py @@ -71,12 +71,12 @@ def _build_flows(args): flows = [] for build_func, flow_name, interval, minute in flow_builders: print(f"Building/registering the {flow_name} flow...") - _flows = build_func(config, args) + _flows = build_func(config) for flow in _flows: flow.executor = executor if not config.debug: - flow.schedule = IntervalSchedule(start_date = plm.now('UTC').set(minute=minute), - interval = plm.duration(minutes=interval)) + flow.schedule = IntervalSchedule(start_date=plm.now('UTC').set(minute=minute), + interval=plm.duration(minutes=interval)) flows.append(flow) return flows @@ -135,7 +135,7 @@ def run(args): return -def build_snapshot_flows(config, args): +def build_snapshot_flows(config): flows = [] for group in config.course_groups: for course_id in config.course_groups[group]: @@ -161,7 +161,7 @@ def combine_dictionaries(dicts): return {k: v for d in dicts for k, v in d.items()} -def build_autoext_flows(config, args): +def build_autoext_flows(config): """ Build the flow for the auto-extension of assignments for students who register late. @@ -211,7 +211,7 @@ def build_autoext_flows(config, args): # rather dynamically from LMS; there we dont know what # assignments there are until runtime. So doing it by group is the # right strategy. -def build_grading_flows(config, args): +def build_grading_flows(config): try: check_output(['sudo', '-n', 'true']) except CalledProcessError as e: