"""Grade revision UI pages.
Implements the grade revision display screen, as well as the interface for
preparing new revisions.
"""
from collections import namedtuple
import csv
from datetime import date
from itertools import groupby
from operator import attrgetter
import re
from ll.xist.ns import html
from uw.stringtools import split_upload_text
from uw.web.html.form import render_hidden
from uw.web.html.format import make_table_format, format_date, format_datetime, format_return
from uw.web.wsgi.delegate import delegate_action, delegate_get_post, status
from uw.web.wsgi.form import use_form_param
from uw.web.wsgi.function import return_html
from uw.local.util.format import format_person
from uw.local.util.identity import use_remote_identity
@delegate_get_post
@return_html
def grades_index_handler (cursor, term, admin, roles):
"""UI implementation of main grade revision display.
Shows all grade revisions currently recorded for this admin offering,
and links to the form to prepare additional revisions.
"""
if not set (['ISC', 'INST']) & roles:
raise status.HTTPForbidden ()
result = [format_return ('Main Menu', None, None, 'Offering')]
grades = cursor.execute_tuples ("select * from grade_revision_complete where (term_id, admin_id) = (%(term_id)s, %(admin_id)s) and not no_revision order by surname, givennames, revision_updated", term_id=term.code (), admin_id=admin.admin_id)
if grades:
table = html.table (html.tr (
html.th ('UW ID'),
html.th ('Student'),
html.th ('Quest Grade'),
html.th ('Revision'),
html.th ('Prepared by'),
html.th ('Submitted'),
))
for uw_id, grades in groupby (grades, attrgetter ('external_id')):
table.extend (format_student_grades (cursor, uw_id, tuple (grades), 'ISC' in roles))
result.append (table)
else:
result.append (html.p ('No grade revisions yet.'))
if set (['ISC', 'INST']) & roles:
message = format_unavailable_message (cursor, term)
if message is None:
message = html.a ('Prepare…', href="create")
result.append (html.p (message))
return "%s (%s) Grade Revisions" % (admin.admin_description, term.description ()), result
@return_html
def create_get_handler (cursor, term, admin, roles):
"""UI implementation of grade revision creation form.
Displays a box into which proposed grade revisions may be pasted.
"""
if not set (['ISC', 'INST']) & roles:
raise status.HTTPForbidden ()
message = format_unavailable_message (cursor, term)
if message is not None:
return 'Error: System Unavailable', html.p (message)
result = [format_return ('Main Menu', None, None, 'Offering', dot='Grades')]
result.append (html.form (
html.p ('Paste or type some grade revisions, one per line.'),
html.p ('One column must have the UW student IDs; another must have the new grades. Columns may be comma-separated or tab-delimited:'),
html.textarea (name="revisions", rows=15, cols=40),
html.p (html.input (type="submit", name="!review-grades", value="Next…")),
method="post"
))
return 'Prepare Grade Revisions', result
uw_id_re = re.compile ('^[0-9]{8}$')
grade_re = re.compile ('^(0|[1-9][0-9]?|100|AEG|CR|DNW|INC|IP|NCR|UR|)$')
[docs]def process_re_column (regex, column):
"""Filter a column of text values, replacing non-matching values with None.
:param regex: The compiled regular expression to match.
:param list column: The list of text values to check.
"""
return [None if regex.match (field or '') is None else field or '' for field in column]
[docs]def find_relevant_columns (grid):
"""Find the UW ID and proposed grade columns in a grid of uploaded data.
:param list grid: A rectangular array of field values.
:return: Two lists, the first a list of UW IDs, the second a list of
proposed grade revisions.
Scan through the provided field values to identify the columns with the
most valid UW IDs and proposed grades, and return those columns as the
result.
"""
most_ids_column = None
most_ids_count = 0
most_grades_column = None
most_grades_count = 0
for column in zip (*grid):
this_column = process_re_column (uw_id_re, column)
if len ([f for f in this_column if f is not None]) > most_ids_count:
most_ids_column = this_column
this_column = process_re_column (grade_re, column)
if len ([f for f in this_column if f is not None]) > most_grades_count:
most_grades_column = this_column
return most_ids_column, most_grades_column
student_row = namedtuple ('student_row', ['uw_id', 'grade', 'db'])
[docs]def parse_revision_upload (cursor, term, admin, form):
"""Handle initial upload of proposed grade revisions.
"""
revisions = form.optional_field_value ('revisions')
revisions = split_upload_text (revisions)
ids, grades = find_relevant_columns (revisions)
errors = []
if ids is None:
errors.append ('No ID column found')
if grades is None:
errors.append ('No grade column found')
result = [format_return ('Main Menu', None, None, 'Offering', dot='Grades')]
if errors:
result.append (html.ul (html.li (error) for error in errors))
else:
students = cursor.execute_tuples ("select * from grade_revision_complete where (term_id, admin_id) = (%(term_id)s, %(admin_id)s) and external_id = any (%(uw_ids)s)", term_id=term.code (), admin_id=admin.admin_id, uw_ids=[id for id in ids if id is not None])
students = dict ((uw_id, tuple (grades)) for uw_id, grades in groupby (students, attrgetter ('external_id')))
students = list(map (student_row, ids, grades, (students.get (id) for id in ids)))
table = html.table (html.tr (
html.th ('UW ID'),
html.th ('Student'),
html.th ('Quest Grade'),
html.th ('Revision'),
html.th ('Prepared by'),
html.th ('Submitted'),
html.th ('Proposed Grade'),
))
rowcount = 0
for s in students:
if s.uw_id is None:
error = 'Invalid ID'
elif s.grade is None:
error = 'Invalid Grade'
else:
if s.db is None:
error = 'Unknown student'
elif s.db[0].dropped is not None:
error = 'Dropped ' + format_date (s.db[0].dropped)
else:
error = None
if error is None:
table.extend (format_student_grades (cursor, s.uw_id, s.db, True, s.grade, rowcount))
rowcount += 1
else:
table.append (html.tr (
html.td (s.uw_id),
html.td (html.span (error, style="color: red;")),
html.td (),
html.td (),
html.td (),
html.td (),
html.td ('[Cancel Pending]' if s.grade == '' else s.grade),
))
result.append (html.form (
html.p ('Valid revisions to prepare: ', rowcount),
table,
render_hidden ("!submit", rowcount),
html.p (html.input (type="submit", value="Prepare Revisions!")),
method="post"
))
return "%s (%s) Prepare Grade Revisions" % (admin.admin_description, term.description ()), result
[docs]def process_revisions (cursor, term, admin, form, remote_identity):
"""Handle submission of parsed and validated grade revisions.
"""
rowcount = int (form.required_field_value ('!submit'))
result = [format_return ('Main Menu', None, None, 'Offering', dot='Grades')]
table = html.table (html.tr (html.th ('Student'), html.th ('Revised Grade')))
for i in range (rowcount):
student_person_id = form.required_field_value ("person-%d" % i)
student_grade = form.required_field_value ("grade-%d" % i)
if student_grade:
cursor.callproc_none ("grade_create_pending", term.code (), admin.admin_id, student_person_id, student_grade, remote_identity.person_id)
else:
cursor.callproc_none ("grade_cancel_pending", term.code (), admin.admin_id, student_person_id)
table.append (html.tr (
html.td (format_person (cursor, student_person_id)),
html.td (student_grade or '[Cancel Pending]'),
))
result.append (html.p ('The following grade revisions have been recorded:'))
result.append (table)
return "%s (%s) Grade Revisions Prepared " % (admin.admin_description, term.description ()), result
@use_form_param
@use_remote_identity
@return_html
def create_post_handler (cursor, term, admin, roles, form, remote_identity):
if not set (['ISC', 'INST']) & roles:
raise status.HTTPForbidden ()
message = format_unavailable_message (cursor, term)
if message is not None:
return 'Error: System Unavailable', html.p (message)
if '!submit' in form:
return process_revisions (cursor, term, admin, form, remote_identity)
else:
return parse_revision_upload (cursor, term, admin, form)
offering_grades_handler = delegate_action (grades_index_handler, {
'create': delegate_get_post (create_get_handler, create_post_handler),
})