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

Slips Calculator Job #1384

Open
wants to merge 28 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b7d01a5
add slipcalc form
epai May 2, 2017
9f0fe75
add link to calculate slips
epai May 2, 2017
d6cbe8c
setup basic job scaffold
epai May 3, 2017
5d5e6a9
logic for slip calculation
epai May 4, 2017
82c50fa
output csv string
epai May 4, 2017
a10376a
save csf file - complete assign route
epai May 4, 2017
8ef25c9
Merge branch 'master' of https://github.com/Cal-CS-61A-Staff/ok
epai May 4, 2017
28d916d
add course slips form
epai May 5, 2017
4073666
fix validation errors
epai May 5, 2017
ddd9e40
cleaup function names
epai May 5, 2017
0fab8ba
finish course slips job
epai May 5, 2017
7e52eee
Merged with the old pull request for calculating slip days
Feb 17, 2020
2c15c3b
Added the TODO list to upgrade the previous pull request
Feb 17, 2020
f804a16
Added user ID to csm
Feb 18, 2020
099f50f
Specified questions
Feb 19, 2020
77945cb
Added SID to calculate slips on a single assignment
Feb 24, 2020
fd2abe6
Modified calculate_assign_slips to calculate slips for the FINAL subm…
Feb 24, 2020
7e22a9e
Tested on the database, calculate_assign_slips functions correctly
Feb 26, 2020
785beb7
Changed filename
Mar 4, 2020
97301ee
Fixed slips calculation for the whole course
Mar 4, 2020
a2d6a08
Refined slips jobs
Mar 4, 2020
6bd8c63
Got rid of output_csv_iterable
Mar 4, 2020
c3a8cd5
Fixed bug when user hasn't submitted yet
Mar 4, 2020
9a85b24
Specified TODO
Mar 4, 2020
bf60677
Changed TIMESCALES
Mar 4, 2020
23d9503
Removed commented old TIMESCALES
Mar 4, 2020
da4b3d6
Reworked course slips
Mar 24, 2020
d53da93
Removed comments
Mar 24, 2020
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
3 changes: 3 additions & 0 deletions server/constants.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""App constants"""
import os
from collections import OrderedDict

STUDENT_ROLE = 'student'
GRADER_ROLE = 'grader'
Expand All @@ -21,6 +22,8 @@
'regrade', 'revision', 'checkpoint 1', 'checkpoint 2',
'private', 'autograder', 'error']

TIMESCALES = OrderedDict([('days', 86400), ('hours', 3600), ('minutes', 60)])

API_PREFIX = '/api'
OAUTH_SCOPES = ['all', 'email']
OAUTH_OUT_OF_BAND_URI = 'urn:ietf:wg:oauth:2.0:oob'
Expand Down
74 changes: 71 additions & 3 deletions server/controllers/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,8 @@
from server.extensions import cache
import server.forms as forms
import server.jobs as jobs
from server.jobs import (example, export, moss, scores_audit, github_search,
scores_notify, checkpoint, effort, upload_scores,
export_grades)
from server.jobs import (example, export, moss, scores_audit, github_search, scores_notify,
checkpoint, effort, upload_scores, export_grades, slips)

import server.highlight as highlight
import server.utils as utils
Expand Down Expand Up @@ -1290,6 +1289,75 @@ def checkpoint_grading(cid, aid):
form=form,
)

@admin.route("/course/<int:cid>/assignments/<int:aid>/slips",
methods=["GET", "POST"])
@is_staff(course_arg='cid')
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'):
flash('Cannot access assignment', 'error')
return abort(404)

form = forms.AssignSlipCalculatorForm()
timescale = form.timescale.data.title()
if form.validate_on_submit():
job = jobs.enqueue_job(
slips.calculate_assign_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.assign.html',
courses=courses,
current_course=current_course,
assignment=assign,
form=form,
)

@admin.route("/course/<int:cid>/assignments/slips",
methods=["GET", "POST"])
@is_staff(course_arg='cid')
def calculate_course_slips(cid):
courses, current_course = get_courses(cid)
assignments = current_course.assignments

form = forms.CourseSlipCalculatorForm()
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(
slips.calculate_course_slips,
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',
courses=courses,
current_course=current_course,
form=form,
)

##############
# Enrollment #
Expand Down
13 changes: 12 additions & 1 deletion server/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -769,6 +769,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.keys()],
description="Time scale for slip calculation.")
show_results = BooleanField('Show Results', default=False)

class CourseSlipCalculatorForm(AssignSlipCalculatorForm):
assigns = MultiCheckboxField('Completed Assignments ', coerce=int,
description="Select which completed assignments to calculate slips for.")

##########
# Canvas #
##########
Expand Down
147 changes: 147 additions & 0 deletions server/jobs/slips.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
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, User
from server.utils import encode_id, local_time, generate_csv
from server.constants import TIMESCALES

"""
TODO:
- Support for timezone in filename?
- Remake templates as specified in the old pull request?
"""


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')

def selector_fn(lst):
if len(lst) != len(header):
raise IndexError(str(lst) + " " + str(header))
result = {}
for i in range(len(lst)):
result[header[i]] = lst[i]
return [result]

csv_iterable = list(map(lambda x: bytes(x, 'utf-8'), generate_csv(rows, header, selector_fn)))

logger.info('Uploading...')
upload = ExternalFile.upload(csv_iterable,
user_id=user.id, course_id=course.id, name=csv_name,
prefix='slips_')
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]
rows = []

enrollments = job.course.get_students()
for enrollment in enrollments:
sid = enrollment.sid
student = enrollment.user
email = student.email
row = [sid, email]
student_id = student.id
logger.info('Processing {}\'s submissions'.format(email))
for assignment in assigns:
deadline = assignment.due_date
subm = assignment.final_submission([student_id])
if subm:
created = subm.submission_time
slips = max(0, timediff(created, deadline, timescale))
else:
slips = 0
row.append(slips)
rows.append(row)

header = [
'User SID',
'User Email',
]
for assignment in assigns:
assign_name = assignment.display_name
header.append('Slip {} Used on '.format(timescale.title())
+ assign_name)

created_time = local_time(dt.now(), course, fmt='%m-%d_%I-%M-%p')
csv_name = '{}_{}.csv'.format(course.display_name.replace('/', '-'), created_time)

return save_csv(csv_name, header, rows, show_results, user, course, logger)


def get_students_with_submission(assignment):
"""Get a list of IDs of students who have made a submission
for the current assignment.

:param ASSIGNMENT instance of the model Assignment

This code is copied from the assignment_stats() method
in the Assignment model methods. May need refactoring."""

data = assignment.course_submissions()
students_ids = set(s['user']['id'] for s in data if s['backup'] and s['backup']['submit'])
return students_ids


@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
students_ids = get_students_with_submission(assignment)
subms = []
for id in students_ids:
subm = assignment.final_submission([id])
if subm:
subms.append(subm)
deadline = assignment.due_date
rows = []
for subm in subms:
curr_user = subm.submitter
enrollment = curr_user.enrollments()[0]
sid = enrollment.sid
email = curr_user.email
created = subm.submission_time
slips = max(0, timediff(created, deadline, timescale))
if slips > 0:
rows.append([sid, email, slips])

header = [
'User SID',
'User Email',
'Slip {} Used'.format(timescale.title())]
created_time = local_time(dt.now(), course, fmt='%m-%d_%I-%M-%p')
csv_name = '{}_{}.csv'.format(assignment.display_name.replace('/', '-'), created_time)

return save_csv(csv_name, header, rows, show_results, user, course, logger)
6 changes: 5 additions & 1 deletion server/templates/staff/course/assignment/assignment.html
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,11 @@ <h3 class="box-title">Actions</h3>
<i class="fa fa-thumbs-up"></i> Effort Grading
</a></li>
<li> <a href="{{ autograder_url }}" target="_blank" type="button">
<i class="fa fa-gear"></i> Configure Autograder
<i class="fa fa-gear"></i> Configure Autograder
</a></li>

<li> <a href="{{ url_for('.calculate_assign_slips', cid=current_course.id, aid=assignment.id) }}" type="button">
<i class="fa fa-calculator"></i> Calculate Slips
</a></li>
<li>
{% call forms.render_form_bare(CSRFForm(), action_url=url_for('.autograde', cid=current_course.id, aid=assignment.id), class_='form') %}
Expand Down
22 changes: 20 additions & 2 deletions server/templates/staff/course/assignment/assignments.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,25 @@ <h1>
<section class="content">
{% include 'alerts.html' %}

<!-- Default box -->
<div class="box box-solid">
<div class="box-header with-border">
<h3 class="box-title">Actions</h3>
<div class="box-tools pull-right">
<button type="button" class="btn btn-box-tool" data-widget="collapse" data-toggle="tooltip" title="" data-original-title="Collapse">
<i class="fa fa-minus"></i></button>
</div>
</div>
<div class="box-body">
<ul class="nav nav-pills">
<li> <a href="{{url_for('.calculate_course_slips', cid=current_course.id)}}" type="button">
<i class="fa fa-calculator"></i> Calculate Slips
</a></li>
</ul>
</div>
<!-- /.box-body -->
</div>

<!-- Default box -->
<div class="box">
<div class="box-header with-border">
Expand Down Expand Up @@ -66,9 +85,8 @@ <h3 class="box-title">Active Assignments</h3>
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">Completed Assignments</h3>

<div class="box-tools pull-right">
<button type="button" class="btn btn-box-tool" data-widget="collapse" data-toggle="tooltip" title="Collapse">
<button type="button" class="btn btn-box-tool" data-widget="collapse" data-toggle="tooltip" title="" data-original-title="Collapse">
<i class="fa fa-minus"></i></button>
</div>
</div>
Expand Down
44 changes: 44 additions & 0 deletions server/templates/staff/jobs/slips/slips.assign.html
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 %}
Loading