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

"""Display individual sittings and lists of sittings.

This implements the pages for viewing a sitting on its own, or as it relates to
an assessment.  It also generates the HTML for the list of sittings related to
an assessment.
"""

from operator import attrgetter
from datetime import datetime

from ll.xist.ns import html

from uw.web.wsgi import status
from uw.web.wsgi.delegate import delegate_get_post
from uw.web.wsgi.form import use_form_param
from uw.web.wsgi.function import return_html, return_pdf
from uw.web.pdf.util import handle_pdf_result

from uw.web.html.format import make_table_format, format_email, format_return, format_datetime, format_tabs
from uw.web.html.join import html_join

from .ui import format_duration, format_seating, format_independent, combine_column_fields, AttrDict, render_columns_as_rows

from .exam_render import render_exam_index, exam_extra_count_columns
from .room_render import render_room_index, room_extra_count_columns, room_all_count_columns

@delegate_get_post
@return_html
def user_sitting_list (cursor, **params):
    """User sitting list display handler.

    :param cursor: DB connection cursor
    :param params: any additional context not needed
    :return: tuple of (title, list of sittings relevant to the current 
        user with links to each)
    :rtype: (string, list)

    *** TODO: Links do not work; list is too comprehensive.  Possibly entire
        section should be removed as it is not used.
    """
    result = [format_return ('Main Menu')]

    result.append (make_table_format (
        ('Sitting', lambda r: html.a (r.sitting_id, href="%s/" % r.sitting_id)),
        ('Start Time', lambda r: format_datetime (r.start_time)),
        ('Sitting Duration', lambda r: format_duration (r.sitting_duration)),
        ('Admin Unit', attrgetter ('admin_description')),
    ) (cursor.execute_tuples ("select es.*, ta.admin_description from exam_sitting as es left join teaching_admin as ta on (es.sitting_admin_id=ta.admin_id) order by start_time, admin_description, sitting_id")
    ))

    return "Assessment Sittings", result

[docs]def render_sitting_exam_list (cursor, sitting, url_prefix): """Render a list of the assessments for a sitting as HTML. :param cursor: DB connection cursor :param sitting: the sitting whose assessments should be listed :param url_prefix: the URL prefix for links to the assessments :param count: whether to return the total number of exams as well :return: an HTML-formatted list of exams :rtype: HTML table <table> """ exams = cursor.exams_by_sitting (sitting_id=sitting.sitting_id) result = render_exam_index (cursor, exams, url_prefix, exam_extra_count_columns + room_extra_count_columns, sitting) header_row = result[0] result.insert (0, html.tr ( header_row[0], header_row[1], header_row[2], html.th ('Candidates', colspan=len (exam_extra_count_columns)), html.th ('Seats', colspan=len (room_extra_count_columns) - 1), header_row[-1], )) for i in [-1, 2, 1, 0]: # Process columns left-to-right so that earlier header_row # column deletions don't interfere with later ones result[0][i]['rowspan'] = 2 del header_row[i] return result
[docs]def render_sitting (cursor, sitting, exam=None): """Render the specified sitting as HTML. :param cursor: DB connection cursor :param sitting: the sitting whose assessments should be listed :param exam: the relevant assessment, if any :return: list of formatted HTML tabs for this sitting :rtype: list If the assessment is specified, only include information relevant to the given assessment. """ result = [] # General information result.append (html.table ( html.tr ( html.th ('Start time:'), html.td (format_datetime (sitting.start_time)), ), html.tr ( html.th ('Sitting Duration:'), html.td (format_duration (sitting.sitting_duration)), ), html.tr ( html.th ('Seating:'), html.td (format_seating (sitting.sitting_assigned)), ), html.tr ( html.th ('Exam Independent:'), html.td (format_independent (sitting.exam_independent)), ), *(render_columns_as_rows (sitting, exam_extra_count_columns)[:] if exam is not None else []) )) if exam is None: result.append (html.p (html.a ('Edit…', href="edit"))) if sitting.print_proctor_package: result.append (html.p ( html.a ('Proctor Package', href="proctor") )) # Rooms if exam is None: rooms = cursor.rooms_by_sitting (sitting_id=sitting.sitting_id) columns = room_all_count_columns addroom_flag = sitting.start_time > datetime.now () else: rooms = cursor.rooms_by_exam_sitting (sitting_id=sitting.sitting_id, exam_id=exam.exam_id) columns = room_extra_count_columns addroom_flag = exam.sequence_assigned is None if addroom_flag: addroom = { 'addroom_cursor': cursor, 'addroom_url': "" } else: addroom = {} rooms_content = render_room_index (rooms, "room/", columns, **addroom) if exam is not None: if exam.sequence_assigned is None: rooms_content.append (html.p (html.a ('Edit Spare Counts and Cram Limits…', href="edit-spares"))) # TODO: Possibly the sitting object should already be the result of # this call, instead of the result of Cursor.exam_sitting_by_id as # currently obtained by delegate.sitting_from_arc (to be changed?). exam_sitting = cursor.exam_exam_sitting_by_id (exam_id=exam.exam_id, sitting_id=sitting.sitting_id) if exam_sitting.count_used_rush_seats < exam_sitting.count_rush_candidates and exam_sitting.count_rush_candidates <= exam_sitting.count_rush_seats or exam_sitting.count_occupied_seats < exam_sitting.count_assigned_candidates and exam_sitting.count_assigned_candidates <= exam_sitting.count_assigned_seats: rooms_content.append (html.form ( html.p ( 'Normally seat assignments automatically take place the Friday before the week (Monday-Sunday) of the midterm assessment. If you wish to assign seats earlier, or if you are setting up the midterm late and have missed the automatic assignment run, you may click here to trigger seat assignment manually: ', html.input (type="submit", name="!assign", value="Assign Seats!"), ), html.p ( 'Caution: this must be done separately for every sitting, and may lead to anomalies resulting from later changes. For example, if a candidate is moved to another sitting after seat assignment, their seat will be empty. Other candidates will not be automatically re-arranged to fill in the hole.' ), method="post", action="" )) tabs_lst = [("Rooms", "rooms", rooms_content if len (rooms) > 0 else "No rooms exist for this sitting.")] # Assessments if exam is None: tabs_lst.append (("Assessments", "exams", render_sitting_exam_list (cursor, sitting, "exam/"))) return format_tabs (tabs_lst)
@delegate_get_post @return_html def sitting_handler (cursor, term, admin, roles, sitting): """Display overall information about the sitting. :param cursor: DB connection cursor :param term: the relevant term :param admin: the relevant admin unit :param roles: the active permissions roles :param sitting: the relevant sitting :return: tuple of (sitting description, sitting information) :rtype: (str, list) """ result = [format_return ('Main Menu', None, None, 'Offering', None)] result.append (render_sitting (cursor, sitting)) return sitting.full_description, result @return_html def exam_sitting_get_handler (cursor, term, admin, roles, exam, sitting): """Assessment sitting 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 sitting: the relevant sitting :return: tuple of (exam title and sitting description, sitting information) :rtype: (str, list) """ result = [format_return ('Main Menu', None, None, 'Offering', None, 'Assessment', None)] result.append (render_sitting (cursor, sitting, exam)) return '%s: %s' % (exam.full_title, sitting.full_description), result @use_form_param @return_html def exam_sitting_post_handler (cursor, term, admin, roles, exam, sitting, form): """Assessment sitting 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 sitting: the relevant sitting :param form: CGI form results Handles the button to assign seats immediately. *** TODO: Also invokes exam_assign_spare_count but no way (or need?) to do this from the UI. """ if 'ISC' in roles and '!spare' in form: # *** TODO: Fails - division by 0 - because it counts *occupied* seats cursor.callproc_none ("exam_assign_spare_count", exam.exam_id, sitting.sitting_id) elif 'ISC' in roles and '!assign' in form: cursor.callproc_none ("exam_assign_seats", exam.exam_id, sitting.sitting_id) raise status.HTTPFound ("") exam_sitting_handler = delegate_get_post (exam_sitting_get_handler, exam_sitting_post_handler)
[docs]def render_sitting_index (sittings, url_prefix, columns=[]): """Render the given list of sittings as HTML. :param sittings: iterable of sittings :param url_prefix: the URL prefix for links to the sittings :param columns: additional table columns to include :return: formatted HTML table for this sitting :rtype: HTML table <table> """ return (make_table_format ( ('Start Time', lambda r: html.a (format_datetime (r.start_time), href=url_prefix + "%d/" % r.sitting_id)), ('Duration', lambda r: format_duration (r.sitting_duration)), *[(title, valuegetter) for valuegetter, title, _ in columns] ) (sittings))
[docs]def render_admin_sitting_list (cursor, term, admin, url_prefix, count=False): """Render a list of sittings for the specified offering. :param cursor: DB connection cursor :param term: the relevant term :param admin: the relevant admin unit :param url_prefix: the URL prefix for links to the sittings :param count: whether to return the total number of sittings as well :return: tuple of (rendered list, number of sittings) :rtype: (HTML table <table>, int) Finds the sittings belonging to the offering and renders a list of them, with links to the individual sittings. If count is True, then returns a tuple of (rendered list, number of sittings). Otherwise, returns the rendered list. """ sittings = cursor.sittings_by_offering (term_id=term.code (), admin_id=admin.admin_id) if sittings: room_columns = room_all_count_columns[1:] result = render_sitting_index (sittings, url_prefix, exam_extra_count_columns + room_columns) header_row = result[0] result.insert (0, html.tr ( *header_row[0:2], html.th ('Candidates', colspan=len (exam_extra_count_columns)), html.th ('Seats', colspan=len (room_columns) - 1), header_row[-1], )) for i in [-1, 1, 0]: # Process columns left-to-right so that earlier header_row # column deletions don't interfere with later ones result[0][i]['rowspan'] = 2 del header_row[i] return (result, len (sittings)) if count else result else: return (None, 0) if count else None
@delegate_get_post @return_html def sitting_index_handler (cursor, term, admin, roles): """Display list of sittings in the offering. :param cursor: DB connection cursor :param term: the relevant term :param admin: the relevant admin unit :param roles: the active permissions roles :return: tuple of (sitting description, sittings) :rtype: (str, list) """ result = [format_return ('Main Menu', None, None, 'Offering')] result.append (render_admin_sitting_list (cursor, term, admin, "")) return '%s %s Sittings' % (admin.admin_description, term.description ()), result
[docs]def render_exam_sitting_list (cursor, exam, url_prefix): """Render list of sittings for an assessment. :param cursor: DB connection cursor :param exam: the relevant assessment :param url_prefix: the URL prefix for links to the sittings :return: HTML table of sittings :rtype: HTML table <table> """ sittings = cursor.sittings_by_exam (exam_id=exam.exam_id) columns = exam_extra_count_columns fields = combine_column_fields (columns) accum = AttrDict ((field, 0) for field in fields) def update (r): for field in fields: value = getattr (r, field) if value is None or accum[field] is None: accum[field] = None else: accum[field] += value def render_sitting (sitting): if sitting.exam_independent and exam.sequence_assigned is not None: proctor_package = html.a ('Proctor Package', href=url_prefix + "%d/proctor" % sitting.sitting_id) else: proctor_package = None return html_join ( [sitting.admin_description, proctor_package], sep=html.br ()) sitting_table = (make_table_format ( ('With', render_sitting), ('Start Time', lambda r: html.a (format_datetime (r.start_time), href=url_prefix + "%d/" % r.sitting_id)), after_row=update, footer_row=lambda: html.tr ( html.th ('Total:', colspan=2), [html.th (valuegetter (accum)) for valuegetter, _, _ in columns] ), *[(title, valuegetter) for valuegetter, title, _ in columns] ) (sittings)) header_row = sitting_table[0] for i in range (2): header_row[i]['rowspan'] = 2 columns = room_extra_count_columns fields = combine_column_fields (columns) addroom_flag = exam.sequence_assigned is None grand_accum = AttrDict ((field, 0) for field in fields) table = html.table ( html.tr ( *header_row[:2], html.th ('Candidates', colspan=len (header_row) - 2), html.th ('Seats', colspan=len (room_extra_count_columns) + 1 - 1), *[html.th (title, rowspan=2) for _, title, _ in room_extra_count_columns[-1:]] ), html.tr ( *header_row[2:], html.th ('Room'), *[html.th (title) for _, title, _ in room_extra_count_columns[:-1]] ), ) for sitting, table_row in zip (sittings, sitting_table[1:-1]): rooms = cursor.rooms_by_exam_sitting (exam_id=exam.exam_id, sitting_id=sitting.sitting_id) if addroom_flag: addroom = { 'addroom_cursor': cursor, 'addroom_url': "sitting/%d/" % sitting.sitting_id, } else: addroom = {} room_table = render_room_index (rooms, url_prefix + "%s/room/" % sitting.sitting_id, columns, grand_accum=grand_accum, **addroom)[1:] for cell in table_row: cell.attrs.rowspan = len (room_table) table.append (html.tr ( *table_row[:], *room_table[0][:] )) table.append (*room_table[1:]) table.append (html.tr ( *sitting_table[-1][:], html.th (), *[html.th (html.b (valuegetter (grand_accum))) for valuegetter, _, _ in columns] )) return table
@delegate_get_post @return_html def exam_sitting_index_handler (cursor, term, admin, roles, exam): """Display list of sittings 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 :return: tuple of (title, HTML table of listings) :rtype: (str, HTML table <table>) """ result = [format_return ('Main Menu', None, None, 'Offering', None, 'Assessment')] result.append (render_exam_sitting_list (cursor, exam, "")) return '%s Sittings' % exam.full_title, result
[docs]def write_sitting_proctor_pdf (term, sitting, exam=None): """Generate a proctor package. :param term: the relevant term :param sitting: the relevant sitting :param exam: the relevant assessment, if any :return: proctor package pdf Generates a proctor package for the relevant sitting (and exam if given). This invokes a Java class to generate the actual PDF. """ params = ['java', 'ca.uwaterloo.odyssey.exams.printExamSittingProctorPackages', str (sitting.sitting_id)] if exam is None or not sitting.exam_independent: title = '%s Proctor Package.pdf' % sitting.full_description else: title = '%s %s %s: %s Proctor Package.pdf' % (exam.admin_description, term.description (), exam.title, sitting.full_description) params.append (str (exam.exam_id)) return handle_pdf_result ( params, title, ('There was a problem attempting to generate the proctor package. Please contact ', format_email ('odyssey@uwaterloo.ca'), ' for more assistance.') )
@delegate_get_post @return_pdf def sitting_proctor_handler (cursor, term, admin, roles, sitting, exam=None): """Proctor package 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 sitting: the relevant sitting :param exam: the relevant assessment, if any :return: proctor package pdf Generates a proctor package for the relevant sitting (and exam if in context). """ return write_sitting_proctor_pdf (term, sitting, exam)