From b7d01a50b259b71ab7b6e6e24d053f4e1377207a Mon Sep 17 00:00:00 2001 From: Eric Pai Date: Tue, 2 May 2017 16:21:13 -0700 Subject: [PATCH 01/10] add slipcalc form --- server/constants.py | 2 ++ server/forms.py | 13 ++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/server/constants.py b/server/constants.py index 3e5443757..3604eb439 100644 --- a/server/constants.py +++ b/server/constants.py @@ -20,6 +20,8 @@ 'regrade', 'revision', 'checkpoint 1', 'checkpoint 2', 'private', 'autograder', 'error'] +TIMESCALES = ['days', 'hours', 'minutes'] + API_PREFIX = '/api' OAUTH_SCOPES = ['all', 'email'] OAUTH_OUT_OF_BAND_URI = 'urn:ietf:wg:oauth:2.0:oob' diff --git a/server/forms.py b/server/forms.py index f5a872174..db29af4bd 100644 --- a/server/forms.py +++ b/server/forms.py @@ -15,7 +15,7 @@ from server import utils import server.canvas.api as canvas_api from server.models import Assignment, User, Client, Course, Message, CanvasCourse -from server.constants import (SCORE_KINDS, COURSE_ENDPOINT_FORMAT, +from server.constants import (SCORE_KINDS, TIMESCALES, COURSE_ENDPOINT_FORMAT, TIMEZONE, STUDENT_ROLE, ASSIGNMENT_ENDPOINT_FORMAT, COMMON_LANGUAGES, ROLE_DISPLAY_NAMES, OAUTH_OUT_OF_BAND_URI) @@ -558,6 +558,17 @@ class ExportAssignment(BaseForm): anonymize = BooleanField('Anonymize', default=False, description="Enable to remove identifying information from submissions") + +class SlipCalculatorForm(BaseForm): + timescale = SelectField('Time Scale', default="Days", + choices=[(c, c.title()) for c in TIMESCALES], + validators=[validators.required()], + description=["Select time scale for slip calculation (slip days, hours, minutes)"]) + grace_amount = DecimalField('Grace Amount', default=0) + grace_scale = SelectField('Grace Scale', default="Minutes", + choices=[(c, c.title()) for c in TIMESCALES], + description=["Select time scale for grace amount (grace days, hours, minutes"]) + ########## # Canvas # ########## From 9f0fe7582e14b60ce3832e19cdc23a477a90afa0 Mon Sep 17 00:00:00 2001 From: Eric Pai Date: Tue, 2 May 2017 16:40:21 -0700 Subject: [PATCH 02/10] add link to calculate slips --- server/controllers/admin.py | 39 +++++++++++++++++ server/forms.py | 6 +-- .../staff/course/assignment/assignment.html | 3 ++ .../templates/staff/jobs/slip_calculator.html | 42 +++++++++++++++++++ 4 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 server/templates/staff/jobs/slip_calculator.html diff --git a/server/controllers/admin.py b/server/controllers/admin.py index 8b091a970..6f9fdde61 100644 --- a/server/controllers/admin.py +++ b/server/controllers/admin.py @@ -974,6 +974,45 @@ def checkpoint_grading(cid, aid): form=form, ) +@admin.route("/course//assignments//slip", + methods=["GET", "POST"]) +@is_staff(course_arg='cid') +def slip_calculator(cid, aid): + courses, current_course = get_courses(cid) + assign = Assignment.query.filter_by(id=aid, course_id=cid).one_or_none() + # if not assign or not Assignment.can(assign, current_user, 'grade'): + # flash('Cannot access assignment', 'error') + # return abort(404) + + form = forms.SlipCalculatorForm() + # if form.validate_on_submit(): + # job = jobs.enqueue_job( + # checkpoint.assign_scores, + # description='Checkpoint Scoring for {}'.format(assign.display_name), + # timeout=600, + # course_id=cid, + # user_id=current_user.id, + # assign_id=assign.id, + # score=form.score.data, + # kind=form.kind.data, + # message=form.message.data, + # deadline=form.deadline.data, + # include_backups=form.include_backups.data) + # return redirect(url_for('.course_job', cid=cid, job_id=job.id)) + # else: + # if not form.kind.data: + # form.kind.default = 'checkpoint 1' + # if not form.deadline.data: + # form.deadline.default = utils.local_time_obj(assign.due_date, assign.course) + # form.process() + + return render_template( + 'staff/jobs/slip_calculator.html', + courses=courses, + current_course=current_course, + assignment=assign, + form=form, + ) ############## # Enrollment # diff --git a/server/forms.py b/server/forms.py index db29af4bd..603c2c9f4 100644 --- a/server/forms.py +++ b/server/forms.py @@ -563,11 +563,7 @@ class SlipCalculatorForm(BaseForm): timescale = SelectField('Time Scale', default="Days", choices=[(c, c.title()) for c in TIMESCALES], validators=[validators.required()], - description=["Select time scale for slip calculation (slip days, hours, minutes)"]) - grace_amount = DecimalField('Grace Amount', default=0) - grace_scale = SelectField('Grace Scale', default="Minutes", - choices=[(c, c.title()) for c in TIMESCALES], - description=["Select time scale for grace amount (grace days, hours, minutes"]) + description="Time scale for slip calculation.") ########## # Canvas # diff --git a/server/templates/staff/course/assignment/assignment.html b/server/templates/staff/course/assignment/assignment.html index 2904f57f8..a56852898 100644 --- a/server/templates/staff/course/assignment/assignment.html +++ b/server/templates/staff/course/assignment/assignment.html @@ -92,6 +92,9 @@

Actions

  • Grant Extension
  • +
  • + Calculate Slips +
  • Configure Autograder
  • diff --git a/server/templates/staff/jobs/slip_calculator.html b/server/templates/staff/jobs/slip_calculator.html new file mode 100644 index 000000000..912f8db5b --- /dev/null +++ b/server/templates/staff/jobs/slip_calculator.html @@ -0,0 +1,42 @@ +{% extends "staff/base.html" %} +{% import "staff/_formhelpers.html" as forms %} + +{% block title %} Calculate Slips for {{ assignment.display_name }} {% endblock %} + +{% block main %} +
    +

    + Calculate Slips for {{ assignment.display_name }} + {{ current_course.offering }} +

    + +
    +
    + {% include 'alerts.html' %} +
    +
    +
    +
    + {% call forms.render_form(form, action_text='Calculate Slips') %} + {{ forms.render_field(form.timescale) }} + {% endcall %} +
    +
    +
    +
    +
    +{% endblock %} From d6cbe8caece0066ebe1bd742266f36193439031c Mon Sep 17 00:00:00 2001 From: Eric Pai Date: Tue, 2 May 2017 17:07:09 -0700 Subject: [PATCH 03/10] setup basic job scaffold --- server/controllers/admin.py | 48 +++++++++---------- server/jobs/slips.py | 8 ++++ .../staff/course/assignment/assignment.html | 2 +- ...p_calculator.html => calculate_slips.html} | 0 4 files changed, 31 insertions(+), 27 deletions(-) create mode 100644 server/jobs/slips.py rename server/templates/staff/jobs/{slip_calculator.html => calculate_slips.html} (100%) diff --git a/server/controllers/admin.py b/server/controllers/admin.py index 6f9fdde61..c42902da9 100644 --- a/server/controllers/admin.py +++ b/server/controllers/admin.py @@ -30,7 +30,7 @@ import server.forms as forms import server.jobs as jobs from server.jobs import (example, export, moss, scores_audit, github_search, - scores_notify, checkpoint) + scores_notify, checkpoint, slips) import server.highlight as highlight import server.utils as utils @@ -977,37 +977,33 @@ def checkpoint_grading(cid, aid): @admin.route("/course//assignments//slip", methods=["GET", "POST"]) @is_staff(course_arg='cid') -def slip_calculator(cid, aid): +def calculate_slips(cid, aid): courses, current_course = get_courses(cid) assign = Assignment.query.filter_by(id=aid, course_id=cid).one_or_none() - # if not assign or not Assignment.can(assign, current_user, 'grade'): - # flash('Cannot access assignment', 'error') - # return abort(404) + if not assign or not Assignment.can(assign, current_user, 'grade'): + flash('Cannot access assignment', 'error') + return abort(404) form = forms.SlipCalculatorForm() - # if form.validate_on_submit(): - # job = jobs.enqueue_job( - # checkpoint.assign_scores, - # description='Checkpoint Scoring for {}'.format(assign.display_name), - # timeout=600, - # course_id=cid, - # user_id=current_user.id, - # assign_id=assign.id, - # score=form.score.data, - # kind=form.kind.data, - # message=form.message.data, - # deadline=form.deadline.data, - # include_backups=form.include_backups.data) - # return redirect(url_for('.course_job', cid=cid, job_id=job.id)) - # else: - # if not form.kind.data: - # form.kind.default = 'checkpoint 1' - # if not form.deadline.data: - # form.deadline.default = utils.local_time_obj(assign.due_date, assign.course) - # form.process() + if form.validate_on_submit(): + print("submitted!") + job = jobs.enqueue_job( + slips.calculate_slips, + description='Calculate Slips for {}'.format(assign.display_name), + # timeout=600, + course_id=cid, + # user_id=current_user.id, + # assign_id=assign.id, + # score=form.score.data, + # kind=form.kind.data, + # message=form.message.data, + # deadline=form.deadline.data, + # include_backups=form.include_backups.data + ) + return redirect(url_for('.course_job', cid=cid, job_id=job.id)) return render_template( - 'staff/jobs/slip_calculator.html', + 'staff/jobs/calculate_slips.html', courses=courses, current_course=current_course, assignment=assign, diff --git a/server/jobs/slips.py b/server/jobs/slips.py new file mode 100644 index 000000000..56666b3df --- /dev/null +++ b/server/jobs/slips.py @@ -0,0 +1,8 @@ + + +from server import jobs + +@jobs.background_job +def calculate_slips(): + logger = jobs.get_job_logger() + logger.info('hello world!') diff --git a/server/templates/staff/course/assignment/assignment.html b/server/templates/staff/course/assignment/assignment.html index a56852898..3d192fe61 100644 --- a/server/templates/staff/course/assignment/assignment.html +++ b/server/templates/staff/course/assignment/assignment.html @@ -92,7 +92,7 @@

    Actions

  • Grant Extension
  • -
  • +
  • Calculate Slips
  • diff --git a/server/templates/staff/jobs/slip_calculator.html b/server/templates/staff/jobs/calculate_slips.html similarity index 100% rename from server/templates/staff/jobs/slip_calculator.html rename to server/templates/staff/jobs/calculate_slips.html From 5d5e6a9b1d003e7210e46c5b631dc24bc584deab Mon Sep 17 00:00:00 2001 From: Eric Pai Date: Wed, 3 May 2017 17:01:43 -0700 Subject: [PATCH 04/10] logic for slip calculation --- server/controllers/admin.py | 8 +++++--- server/jobs/slips.py | 25 +++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/server/controllers/admin.py b/server/controllers/admin.py index c42902da9..fee5399cc 100644 --- a/server/controllers/admin.py +++ b/server/controllers/admin.py @@ -985,15 +985,17 @@ def calculate_slips(cid, aid): return abort(404) form = forms.SlipCalculatorForm() + timescale = form.timescale.data.title() if form.validate_on_submit(): print("submitted!") job = jobs.enqueue_job( slips.calculate_slips, - description='Calculate Slips for {}'.format(assign.display_name), - # timeout=600, + description='Calculate Slip {} for {}'.format(timescale, assign.display_name), + timeout=600, course_id=cid, # user_id=current_user.id, - # assign_id=assign.id, + assign_id=assign.id, + timescale=timescale, # score=form.score.data, # kind=form.kind.data, # message=form.message.data, diff --git a/server/jobs/slips.py b/server/jobs/slips.py index 56666b3df..8f127b93d 100644 --- a/server/jobs/slips.py +++ b/server/jobs/slips.py @@ -1,8 +1,29 @@ from server import jobs +from server.models import Assignment +from server.constants import TIMESCALES +from datetime import datetime as dt +from math import ceil + +timescales = {'days':86400, 'hours':3600, 'minutes':60} + +def timediff(created, deadline, timescale): + secs_over = (created - deadline).total_seconds() + return ceil(secs_over / timescales[timescale.lower()]) @jobs.background_job -def calculate_slips(): +def calculate_slips(assign_id, timescale): logger = jobs.get_job_logger() - logger.info('hello world!') + job = jobs.get_current_job() + + logger.info('Calculating slip {}...'.format(timescale)) + assignment = Assignment.query.get(assign_id) + subms = assignment.course_submissions(include_empty=False) + deadline = assignment.due_date + for subm in subms: + email = subm['user']['email'] + created = subm['backup']['created'] + logger.info('{} {}' + .format(email, timediff(created, deadline, timescale))) + From 82c50fa880b2a263d10f00fe436fff087dacf68a Mon Sep 17 00:00:00 2001 From: Eric Pai Date: Wed, 3 May 2017 23:11:16 -0700 Subject: [PATCH 05/10] output csv string --- server/controllers/admin.py | 7 +------ server/jobs/slips.py | 39 ++++++++++++++++++++++++++++--------- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/server/controllers/admin.py b/server/controllers/admin.py index fee5399cc..108b1bebf 100644 --- a/server/controllers/admin.py +++ b/server/controllers/admin.py @@ -993,14 +993,9 @@ def calculate_slips(cid, aid): description='Calculate Slip {} for {}'.format(timescale, assign.display_name), timeout=600, course_id=cid, - # user_id=current_user.id, + user_id=current_user.id, assign_id=assign.id, timescale=timescale, - # score=form.score.data, - # kind=form.kind.data, - # message=form.message.data, - # deadline=form.deadline.data, - # include_backups=form.include_backups.data ) return redirect(url_for('.course_job', cid=cid, job_id=job.id)) diff --git a/server/jobs/slips.py b/server/jobs/slips.py index 8f127b93d..a8db7131d 100644 --- a/server/jobs/slips.py +++ b/server/jobs/slips.py @@ -1,29 +1,50 @@ - +from datetime import datetime as dt +import math +import io +import csv from server import jobs from server.models import Assignment from server.constants import TIMESCALES -from datetime import datetime as dt -from math import ceil timescales = {'days':86400, 'hours':3600, 'minutes':60} def timediff(created, deadline, timescale): secs_over = (created - deadline).total_seconds() - return ceil(secs_over / timescales[timescale.lower()]) + return math.ceil(secs_over / timescales[timescale.lower()]) + +def csv_data(header, rows): + output = io.StringIO() + writer = csv.writer(output, quoting=csv.QUOTE_NONNUMERIC) + writer.writerow(header) + [writer.writerow(row) for row in rows] + return output.getvalue() @jobs.background_job def calculate_slips(assign_id, timescale): logger = jobs.get_job_logger() job = jobs.get_current_job() - - logger.info('Calculating slip {}...'.format(timescale)) + + logger.info('Calculating Slip {}...'.format(timescale.title())) + assignment = Assignment.query.get(assign_id) subms = assignment.course_submissions(include_empty=False) deadline = assignment.due_date - for subm in subms: + + def get_row(subm): email = subm['user']['email'] created = subm['backup']['created'] - logger.info('{} {}' - .format(email, timediff(created, deadline, timescale))) + slips = min(0, timediff(created, deadline, timescale)) + return [email, slips] + + header = ('User Email', 'Slip {} Used'.format(timescale.title())) + rows = (get_row(subm) for subm in subms) + data = csv_data(header, rows) + + logger.info(repr(data)) + + # upload = ExternalFile.upload(csv_data, user_id=1, course_id=1, + # name='temp.okfile', prefix='jobs/example/') + + From a10376ae52075a3ddb88b6147bb0c73beebe84f8 Mon Sep 17 00:00:00 2001 From: Eric Pai Date: Thu, 4 May 2017 00:55:23 -0700 Subject: [PATCH 06/10] save csf file - complete assign route --- server/controllers/admin.py | 2 + server/forms.py | 1 + server/jobs/slips.py | 40 +++++++++++-------- .../templates/staff/jobs/calculate_slips.html | 2 + server/utils.py | 10 +++++ 5 files changed, 39 insertions(+), 16 deletions(-) diff --git a/server/controllers/admin.py b/server/controllers/admin.py index 108b1bebf..434adf0a1 100644 --- a/server/controllers/admin.py +++ b/server/controllers/admin.py @@ -996,6 +996,8 @@ def calculate_slips(cid, aid): user_id=current_user.id, assign_id=assign.id, timescale=timescale, + show_results=form.show_results.data, + result_kind='link', ) return redirect(url_for('.course_job', cid=cid, job_id=job.id)) diff --git a/server/forms.py b/server/forms.py index 603c2c9f4..924fae3dd 100644 --- a/server/forms.py +++ b/server/forms.py @@ -564,6 +564,7 @@ class SlipCalculatorForm(BaseForm): choices=[(c, c.title()) for c in TIMESCALES], validators=[validators.required()], description="Time scale for slip calculation.") + show_results = BooleanField('Show Results', default=False) ########## # Canvas # diff --git a/server/jobs/slips.py b/server/jobs/slips.py index a8db7131d..c28ff892f 100644 --- a/server/jobs/slips.py +++ b/server/jobs/slips.py @@ -4,7 +4,8 @@ import csv from server import jobs -from server.models import Assignment +from server.models import Assignment, ExternalFile +from server.utils import encode_id, local_time, output_csv_iterable from server.constants import TIMESCALES timescales = {'days':86400, 'hours':3600, 'minutes':60} @@ -13,21 +14,15 @@ def timediff(created, deadline, timescale): secs_over = (created - deadline).total_seconds() return math.ceil(secs_over / timescales[timescale.lower()]) -def csv_data(header, rows): - output = io.StringIO() - writer = csv.writer(output, quoting=csv.QUOTE_NONNUMERIC) - writer.writerow(header) - [writer.writerow(row) for row in rows] - return output.getvalue() - @jobs.background_job -def calculate_slips(assign_id, timescale): +def calculate_slips(assign_id, timescale, show_results): logger = jobs.get_job_logger() job = jobs.get_current_job() logger.info('Calculating Slip {}...'.format(timescale.title())) assignment = Assignment.query.get(assign_id) + course = assignment.course subms = assignment.course_submissions(include_empty=False) deadline = assignment.due_date @@ -39,12 +34,25 @@ def get_row(subm): header = ('User Email', 'Slip {} Used'.format(timescale.title())) rows = (get_row(subm) for subm in subms) - data = csv_data(header, rows) - - logger.info(repr(data)) - - # upload = ExternalFile.upload(csv_data, user_id=1, course_id=1, - # name='temp.okfile', prefix='jobs/example/') - + logger.info('Outputting csv...\n') + csv_iterable = output_csv_iterable(header, rows) + + logger.info('Uploading...') + created_time = local_time(dt.now(), course, fmt='%m-%d-%I-%M-%p') + csv_name = '{}_{}.csv'.format(assignment.name.replace('/', '-'), created_time) + upload = ExternalFile.upload(csv_iterable, + user_id=job.user.id, course_id=course.id, name=csv_name, + prefix='jobs/slips/{}'.format(course.offering)) + logger.info('Saved as: {}'.format(upload.object_name)) + + download_link = "/files/{}".format(encode_id(upload.id)) + logger.info('Download link: {} (see "result" above)\n'.format(download_link)) + + if show_results: + logger.info('Results:\n') + csv_data = ''.join([row.decode('utf-8') for row in csv_iterable]) + logger.info(csv_data) + + return download_link diff --git a/server/templates/staff/jobs/calculate_slips.html b/server/templates/staff/jobs/calculate_slips.html index 912f8db5b..08047151b 100644 --- a/server/templates/staff/jobs/calculate_slips.html +++ b/server/templates/staff/jobs/calculate_slips.html @@ -31,8 +31,10 @@

    +

    Calculate slips and save as a .csv file.

    {% call forms.render_form(form, action_text='Calculate Slips') %} {{ forms.render_field(form.timescale) }} + {{ forms.render_checkbox_field(form.show_results) }} {% endcall %}
    diff --git a/server/utils.py b/server/utils.py index 2c1d80bb6..481e1bd70 100644 --- a/server/utils.py +++ b/server/utils.py @@ -236,6 +236,16 @@ def chunks(l, n): prev_index = index +def output_csv_iterable(header, rows): + """ Generate csv string for given header list and list of rows (lists). """ + output = StringIO() + writer = csv.writer(output, quoting=csv.QUOTE_NONNUMERIC) + writer.writerow(header) + [writer.writerow(row) for row in rows] + rows = output.getvalue().split('\r\n') + return [bytes(row + '\r\n', 'utf-8') for row in rows] + + def generate_csv(query, items, selector_fn): """ Generate csv export of scores for assignment. selector_fn: 1 arg function that returns a list of dictionaries From 28d916dd49663f644518233800d0f236be97f703 Mon Sep 17 00:00:00 2001 From: Eric Pai Date: Thu, 4 May 2017 21:33:18 -0700 Subject: [PATCH 07/10] add course slips form --- server/controllers/admin.py | 40 +++++++++++++++-- server/forms.py | 11 ++++- .../staff/course/assignment/assignments.html | 22 +++++++++- .../slips.assign.html} | 0 .../staff/jobs/slips/slips.course.html | 44 +++++++++++++++++++ 5 files changed, 111 insertions(+), 6 deletions(-) rename server/templates/staff/jobs/{calculate_slips.html => slips/slips.assign.html} (100%) create mode 100644 server/templates/staff/jobs/slips/slips.course.html diff --git a/server/controllers/admin.py b/server/controllers/admin.py index 434adf0a1..af1a5a104 100644 --- a/server/controllers/admin.py +++ b/server/controllers/admin.py @@ -974,7 +974,7 @@ def checkpoint_grading(cid, aid): form=form, ) -@admin.route("/course//assignments//slip", +@admin.route("/course//assignments//slips", methods=["GET", "POST"]) @is_staff(course_arg='cid') def calculate_slips(cid, aid): @@ -984,7 +984,7 @@ def calculate_slips(cid, aid): flash('Cannot access assignment', 'error') return abort(404) - form = forms.SlipCalculatorForm() + form = forms.AssignSlipCalculatorForm() timescale = form.timescale.data.title() if form.validate_on_submit(): print("submitted!") @@ -1002,13 +1002,47 @@ def calculate_slips(cid, aid): return redirect(url_for('.course_job', cid=cid, job_id=job.id)) return render_template( - 'staff/jobs/calculate_slips.html', + 'staff/jobs/slips/slips.assign.html', courses=courses, current_course=current_course, assignment=assign, form=form, ) +@admin.route("/course//assignments/slips", + methods=["GET", "POST"]) +@is_staff(course_arg='cid') +def calculate_all_slips(cid): + courses, current_course = get_courses(cid) + # assgns = current_course.assignments + # active_asgns = [a for a in assgns if a.active] + # due_asgns = [a for a in assgns if not a.active] + form = forms.CourseSlipCalculatorForm() + form.assigns.choices = [('ONE', 'one'), ('TWO', 'two')] + form.process() + # timescale = form.timescale.data.title() + # if form.validate_on_submit(): + # print("submitted!") + # job = jobs.enqueue_job( + # slips.calculate_slips, + # description='Calculate Slip {} for {}'.format(timescale, assign.display_name), + # timeout=600, + # course_id=cid, + # user_id=current_user.id, + # assign_id=assign.id, + # timescale=timescale, + # show_results=form.show_results.data, + # result_kind='link', + # ) + # return redirect(url_for('.course_job', cid=cid, job_id=job.id)) + + return render_template( + 'staff/jobs/slips/slips.course.html', + courses=courses, + current_course=current_course, + form=form, + ) + ############## # Enrollment # ############## diff --git a/server/forms.py b/server/forms.py index 924fae3dd..30c3141cb 100644 --- a/server/forms.py +++ b/server/forms.py @@ -559,13 +559,22 @@ class ExportAssignment(BaseForm): description="Enable to remove identifying information from submissions") -class SlipCalculatorForm(BaseForm): +class AssignSlipCalculatorForm(BaseForm): timescale = SelectField('Time Scale', default="Days", choices=[(c, c.title()) for c in TIMESCALES], validators=[validators.required()], description="Time scale for slip calculation.") show_results = BooleanField('Show Results', default=False) +class CourseSlipCalculatorForm(BaseForm): + timescale = SelectField('Time Scale', default="Days", + choices=[(c, c.title()) for c in TIMESCALES], + validators=[validators.required()], + description="Time scale for slip calculation.") + assigns = MultiCheckboxField('Assignments', + description="Select which assignments to calculate slips for.") + show_results = BooleanField('Show Results', default=False) + ########## # Canvas # ########## diff --git a/server/templates/staff/course/assignment/assignments.html b/server/templates/staff/course/assignment/assignments.html index bdfdb9edb..ddfdea6ce 100644 --- a/server/templates/staff/course/assignment/assignments.html +++ b/server/templates/staff/course/assignment/assignments.html @@ -19,6 +19,25 @@

    {% include 'alerts.html' %} + + +
    @@ -66,9 +85,8 @@

    Active Assignments

    Completed Assignments

    -
    -
    diff --git a/server/templates/staff/jobs/calculate_slips.html b/server/templates/staff/jobs/slips/slips.assign.html similarity index 100% rename from server/templates/staff/jobs/calculate_slips.html rename to server/templates/staff/jobs/slips/slips.assign.html diff --git a/server/templates/staff/jobs/slips/slips.course.html b/server/templates/staff/jobs/slips/slips.course.html new file mode 100644 index 000000000..365509541 --- /dev/null +++ b/server/templates/staff/jobs/slips/slips.course.html @@ -0,0 +1,44 @@ +{% extends "staff/base.html" %} +{% import "staff/_formhelpers.html" as forms %} + +{% block title %} Calculate Slips for {{ current_course.display_name }} {% endblock %} + +{% block main %} +
    +

    + Calculate Slips for {{ current_course.display_name }} + {{ current_course.offering }} +

    +
    +
    +
    + {% include 'alerts.html' %} +
    +
    +
    +
    +

    Calculate slips and save as a .csv file.

    + {% call forms.render_form(form, action_text='Calculate Slips') %} + {{ forms.render_field(form.timescale) }} + {{ forms.render_checkbox_field(form.assigns) }} + {{ forms.render_checkbox_field(form.show_results) }} + {% endcall %} +
    +
    +
    +
    +
    +{% endblock %} From 40736667b8327a0e3dd9f2bb00d6cbb163e4f251 Mon Sep 17 00:00:00 2001 From: Eric Pai Date: Thu, 4 May 2017 22:17:11 -0700 Subject: [PATCH 08/10] fix validation errors --- server/controllers/admin.py | 40 ++++++++++++++++++------------------- server/forms.py | 14 ++++--------- server/jobs/slips.py | 9 ++++++++- 3 files changed, 31 insertions(+), 32 deletions(-) diff --git a/server/controllers/admin.py b/server/controllers/admin.py index af1a5a104..c6283e81e 100644 --- a/server/controllers/admin.py +++ b/server/controllers/admin.py @@ -987,9 +987,8 @@ def calculate_slips(cid, aid): form = forms.AssignSlipCalculatorForm() timescale = form.timescale.data.title() if form.validate_on_submit(): - print("submitted!") job = jobs.enqueue_job( - slips.calculate_slips, + slips.calculate_slips_assign, description='Calculate Slip {} for {}'.format(timescale, assign.display_name), timeout=600, course_id=cid, @@ -1014,27 +1013,26 @@ def calculate_slips(cid, aid): @is_staff(course_arg='cid') def calculate_all_slips(cid): courses, current_course = get_courses(cid) - # assgns = current_course.assignments - # active_asgns = [a for a in assgns if a.active] - # due_asgns = [a for a in assgns if not a.active] + assignments = current_course.assignments form = forms.CourseSlipCalculatorForm() - form.assigns.choices = [('ONE', 'one'), ('TWO', 'two')] + form.assigns.choices = [(a.id, a.display_name) for a in assignments] + form.assigns.default = [a.id for a in assignments] form.process() - # timescale = form.timescale.data.title() - # if form.validate_on_submit(): - # print("submitted!") - # job = jobs.enqueue_job( - # slips.calculate_slips, - # description='Calculate Slip {} for {}'.format(timescale, assign.display_name), - # timeout=600, - # course_id=cid, - # user_id=current_user.id, - # assign_id=assign.id, - # timescale=timescale, - # show_results=form.show_results.data, - # result_kind='link', - # ) - # return redirect(url_for('.course_job', cid=cid, job_id=job.id)) + timescale = form.timescale.data.title() + if form.validate_on_submit(): + job = jobs.enqueue_job( + slips.calculate_slips_course, + description="Calculate Slip {} for {}'s assignments" + .format(timescale, current_course.display_name), + timeout=600, + course_id=cid, + user_id=current_user.id, + timescale=timescale, + assigns=form.assigns.data, + show_results=form.show_results.data, + result_kind='link', + ) + return redirect(url_for('.course_job', cid=cid, job_id=job.id)) return render_template( 'staff/jobs/slips/slips.course.html', diff --git a/server/forms.py b/server/forms.py index 30c3141cb..5da7f3afc 100644 --- a/server/forms.py +++ b/server/forms.py @@ -560,20 +560,14 @@ class ExportAssignment(BaseForm): class AssignSlipCalculatorForm(BaseForm): - timescale = SelectField('Time Scale', default="Days", - choices=[(c, c.title()) for c in TIMESCALES], - validators=[validators.required()], + timescale = SelectField('Time Scale', default="days", + choices=[(c.lower(), c.title()) for c in TIMESCALES], description="Time scale for slip calculation.") show_results = BooleanField('Show Results', default=False) -class CourseSlipCalculatorForm(BaseForm): - timescale = SelectField('Time Scale', default="Days", - choices=[(c, c.title()) for c in TIMESCALES], - validators=[validators.required()], - description="Time scale for slip calculation.") - assigns = MultiCheckboxField('Assignments', +class CourseSlipCalculatorForm(AssignSlipCalculatorForm): + assigns = MultiCheckboxField('Assignments', coerce=int, description="Select which assignments to calculate slips for.") - show_results = BooleanField('Show Results', default=False) ########## # Canvas # diff --git a/server/jobs/slips.py b/server/jobs/slips.py index c28ff892f..e0929b303 100644 --- a/server/jobs/slips.py +++ b/server/jobs/slips.py @@ -15,7 +15,14 @@ def timediff(created, deadline, timescale): return math.ceil(secs_over / timescales[timescale.lower()]) @jobs.background_job -def calculate_slips(assign_id, timescale, show_results): +def calculate_slips_course(assigns, timescale, show_results): + logger = jobs.get_job_logger() + logger.info(assigns) + logger.info(timescale) + logger.info(show_results) + +@jobs.background_job +def calculate_slips_assign(assign_id, timescale, show_results): logger = jobs.get_job_logger() job = jobs.get_current_job() From ddd9e40fdc39872d2a3f66c5d7f07095e2890d2b Mon Sep 17 00:00:00 2001 From: Eric Pai Date: Thu, 4 May 2017 22:19:43 -0700 Subject: [PATCH 09/10] cleaup function names --- server/controllers/admin.py | 8 ++++---- server/jobs/slips.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/server/controllers/admin.py b/server/controllers/admin.py index c6283e81e..a4406e004 100644 --- a/server/controllers/admin.py +++ b/server/controllers/admin.py @@ -977,7 +977,7 @@ def checkpoint_grading(cid, aid): @admin.route("/course//assignments//slips", methods=["GET", "POST"]) @is_staff(course_arg='cid') -def calculate_slips(cid, aid): +def calculate_assign_slips(cid, aid): courses, current_course = get_courses(cid) assign = Assignment.query.filter_by(id=aid, course_id=cid).one_or_none() if not assign or not Assignment.can(assign, current_user, 'grade'): @@ -1011,7 +1011,7 @@ def calculate_slips(cid, aid): @admin.route("/course//assignments/slips", methods=["GET", "POST"]) @is_staff(course_arg='cid') -def calculate_all_slips(cid): +def calculate_course_slips(cid): courses, current_course = get_courses(cid) assignments = current_course.assignments form = forms.CourseSlipCalculatorForm() @@ -1021,8 +1021,8 @@ def calculate_all_slips(cid): timescale = form.timescale.data.title() if form.validate_on_submit(): job = jobs.enqueue_job( - slips.calculate_slips_course, - description="Calculate Slip {} for {}'s assignments" + slips.calculate_course_slips, + description="Calculate Slip {} for {}'s Assignments" .format(timescale, current_course.display_name), timeout=600, course_id=cid, diff --git a/server/jobs/slips.py b/server/jobs/slips.py index e0929b303..c54440cc2 100644 --- a/server/jobs/slips.py +++ b/server/jobs/slips.py @@ -15,14 +15,14 @@ def timediff(created, deadline, timescale): return math.ceil(secs_over / timescales[timescale.lower()]) @jobs.background_job -def calculate_slips_course(assigns, timescale, show_results): +def calculate_course_slips(assigns, timescale, show_results): logger = jobs.get_job_logger() logger.info(assigns) logger.info(timescale) logger.info(show_results) @jobs.background_job -def calculate_slips_assign(assign_id, timescale, show_results): +def calculate_assign_slips(assign_id, timescale, show_results): logger = jobs.get_job_logger() job = jobs.get_current_job() From 0fab8baa39f83909c0e07087f25bcd0bffa59b6f Mon Sep 17 00:00:00 2001 From: Eric Pai Date: Fri, 5 May 2017 01:26:39 -0700 Subject: [PATCH 10/10] finish course slips job --- server/controllers/admin.py | 11 ++- server/forms.py | 4 +- server/jobs/slips.py | 99 +++++++++++++------ .../staff/course/assignment/assignment.html | 2 +- .../staff/course/assignment/assignments.html | 2 +- 5 files changed, 81 insertions(+), 37 deletions(-) diff --git a/server/controllers/admin.py b/server/controllers/admin.py index a4406e004..48f26c720 100644 --- a/server/controllers/admin.py +++ b/server/controllers/admin.py @@ -988,7 +988,7 @@ def calculate_assign_slips(cid, aid): timescale = form.timescale.data.title() if form.validate_on_submit(): job = jobs.enqueue_job( - slips.calculate_slips_assign, + slips.calculate_assign_slips, description='Calculate Slip {} for {}'.format(timescale, assign.display_name), timeout=600, course_id=cid, @@ -1014,10 +1014,13 @@ def calculate_assign_slips(cid, aid): def calculate_course_slips(cid): courses, current_course = get_courses(cid) assignments = current_course.assignments + form = forms.CourseSlipCalculatorForm() - form.assigns.choices = [(a.id, a.display_name) for a in assignments] - form.assigns.default = [a.id for a in assignments] - form.process() + inactive_assigns = [a for a in assignments if not a.active] + form.assigns.choices = [(a.id, a.display_name) for a in inactive_assigns] + form.assigns.default = [a.id for a in inactive_assigns] + form.process(request.form) + timescale = form.timescale.data.title() if form.validate_on_submit(): job = jobs.enqueue_job( diff --git a/server/forms.py b/server/forms.py index 5da7f3afc..900e81489 100644 --- a/server/forms.py +++ b/server/forms.py @@ -566,8 +566,8 @@ class AssignSlipCalculatorForm(BaseForm): show_results = BooleanField('Show Results', default=False) class CourseSlipCalculatorForm(AssignSlipCalculatorForm): - assigns = MultiCheckboxField('Assignments', coerce=int, - description="Select which assignments to calculate slips for.") + assigns = MultiCheckboxField('Completed Assignments ', coerce=int, + description="Select which completed assignments to calculate slips for.") ########## # Canvas # diff --git a/server/jobs/slips.py b/server/jobs/slips.py index c54440cc2..1fe8712be 100644 --- a/server/jobs/slips.py +++ b/server/jobs/slips.py @@ -1,7 +1,8 @@ -from datetime import datetime as dt import math import io import csv +from collections import defaultdict +from datetime import datetime as dt from server import jobs from server.models import Assignment, ExternalFile @@ -14,20 +15,76 @@ def timediff(created, deadline, timescale): secs_over = (created - deadline).total_seconds() return math.ceil(secs_over / timescales[timescale.lower()]) + +def save_csv(csv_name, header, rows, show_results, user, course, logger): + logger.info('Outputting csv...\n') + csv_iterable = output_csv_iterable(header, rows) + + logger.info('Uploading...') + upload = ExternalFile.upload(csv_iterable, + user_id=user.id, course_id=course.id, name=csv_name, + prefix='jobs/slips/{}'.format(course.offering)) + logger.info('Saved as: {}'.format(upload.object_name)) + + download_link = "/files/{}".format(encode_id(upload.id)) + logger.info('Download link: {} (see "result" above)\n'.format(download_link)) + + if show_results: + logger.info('Results:\n') + csv_data = ''.join([row.decode('utf-8') for row in csv_iterable]) + logger.info(csv_data) + + return download_link + + @jobs.background_job def calculate_course_slips(assigns, timescale, show_results): logger = jobs.get_job_logger() - logger.info(assigns) - logger.info(timescale) - logger.info(show_results) + logger.info('Calculating Slip {}...\n'.format(timescale.title())) + + job = jobs.get_current_job() + user = job.user + course = job.course + assigns_set = set(assigns) + assigns = (a for a in course.assignments if a.id in assigns_set) + + course_slips = defaultdict(list) + for i, assign in enumerate(assigns, 1): + logger.info('Processing {} ({} of {})...' + .format(assign.display_name, i, len(assigns_set))) + subms = assign.course_submissions(include_empty=False) + deadline = assign.due_date + assign_slips = {} + for subm in subms: + email = subm['user']['email'] + created = subm['backup']['created'] + slips = max(0, timediff(created, deadline, timescale)) + assign_slips[email] = [(assign.display_name, slips)] + course_slips = {k:course_slips[k] + assign_slips[k] + for k in course_slips.keys() | assign_slips.keys()} + + def get_row(email, assign_slips): + total_slips = sum((s for a, s in assign_slips)) + assignments = ', '.join([a for a, s in assign_slips if s > 0]) + return (email, total_slips, assignments) + + header = ( + 'User Email', + 'Slip {} Used'.format(timescale.title()), + 'Late Assignments') + rows = (get_row(*user_slips) for user_slips in course_slips.items()) + created_time = local_time(dt.now(), course, fmt='%m-%d-%I-%M-%p') + csv_name = '{}_{}.csv'.format(course.offering.replace('/', '-'), created_time) + + return save_csv(csv_name, header, rows, show_results, user, course, logger) + @jobs.background_job def calculate_assign_slips(assign_id, timescale, show_results): logger = jobs.get_job_logger() - job = jobs.get_current_job() - logger.info('Calculating Slip {}...'.format(timescale.title())) - + + user = jobs.get_current_job().user assignment = Assignment.query.get(assign_id) course = assignment.course subms = assignment.course_submissions(include_empty=False) @@ -36,30 +93,14 @@ def calculate_assign_slips(assign_id, timescale, show_results): def get_row(subm): email = subm['user']['email'] created = subm['backup']['created'] - slips = min(0, timediff(created, deadline, timescale)) - return [email, slips] + slips = max(0, timediff(created, deadline, timescale)) + return (email, slips) - header = ('User Email', 'Slip {} Used'.format(timescale.title())) + header = ( + 'User Email', + 'Slip {} Used'.format(timescale.title())) rows = (get_row(subm) for subm in subms) - logger.info('Outputting csv...\n') - csv_iterable = output_csv_iterable(header, rows) - - logger.info('Uploading...') created_time = local_time(dt.now(), course, fmt='%m-%d-%I-%M-%p') csv_name = '{}_{}.csv'.format(assignment.name.replace('/', '-'), created_time) - upload = ExternalFile.upload(csv_iterable, - user_id=job.user.id, course_id=course.id, name=csv_name, - prefix='jobs/slips/{}'.format(course.offering)) - logger.info('Saved as: {}'.format(upload.object_name)) - - download_link = "/files/{}".format(encode_id(upload.id)) - logger.info('Download link: {} (see "result" above)\n'.format(download_link)) - - if show_results: - logger.info('Results:\n') - csv_data = ''.join([row.decode('utf-8') for row in csv_iterable]) - logger.info(csv_data) - - return download_link - + return save_csv(csv_name, header, rows, show_results, user, course, logger) \ No newline at end of file diff --git a/server/templates/staff/course/assignment/assignment.html b/server/templates/staff/course/assignment/assignment.html index 3d192fe61..6f0f649d8 100644 --- a/server/templates/staff/course/assignment/assignment.html +++ b/server/templates/staff/course/assignment/assignment.html @@ -92,7 +92,7 @@

    Actions

  • Grant Extension
  • -
  • +
  • Calculate Slips
  • diff --git a/server/templates/staff/course/assignment/assignments.html b/server/templates/staff/course/assignment/assignments.html index ddfdea6ce..2a10d0b49 100644 --- a/server/templates/staff/course/assignment/assignments.html +++ b/server/templates/staff/course/assignment/assignments.html @@ -30,7 +30,7 @@

    Actions