"""Assessment candidate special case UI.
This implements the pages which allow course staff to view special cases and
select/unselect special-case candidates from an assessment.
"""
import re
from operator import attrgetter
from ll.xist.ns import html
from uw.web.html.form import render_hidden
from uw.web.html.format import make_table_format, format_return, format_datetime
from uw.web.wsgi.delegate import *
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
from .ui import format_candidate_list
from .exam_edit import exam_may_edit_seat
@delegate_get_post
@return_html
def exam_special_case_index_handler (cursor, term, admin, roles, exam):
"""Display all special candidate cases for the assessment.
:param cursor: DB connection cursor
:param term: the relevant term
:param admin: the relevant admin unit
:param roles: the active permissions roles
:param exam: the relevant assessment, if any
:return: tuple of (description, special candidate cases)
:rtype: (str, list)
This means all candidates who are not writing in the primary sitting or who
are extra (typically INC-writing) candidates. The resulting page consists
of a table with one row per candidate. If the current user is authorized
as an ISC according to the roles, then form controls to select candidates
for de-selection and a link to a form for adding special candidates are
provided.
"""
result = [format_return ('Main Menu', None, None, 'Offering', None, 'Assessment')]
candidates = cursor.execute_tuples ("select person_id, sitting_id, userid, uw_id::integer, surname, givennames, start_time, admin_description as sitting_admin_description, extra_candidate, candidate_reserved, candidate_assigned, building_code, room_code, seat_code from accom_candidate_sitting_plus natural join person_identity_complete natural left join (exam_sitting join teaching_admin on (admin_id=sitting_admin_id)) left join (select * from room_room_plus where room_current) as rrp using (room_id) natural left join exam_sitting_room_seat_extra where exam_id=%(exam_id)s and (extra_candidate or not unselected) order by start_time, sitting_admin_description, surname, givennames, uw_id", exam_id=exam.exam_id)
if 'ISC' in roles and not exam.master_accepted:
extra_columns = [('Unselect', lambda r: html.a ('Unselect…', href="edit?unselect=%s" % r.person_id) if r.extra_candidate else None)]
else:
extra_columns = []
result.append (format_candidate_list (cursor, candidates, extra_columns=extra_columns))
if 'ISC' in roles and not exam.master_accepted:
result.append (html.p (html.a ('Select Extra Candidates…', href="edit")))
result.append (html.p (html.a ('Update Candidates', href="../edit-candidate")))
return '%s (%s) %s: Special Cases' % (admin.admin_description, term.description (), exam.title), result
@return_html
def exam_special_case_edit_get_handler (cursor, term, admin, roles, exam, form):
"""Special case candidate editing form GET handler.
:param cursor: DB connection cursor
:param term: the relevant term
:param admin: the relevant admin unit
:param roles: the active permissions roles
:param exam: the relevant assessment, if any
:param form: CGI form results
:return: tuple of (description, HTML for editing form)
:rtype: (str, list)
This is either a box in which to type or paste the identities of some
extra students to be selected for the assessment, or a presentation of
an already-selected student to be verified for un-selection from the
assessment, depending on form parameters.
"""
if not 'ISC' in roles:
raise status.HTTPForbidden ()
result = []
if "unselect" in form:
result.append (html.form (
html.p ('The following candidate will be unselected from this assessment:'),
format_candidate_list (cursor, cursor.exam_candidate_details_ordered (exam_id=exam.exam_id, candidates=[int (form.required_field_value ("unselect"))])),
html.p (html.input (type="submit", name="!unselect", value="Unselect!")),
method="post", action=""
))
return '%s (%s) %s: Unselect Extra Candidates' % (admin.admin_description, term.description (), exam.title), result
else:
result.append (html.form (
html.p ('Paste or type some candidates, one per line.'),
html.p ('Each line must include the UW student ID and userid separated by either a space, comma, or tab. The userid may be all uppercase or all lowercase but not mixed case, and must be a maximum of 8 characters:'),
html.textarea (name="candidates", rows=15, cols=40),
html.p (html.input (type="submit", name="!review-add", value="Next…")),
method="post", action=""
))
return '%s (%s) %s: Select Extra Candidates' % (admin.admin_description, term.description (), exam.title), result
[docs]def process_candidate_list (cursor, candidates):
"""Scan lines of text for candidate identities.
:param cursor: DB connection cursor
:param candidates: lines of text to search for candidate identities
:return: list of candidate identities
:rtype: list
This routine will recognize one candidate per line, assuming that it can
find a UW ID and a userid which match.
"""
uw_id_re = re.compile ('(^|[^0-9])([0-9]{8})([^0-9]|$)')
re_str = '(^|[^a-z0-9-])([a-z][a-z0-9-]{1,7})([^a-z0-9-]|$)'
userid_re = re.compile (re_str + '|' + re_str.upper ())
def process_candidate (candidate):
m = re.search (uw_id_re, candidate)
if m is None:
return None
n = re.search (userid_re, candidate)
if n is None:
return None
return cursor.execute_optional_tuple ("select uw_id, person_id from person_identity_complete where (uw_id, userid) = (%(uw_id)s, %(userid)s)", uw_id=m.group (2), userid=(n.group (2) or n.group(5)).lower ())
return list(map (process_candidate, candidates))
@use_remote_identity
@return_html
def exam_special_case_edit_post_handler (cursor, remote_identity, term, admin, roles, exam, form):
"""Special case candidate editing form POST handler.
:param cursor: DB connection cursor
:param term: the relevant term
:param admin: the relevant admin unit
:param roles: the active permissions roles
:param exam: the relevant assessment, if any
:param form: CGI form results
:return: tuple of (description, HTML elements) if reviewing list or
selecting candidates OR None if unselecting candidates
:rtype: (str, list) or None
Depending on form parameters, this may do any of the following:
- Allow the user to review a list of potential candidates to be selected
for an assessment;
- Select candidates for an assessment; or
- Unselect special candidates from an assessment.
"""
if not 'ISC' in roles:
raise status.HTTPForbidden ()
if exam.master_accepted:
raise status.HTTPFound (".")
result = []
if "!review-add" in form:
given = form.optional_field_value ("candidates")
if given is None:
result.append (html.p ('No candidates specified.'))
else:
given = given.splitlines ()
people = process_candidate_list (cursor, given)
found = sum (p is not None for p in people)
if found == len (given):
result.append (html.p ('%s %s found:' % (found, 'person' if found == 1 else 'people')))
else:
result.append (html.p ('%s lines given, %s %s found:' % (len (people), found, 'person' if found == 1 else 'people')))
table = html.table (html.tr (
html.th ('Given'),
html.th ('UW ID'),
))
for g, p in zip (given, people):
if p is None:
cells = html.td (colspan=2)
else:
cells = (
html.td (
render_hidden ("c", str (p.person_id)),
format_person (cursor, p.person_id)
),
)
table.append (html.tr (html.td (g or ' '), cells))
result.append (html.form (
table,
html.p (html.input (type="submit", name="!add",
value="Select %s Candidate%s!" % (found, '' if found == 1 else 's'))),
method="post", action=""
))
return '%s (%s) %s: Select Extra Candidates' % (admin.admin_description, term.description (), exam.title), result
elif "!add" in form:
result = [format_return ('Main Menu', None, None, 'Offering', None, 'Assessment', dot='Special Cases')]
candidates = [int (c) for c in form.multiple_field_value ("c")]
request_id = cursor.callproc_required_value ("accom_create_exam_request", exam.exam_id, remote_identity.person_id, candidates)
cursor.execute_none ("update accom_request_candidate set extra_candidate = True where (term_id, accom_admin_id, request_id, exam_id) = (%(term_id)s, %(accom_admin_id)s, %(request_id)s, %(exam_id)s) and person_id = any (%(candidates)s)", term_id=exam.term_id, accom_admin_id=admin.admin_id, request_id=request_id, exam_id=exam.exam_id, candidates=candidates)
cursor.callproc_none ("accom_request_completed", exam.term_id, admin.admin_id)
result.append (html.p ('The following %s individual%s been selected for this assessment:' % (len (candidates), ' has' if len (candidates) == 1 else 's have')))
result.append (format_candidate_list (cursor, cursor.exam_special_candidate_details_ordered (exam_id=exam.exam_id, candidates=candidates)))
cursor.callproc_none ("exam_assign_designate_seats", exam.exam_id)
return '%s (%s) %s: Select Extra Candidates' % (admin.admin_description, term.description (), exam.title), result
elif "!unselect" in form:
candidates = [int (c) for c in form.multiple_field_value ("c")]
request_id = cursor.callproc_required_value ("accom_create_exam_request", exam.exam_id, remote_identity.person_id, candidates)
cursor.execute_none ("update accom_request_candidate set extra_candidate = false where (term_id, accom_admin_id, request_id, exam_id) = (%(term_id)s, %(accom_admin_id)s, %(request_id)s, %(exam_id)s) and person_id = any (%(candidates)s)", term_id=exam.term_id, accom_admin_id=admin.admin_id, request_id=request_id, exam_id=exam.exam_id, candidates=candidates)
cursor.callproc_none ("accom_request_completed", exam.term_id, admin.admin_id)
cursor.callproc_none ("exam_assign_designate_seats", exam.exam_id)
raise status.HTTPFound (".")
exam_special_case_handler = delegate_action (exam_special_case_index_handler, {
'edit': use_form_param (delegate_get_post (exam_special_case_edit_get_handler, exam_special_case_edit_post_handler)),
})