Source code for uw.local.teaching.webui.candidate

"""Candidate editing UI.

This implements the pages related to updating details of candidates for the
assessment, including allocating, reserving/unreserving, and unseating them.
"""

import re

from operator import attrgetter

from ll.xist.ns import html

from uw.web.html.form import render_select
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 .ui import format_candidate_list, nullif
from .exam_edit import exam_may_edit_seat

from ...util.identity import use_remote_identity

@return_html
def candidate_edit_get_handler (cursor, term, admin, roles, exam):
    """Candidate editor GET URL 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
    :return: a form which asks for UW IDs of assessment candidates to modify
    :rtype: HTML form <form>
    """
    if not 'ISC' in roles:
        raise status.HTTPForbidden ()

    result = [format_return ('Main Menu', None, None, 'Offering', None, dot='Assessment')]

    result.append (html.form (
        html.p ('Paste or type some UW student IDs:'),
        html.textarea (name="candidates", rows=15, cols=40),
        html.p (html.input (type="submit", name="!review", value="Next…")),
        method="post", action=""
    ))

    return '%s (%s) %s: Update Candidates' % (admin.admin_description, term.description (), exam.title), result

[docs]def process_candidate_list (candidates): """Find UW IDs in a text string. :param candidates: a string of text potentially containing UW IDs :return: list of blocks of digits that look like UW IDs :rtype: list """ if candidates is None: return None return list(map (int, re.findall ('[0-9]{7,}', candidates)))
@use_remote_identity @use_form_param @return_html def candidate_edit_post_handler (cursor, remote_identity, term, admin, roles, exam, form): """Candidate editor POST URL 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: HTML elements for assessment candidate editing form :rtype: (str, list) Handles the results of the assessment candidate editing form. This means either reviewing a list of candidates or updating the candidates. Reviewing a list of candidates allows the user to see a list of candidates submitted as UW IDs expanded to show names, and with UW IDs not found in the assessment roster reported as errors. Below the list will be a form with drop-downs to select available editing actions. Updating the candidates means adjusting the candidates' details according to the selected editing actions. """ if not 'ISC' in roles: raise status.HTTPForbidden () if exam.master_accepted: raise status.HTTPFound (".") result = [format_return ('Main Menu', None, None, 'Offering', None, dot='Assessment')] if "!review" in form: uw_ids = process_candidate_list (form.optional_field_value ("candidates")) candidates = cursor.execute_tuples ("select start_time, sitting_admin_description, extra_candidate, uw_id, person_id, candidate_reserved, candidate_assigned, building_code, room_code, seat_code, not s is null as found, CASE WHEN original_sitting_admin_description is NULL THEN sitting_admin_description ELSE original_sitting_admin_description END as original_sitting_admin_description, CASE WHEN original_start_time IS NULL THEN start_time ELSE original_start_time END as original_start_time from (select uw_id, row_number () over () from unnest (%(candidates)s::integer[]) as uw_id) t left join (select * from exam_exam_student_sitting_complete where exam_id=%(exam_id)s) s using (uw_id) left join (select exam_id, person_id, admin_description as original_sitting_admin_description, start_time as original_start_time from division_candidate_sitting natural join exam_sitting join teaching_admin on (sitting_admin_id=admin_id) where exam_id=%(exam_id)s) dcs using (person_id) order by row_number", candidates=uw_ids, exam_id=exam.exam_id) table = format_candidate_list (cursor, candidates, [('Original Sitting', lambda r: format_datetime (r.original_start_time)), ('With', attrgetter ('original_sitting_admin_description'))]) uw_ids_seen = {} seq_number = 1 ids_duplicate = 0 ids_found = 0 ids_notfound = 0 table[0][:0] = html.th ('#') for uw_id, candidate, tr in zip (uw_ids, candidates, table[1:]): if uw_id in uw_ids_seen: tr[1:] = html.td ('Duplicate of #%d above' % uw_ids_seen[uw_id], colspan=len (tr) - 1) tr[:0] = html.td () ids_duplicate += 1 elif candidate.found: uw_ids_seen[uw_id] = seq_number tr[:0] = html.td (html.b (seq_number)) seq_number += 1 ids_found += 1 else: uw_ids_seen[uw_id] = None tr[1:] = html.td ('No student with this UW ID is selected for this assessment', colspan=len (tr) - 1) tr[:0] = html.td () ids_notfound += 1 if ids_duplicate: text_duplicate = ', %d duplicate' % ids_duplicate else: text_duplicate = None if ids_notfound: text_notfound = ', %d not found' % ids_notfound else: text_notfound = None result.append (html.p ('%d candidate%s found' % (ids_found, '' if ids_found == 1 else 's'), text_notfound, text_duplicate, ':')) result.append (html.form ( table, html.p ('You may make any of the following changes to the above candidates (leave blank for no change):'), html.table ( html.col (width="40%"), html.col (width="60%"), html.tr ( html.td ('Allocation: ', render_select ("allocate", [('u', 'Unallocated')] + [(r.sitting_id, 'At %s with %s' % (format_datetime (r.start_time), r.admin_description)) for r in cursor.sittings_by_exam (exam_id=exam.exam_id)] + [('r', 'Revert to Original Sitting')])), html.td ('Choose a sitting to move the selected candidates to that sitting. “Unallocated” will remove them from their sittings without moving them to a new sitting. Select “Revert to Original Sitting” to revert all selected candidates to their original sittings according to the list above. Leave blank to leave the candidates’ sitting allocation unchanged.') ), ), html.p (html.input (type="submit", name="!update", value="Update…")), method="post", action="" )) elif "!update" in form: candidates = [int (c) for c in form.multiple_field_value ("c")] allocate = form.optional_field_value ("allocate") if allocate == 'u': cursor.execute_values ("select exam_register_unallocate_candidate (%(exam_id)s, person_id) from unnest (%(candidates)s) as person_id", exam_id=exam.exam_id, candidates=candidates) elif allocate: cursor.execute_values ("select exam_register_unreserve_candidate (%(exam_id)s, person_id) from exam_exam_student_sitting where candidate_reserved and person_id = any (%(candidates)s)", exam_id=exam.exam_id, candidates=candidates) request_id = cursor.callproc_required_value ("accom_create_exam_request", exam.exam_id, remote_identity.person_id, candidates) allocate = None if allocate == 'r' else int (allocate) cursor.execute_none ("update accom_request_candidate set sitting_id = %(allocate)s 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)", allocate=nullif (allocate), 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 candidate%s been updated:' % (len (candidates), ' has' if len (candidates) == 1 else 's have'))) result.append (format_candidate_list (cursor, cursor.exam_candidate_details_ordered (exam_id=exam.exam_id, candidates=candidates))) return '%s (%s) %s: Update Candidates' % (admin.admin_description, term.description (), exam.title), result candidate_edit_handler = delegate_get_post (candidate_edit_get_handler, candidate_edit_post_handler)