Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Section override fix #178

Open
wants to merge 31 commits into
base: prefect-by-assignment
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d84e14b
Add field 'course_section_id' to people.
briank-git Sep 10, 2023
495bc92
Have 'course_section_id' printed with student info.
briank-git Sep 10, 2023
f258db2
Add handling for course section overrides.
briank-git Sep 10, 2023
215539e
Add handling for section overrides in _get_due_date.
briank-git Sep 11, 2023
96f01ba
Remove submission if student is SD.
briank-git Sep 11, 2023
bbbda7b
Allow assignments with overrides for all sections past date checks.
briank-git Sep 11, 2023
3150828
Handle when basic_date is None
briank-git Sep 11, 2023
2816b18
Change method of removing SD student submission.
briank-git Sep 11, 2023
80c118a
Convert course_section_id to string in people.
briank-git Sep 11, 2023
a6edda0
Fix removal of SD student submissions.
briank-git Sep 11, 2023
57a6590
Changes to get_latereg_overrides so it takes into account per section…
briank-git Sep 11, 2023
1697e3e
Update get_all_snapshots for section overrides.
briank-git Sep 12, 2023
465bc4d
Throw exception if assignment has no due date and no overrides.
briank-git Sep 12, 2023
fc9cc79
Add handling in build_grading_team for multiple section due dates.
briank-git Sep 12, 2023
1f7ff05
Fix for different source path.
briank-git Sep 12, 2023
cb0bc3e
Update build_submission_set to get correct zfs snap path.
briank-git Sep 12, 2023
820b8c0
Remove length check from loop.
briank-git Sep 12, 2023
b3a8150
else if to elif
briank-git Sep 12, 2023
db4e755
Changes to grader for different source path in instructor repo.
briank-git Sep 12, 2023
c33abe5
Move submitted, feedback, autograded folders to R/python directories.
briank-git Sep 12, 2023
07a9dc1
Changes to point nbgrader to the correct course directory.
briank-git Sep 12, 2023
92374ec
Add check so config file is not created unnecessarily.
briank-git Sep 12, 2023
f9a0634
Bugfix.
briank-git Sep 12, 2023
5f98e29
Throw exception if override(s) have due or unlock dates before course…
briank-git Sep 12, 2023
446ba89
Prepend config to nbgrader_path.
briank-git Sep 12, 2023
f7ea51e
Generate correct release_nb_path.
briank-git Sep 13, 2023
3c5f4e9
Pass config to _compute_max_score.
briank-git Sep 13, 2023
c0e43c9
Throw warning instead of FAIL if Canvas returns 404 for submission.
briank-git Sep 14, 2023
2a75163
Revert "Throw warning instead of FAIL if Canvas returns 404 for submi…
briank-git Sep 14, 2023
e852eaf
Revert to manual override file put in root of grader folder
briank-git Oct 6, 2023
cecf555
Update rudaux config template with new value
briank-git Oct 6, 2023
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
2 changes: 1 addition & 1 deletion rudaux/course.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ def create_grader_folders(self, a):
print('Assignment already generated')

# if solution not generated yet, generate it
local_path = os.path.join('source', a.name, a.name + '.ipynb')
local_path = os.path.join(config.source_path, a.name, a.name + '.ipynb')
soln_name = a.name + '_solution.html'
print('Checking if solution generated...')
if not os.path.exists(os.path.join(repo_path, soln_name)):
Expand Down
10 changes: 9 additions & 1 deletion rudaux/course_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ def _canvas_get_people_by_type(config, course_id, typ):
return [ { 'id' : str(p['user']['id']),
'name' : p['user']['name'],
'sortable_name' : p['user']['sortable_name'],
'course_section_id' : str(p['course_section_id']),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this always exist? I imagine for instructors it might not? You should probably check existence and fill with None otherwise

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also try probing the rest API directly to find out. Easy way to do that is to put the URL that I send via requests to get lists of people directly into the URL bar in your browser instead. You'll see raw JSON response in the browser, just look through that to check.

Copy link
Author

@briank-git briank-git Sep 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked the API docs and it appears to be a required field https://canvas.instructure.com/doc/api/enrollments.html. Also checked list of enrollments, looked like it was there for everyone.

'school_id' : str(p['user']['sis_user_id']),
'reg_date' : plm.parse(p['created_at']) if (plm.parse(p['created_at']) is not None) else plm.parse(p['updated_at']),
'status' : p['enrollment_state']
Expand All @@ -102,7 +103,13 @@ def _canvas_get_overrides(config, course_id, assignment):
overs = _canvas_get(config, course_id, 'assignments/'+assignment['id']+'/overrides')
for over in overs:
over['id'] = str(over['id'])
over['student_ids'] = list(map(str, over['student_ids']))

# If 'student_ids' does not exist, then it's a course section override.
if 'student_ids' in over:
over['student_ids'] = list(map(str, over['student_ids']))
else:
over['course_section_id'] = str(over['course_section_id'])

for key in ['due_at', 'lock_at', 'unlock_at']:
if over.get(key) is not None:
over[key] = plm.parse(over[key])
Expand Down Expand Up @@ -222,6 +229,7 @@ def get_assignments(config, course_id, assignment_names):
'lock_at' : None if a['lock_at'] is None else plm.parse(a['lock_at']),
'unlock_at' : None if a['unlock_at'] is None else plm.parse(a['unlock_at']),
'has_overrides' : a['has_overrides'],
'only_visible_to_overrides' : a['only_visible_to_overrides'],
'overrides' : [],
'published' : a['published']
} for a in asgns if a['name'] in assignment_names]
Expand Down
2 changes: 1 addition & 1 deletion rudaux/flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,7 @@ def list_course_info(args):
print()
print('Students')
print()
print('\n'.join([f"{c[0] : <16}{c[1]['name'] : <32}{c[1]['id'] : <16}{str(c[1]['reg_date'].in_timezone(config.notify_timezone)) : <32}{c[1]['status'] : <16}" for c in studs]))
print('\n'.join([f"{c[0] : <16}{c[1]['name'] : <32}{c[1]['id'] : <16}{str(c[1]['reg_date'].in_timezone(config.notify_timezone)) : <32}{c[1]['status'] : <16}{c[1]['course_section_id'] : <16}" for c in studs]))

print()
print('Teaching Assistants')
Expand Down
63 changes: 50 additions & 13 deletions rudaux/grader.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,34 @@ def build_grading_team(config, course_group, subm_set):
if course_name == '__name__':
continue
assignment = subm_set[course_name]['assignment']

# skip the assignment if it isn't due yet
if assignment['due_at'] > plm.now():
raise signals.SKIP(f"Assignment {assignment['name']} ({assignment['id']}) due date {assignment['due_at']} is in the future. Skipping.")

# Dict for storing course_section_id and unlock_at date key/value pairs.
date_dict = {}
date_dict['everyone'] = {'unlock_at' : assignment['unlock_at'], 'due_at' : assignment['due_at'], 'lock_at' : assignment['lock_at']}

# Process per course section unlock, due, lock dates and add to date_dict
section_overrides=[]
for over in assignment['overrides']:
if 'course_section_id' in over:
section_overrides.append(over)

#if there was at least one, get the override dates
for over in section_overrides:
over_dict = {}
over_dict['unlock_at'] = over['unlock_at']
over_dict['due_at'] = over['due_at']
over_dict['lock_at'] = over['lock_at']
date_dict[over['course_section_id']] = over_dict

# Get the latest due date
latest_due = None
for section in date_dict.values():
if latest_due == None or section['due_at'] > latest_due:
latest_due = section['due_at']

# skip the assignment if the latest due date hasn't passed yet
if latest_due > plm.now():
raise signals.SKIP(f"Assignment {assignment['name']} ({assignment['id']}) due date {latest_due} is in the future. Skipping.")

# check whether all grades have been posted (assignment is done). If so, skip
all_posted = True
Expand Down Expand Up @@ -89,14 +113,14 @@ def build_grading_team(config, course_group, subm_set):
grader['unix_quota'] = config.user_quota
grader['folder'] = os.path.join(config.user_root, grader['name']).rstrip('/')
grader['local_source_path'] = os.path.join('source', asgn_name, asgn_name+'.ipynb')
grader['submissions_folder'] = os.path.join(grader['folder'], config.submissions_folder)
grader['autograded_folder'] = os.path.join(grader['folder'], config.autograded_folder)
grader['feedback_folder'] = os.path.join(grader['folder'], config.feedback_folder)
grader['submissions_folder'] = os.path.join(grader['folder'], config.nbgrader_path, config.submissions_folder)
grader['autograded_folder'] = os.path.join(grader['folder'], config.nbgrader_path, config.autograded_folder)
grader['feedback_folder'] = os.path.join(grader['folder'], config.nbgrader_path, config.feedback_folder)
grader['workload'] = 0
if os.path.exists(grader['submissions_folder']):
grader['workload'] = len([f for f in os.listdir(grader['submissions_folder']) if os.path.isdir(f)])
grader['soln_name'] = asgn_name + '_solution.html'
grader['soln_path'] = os.path.join(grader['folder'], grader['soln_name'])
grader['soln_path'] = os.path.join(grader['folder'], config.nbgrader_path, grader['soln_name'])
graders.append(grader)

return graders
Expand Down Expand Up @@ -134,6 +158,12 @@ def initialize_volumes(config, graders):
logger.info(f"{grader['folder']} is not a valid course repo. Cloning course repository from {config.instructor_repo_url}")
git.Repo.clone_from(config.instructor_repo_url, grader['folder'])
logger.info("Cloned!")

# Create nbgrader config file to point it to the right course directory
if config.nbgrader_path != "":
f = open(os.path.join(grader['folder'], "nbgrader_config.py"), "w")
f.writelines(["c = get_config()\n", f"c.CourseDirectory.root = \"{config.nbgrader_path}\""])
f.close()

# create the submissions folder
if not os.path.exists(grader['submissions_folder']):
Expand All @@ -146,24 +176,31 @@ def initialize_volumes(config, graders):

# if the assignment hasn't been generated yet, generate it
# TODO error handling if the container fails
generated_asgns = run_container(config, 'nbgrader db assignment list', grader['folder'])

# Construct path to nbgrader dir
if config.nbgrader_path != "":
nbgrader_root = os.path.join(grader['folder'], config.nbgrader_path)
else:
nbgrader_root = grader['folder']

generated_asgns = run_container(config, 'nbgrader db assignment list', nbgrader_root)
if aname not in generated_asgns['log']:
logger.info(f"Assignment {aname} not yet generated for grader {grader['name']}")
output = run_container(config, 'nbgrader generate_assignment --force '+aname, grader['folder'])
output = run_container(config, 'nbgrader generate_assignment --force '+aname, nbgrader_root)
logger.info(output['log'])
if 'ERROR' in output['log']:
msg = f"Error generating assignment {aname} for grader {grader['name']} at path {grader['folder']}"
msg = f"Error generating assignment {aname} for grader {grader['name']} at path {nbgrader_root}"
sig = signals.FAIL(msg)
sig.msg = msg
raise sig

# if the solution hasn't been generated yet, generate it
if not os.path.exists(grader['soln_path']):
logger.info(f"Solution for {aname} not yet generated for grader {grader['name']}")
output = run_container(config, 'jupyter nbconvert ' + grader['local_source_path'] + ' --output=' + grader['soln_name'] + ' --output-dir=.' + ' --to html', grader['folder'])
output = run_container(config, 'jupyter nbconvert ' + grader['local_source_path'] + ' --output=' + grader['soln_name'] + ' --output-dir=.' + ' --to html', nbgrader_root)
logger.info(output['log'])
if 'ERROR' in output['log']:
msg = f"Error generating solution for {aname} for grader {grader['name']} at path {grader['folder']}"
msg = f"Error generating solution for {aname} for grader {grader['name']} at path {nbgrader_root}"
sig = signals.FAIL(msg)
sig.msg = msg
raise sig
Expand Down
26 changes: 19 additions & 7 deletions rudaux/snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,14 +138,26 @@ def _get_snap_name(course_name, assignment, override):
def get_all_snapshots(config, course_id, assignments):
snaps = []
for asgn in assignments:
snaps.append( {'due_at' : asgn['due_at'],
'name' : _get_snap_name(config.course_names[course_id], asgn, None),
'student_id' : None})
if asgn['due_at'] is not None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this change might be a bit tricky. Consider an assignment with no overrides but the instructor forgot to set the deadline -- we don't want to silently ignore that problem.

But maybe that would have thrown an exception elsewhere anyway?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth raising here if there is an assignment that doesn't have any overrides and doesn't have due_at/etc. Then from here on out we can assume if due_at is missing it's because their are overrides

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made it so it throws an error here if due_at is missing and there are no overrides in commit 465bc4d.

snaps.append( {'due_at' : asgn['due_at'],
'name' : _get_snap_name(config.course_names[course_id], asgn, None),
'student_id' : None})
for override in asgn['overrides']:
for student_id in override['student_ids']:
snaps.append({'due_at': override['due_at'],
'name' : _get_snap_name(config.course_names[course_id], asgn, override),
'student_id' : student_id})
if 'course_section_id' in override:
snaps.append( {'due_at' : override['due_at'],
'name' : _get_snap_name(config.course_names[course_id], asgn, override),
'student_id' : None})
else:
for student_id in override['student_ids']:
snaps.append({'due_at': override['due_at'],
'name' : _get_snap_name(config.course_names[course_id], asgn, override),
'student_id' : student_id})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should all work OK.

if asgn['due_at'] is None and len(asgn['overrides']) == 0:
msg = f"Assignment {asgn['name']} has no due date and no overrides."
sig = signals.FAIL(msg)
sig.msg = msg
raise sig

return snaps

@task(checkpoint=False)
Expand Down
Loading