-
Notifications
You must be signed in to change notification settings - Fork 83
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
Slip Calculator Jobs for Assignment and Course #1148
base: master
Are you sure you want to change the base?
Changes from all commits
b7d01a5
9f0fe75
d6cbe8c
5d5e6a9
82c50fa
a10376a
8ef25c9
28d916d
4073666
ddd9e40
0fab8ba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 AssignSlipCalculatorForm(BaseForm): | ||
timescale = SelectField('Time Scale', default="days", | ||
choices=[(c.lower(), c.title()) for c in TIMESCALES], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
description="Time scale for slip calculation.") | ||
show_results = BooleanField('Show Results', default=False) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure if this is useful as an option. We should either always show the results, or don't |
||
|
||
class CourseSlipCalculatorForm(AssignSlipCalculatorForm): | ||
assigns = MultiCheckboxField('Completed Assignments ', coerce=int, | ||
description="Select which completed assignments to calculate slips for.") | ||
|
||
########## | ||
# Canvas # | ||
########## | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
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 | ||
from server.utils import encode_id, local_time, output_csv_iterable | ||
from server.constants import TIMESCALES | ||
|
||
timescales = {'days':86400, 'hours':3600, 'minutes':60} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should go in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did this originally, but realized if I would then have to use the dictionary keys to fill in the form options, which wouldn't have a guaranteed ordering. Now that I've thought about it, I think I'll just use an OrderedDict to preserve the ordering of the keys for display on the form. |
||
|
||
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('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() | ||
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) | ||
deadline = assignment.due_date | ||
|
||
def get_row(subm): | ||
email = subm['user']['email'] | ||
created = subm['backup']['created'] | ||
slips = max(0, timediff(created, deadline, timescale)) | ||
return (email, slips) | ||
|
||
header = ( | ||
'User Email', | ||
'Slip {} Used'.format(timescale.title())) | ||
rows = (get_row(subm) for subm in subms) | ||
created_time = local_time(dt.now(), course, fmt='%m-%d-%I-%M-%p') | ||
csv_name = '{}_{}.csv'.format(assignment.name.replace('/', '-'), created_time) | ||
|
||
return save_csv(csv_name, header, rows, show_results, user, course, logger) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
{% extends "staff/base.html" %} | ||
{% import "staff/_formhelpers.html" as forms %} | ||
|
||
{% block title %} Calculate Slips for {{ assignment.display_name }} {% endblock %} | ||
|
||
{% block main %} | ||
<section class="content-header"> | ||
<h1> | ||
Calculate Slips for {{ assignment.display_name }} | ||
<small>{{ current_course.offering }}</small> | ||
</h1> | ||
<ol class="breadcrumb"> | ||
<li><a href="{{ url_for(".course", cid=current_course.id) }}"> | ||
<i class="fa fa-university"></i> {{ current_course.offering }} | ||
</a></li> | ||
<li><a href="{{ url_for('.course_assignments', cid=current_course.id) }}"> | ||
<i class="fa fa-list"></i> Assignments</a> | ||
</li> | ||
<li> <a href="{{ url_for('.assignment', cid=current_course.id, aid=assignment.id) }}"><i class="fa fa-book"></i> {{ assignment.display_name }} </a></li> | ||
<li><a href="{{ url_for(".course_jobs", cid=current_course.id) }}"> | ||
<i class="fa fa-list"></i>Jobs | ||
</a></li> | ||
<li class="active"><a href="#"> | ||
<i class="fa fa-inbox"></i>Calculate Slips</a> | ||
</li> | ||
</ol> | ||
</section> | ||
<section class="content"> | ||
{% include 'alerts.html' %} | ||
<div class="row"> | ||
<div class="col-md-12"> | ||
<div class="box"> | ||
<div class="box-body"> | ||
<p> Calculate slips and save as a .csv file. </p> | ||
{% call forms.render_form(form, action_text='Calculate Slips') %} | ||
{{ forms.render_field(form.timescale) }} | ||
{{ forms.render_checkbox_field(form.show_results) }} | ||
{% endcall %} | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
</section> | ||
{% endblock %} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 %} | ||
<section class="content-header"> | ||
<h1> | ||
Calculate Slips for {{ current_course.display_name }} | ||
<small>{{ current_course.offering }}</small> | ||
</h1> | ||
<ol class="breadcrumb"> | ||
<li><a href="{{ url_for(".course", cid=current_course.id) }}"> | ||
<i class="fa fa-university"></i> {{ current_course.offering }} | ||
</a></li> | ||
<li><a href="{{ url_for('.course_assignments', cid=current_course.id) }}"> | ||
<i class="fa fa-list"></i> Assignments</a> | ||
</li> | ||
<li><a href="{{ url_for(".course_jobs", cid=current_course.id) }}"> | ||
<i class="fa fa-list"></i>Jobs | ||
</a></li> | ||
<li class="active"><a href="#"> | ||
<i class="fa fa-inbox"></i>Calculate Slips</a> | ||
</li> | ||
</ol> | ||
</section> | ||
<section class="content"> | ||
{% include 'alerts.html' %} | ||
<div class="row"> | ||
<div class="col-md-12"> | ||
<div class="box"> | ||
<div class="box-body"> | ||
<p> Calculate slips and save as a .csv file. </p> | ||
{% 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 %} | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
</section> | ||
{% endblock %} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -236,6 +236,16 @@ def chunks(l, n): | |
prev_index = index | ||
|
||
|
||
def output_csv_iterable(header, rows): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is too similar to Instead, try to use
where |
||
""" 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 | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest keeping the timescale lowercase, so it matches up with the TIMESCALES enum. And you won't need the
.lower()
on L16 in slips.py