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

"""Format an assessment or list of assessments as HTML.

Contains functions for producing the HTML display of an assessment on its own,
or as it relates to a sitting.  It also has functions for generating the HTML
for a list of assessments.
"""

from operator import attrgetter
import datetime

from ll.xist.ns import html

from uw.color import rgb2html, hsv2rgb

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

from uw.local.util.format import format_person, format_aff
from uw.local.termtools import fromCode

from ..db.aids import ExamAidsEditor
from .ui import format_duration, format_seating, combine_column_fields, AttrDict, render_columns_as_rows, format_allocated_count, bool_to_yes_no
from .order_edit import format_order
from .room_render import render_room_index, room_extra_count_columns, render_allow_tablet_seats, render_allow_single_seating
from .exam_edit import exam_may_edit_cover, exam_may_edit_seat
from .exam_scan import printing_service, exam_may_edit_scan
from .exam_version import version_assignment_section
from .integration import format_integration

[docs]def format_fields (fields): result = html.table () for title, content in fields: if content is None: continue result.append ( html.tr ( html.th (title, ':'), html.td (content), ) ) return result
[docs]def format_scheduled (cursor, exam): if exam.schedule_admin_id is None: return None else: return cursor.admin_by_id (admin_id=exam.schedule_admin_id).admin_description
[docs]def format_administered (cursor, exam): if exam.administer_admin_id is None: return None else: return cursor.admin_by_id (admin_id=exam.administer_admin_id).admin_description
[docs]def format_learn_info (cursor, exam): learn_info = cursor.execute_optional_tuple ("select * from learn_admin_term natural join learn_course where (term_id, admin_id) = (%(term_id)s, %(admin_id)s)", term_id=exam.term_id, admin_id=exam.admin_id) if learn_info is None: return 'Unknown' else: return '%d (%s)' % (learn_info.learn_id, learn_info.learn_description)
[docs]def format_general_information (cursor, exam, exam_permissions, sitting): '''Format general information. :param cursor: DB connection cursor. :param exam: A database row representing an assessment. :param set exam_permissions: Set of active roles for user. :param sitting: A database row representing a sitting. ''' result = [] result.append (format_fields (( ('Primary Start Time', format_datetime (exam.primary_start_time)), ('Duration', exam.duration_formatted), ('Scheduled By', format_scheduled (cursor, exam)), ('Administered By', format_administered (cursor, exam)), ('Learn ID', format_learn_info (cursor, exam)), ))) if sitting is None: if 'ISC' in exam_permissions and exam_may_edit_cover (exam): result.append ( html.p ( html.a ('Edit Setup', href="edit-setup"), ' — Edit the primary start time, duration and integration.', ) ) return result
[docs]def format_primary_sitting (cursor, exam, sitting): if exam.primary_sitting_id is None: return None elif sitting is None or sitting.sitting_id != exam.primary_sitting_id: # Show primary sitting admin unit, linked if accessible primary_sitting = cursor.execute_required_tuple ("select admin_description, sitting_id from exam_sitting as es join (teaching_admin_term natural join teaching_admin) as tat on ((sitting_term_id, sitting_admin_id)=(term_id, admin_id)) where sitting_id=%(primary_sitting_id)s", primary_sitting_id=exam.primary_sitting_id) url_pattern = "sitting" if sitting is None else "../../.." return [html.a (primary_sitting.admin_description, href=url_pattern + "/%d/" % primary_sitting.sitting_id)] else: # Indicate this is the primary sitting for the this assessment return 'This Sitting'
[docs]def format_section_seating (cursor, exam, exam_permissions, sitting): '''Format seating information. :param cursor: DB connection cursor. :param exam: A database row representing an assessment. :param set exam_permissions: Set of active roles for user. :param sitting: A database row representing a sitting. ''' result = [] formatted_seating = format_seating (exam.exam_assigned) table = (format_fields (( ('Primary Sitting', format_primary_sitting (cursor, exam, sitting)), ('First Sequence', exam.first_sequence_text), ('Seating', (formatted_seating, '; ' if formatted_seating else '', render_allow_tablet_seats (exam.allow_tablet_seats), '; ', render_allow_single_seating (exam.allow_single_seating))), ))) if sitting is None: candidate_counts = exam columns = exam_basic_count_columns else: # sitting corresponds to exam_sitting row; we need exam_exam_sitting candidate_counts = cursor.exam_exam_sitting_by_id (sitting_id=sitting.sitting_id, exam_id=exam.exam_id) columns = exam_extra_count_columns table.append (*render_columns_as_rows (candidate_counts, columns)[:]) result.append (table) if sitting is None: if 'ISC' in exam_permissions and exam_may_edit_seat (exam): result.append ( html.p ( html.a ('Edit Seating', href="edit-seating"), ' — Edit the first sequence and seating configuration.', ) ) result.append ( html.p ( html.a ('Special Cases', href="special/"), ' — View or edit special-case candidates.', ) ) if not exam_may_edit_seat (exam): result.append (html.h3 ('Document Downloads')) links = [ (html.a ('Mark Entry List', href="markentry"), 'CSV file of sequence numbers, UW IDs, and userids suitable for loading into spreadsheet or other software.'), (['Posting list: ', html.a ('Letter', href="postinglist?lt="), ', ', html.a ('Ledger', href="postinglist")], 'Candidate seating lookup lists for use in the event a candidate does not remember their seat location (PDF).'), (html.a ('Sequence Lookup', href="sequence"), 'Candidate sequence lookup list to allow use of sequence numbers when handing back midterm assessments (PDF).'), (html.a ('Folders', href="folders"), 'Pre-labelled hanging file folder inserts (PDF).'), (html.a ('Labels', href="labels"), 'Candidate paper identification labels for printing onto 4" by 2" label stock (PDF). Please remember to choose “no automatic page scaling” when printing.'), ] result.append ( html.p ( html.a ('Administrative Documents', href="allzip"), ' — Administrative documents zipped together, also available separately from the following links:', ) ) result.append (html.ul (html.li (link, ' — ', help) for link, help in links)) result.append (html.h3 ('Seat & Sequence Assignment Order')) order = cursor.order_by_exam (exam_id=exam.exam_id, all=False) result.append (format_order (cursor, exam, order, exam.master_count)) if sitting is None: if exam_may_edit_seat (exam) and 'ISC' in exam_permissions: result.append (html.p (html.a ('Edit Assignment Order', href="edit-order/"))) result.append (html.h3 ('Rooms & Seats')) if sitting is None: from .sitting import render_exam_sitting_list result.append (render_exam_sitting_list (cursor, exam, "sitting/")) if exam.sequence_assigned is None and 'ISC' in exam_permissions and exam.primary_sitting_id is not None: result.append (html.form ( html.p ( 'Add new special sitting on ', fromCode (exam.term_id).render_time_selector ("ns", 8, 13), ' ', html.input (type="submit", name="!add-sitting", value="Add Sitting!"), ), action="add-sitting", method="post" )) else: rooms = cursor.rooms_by_exam_sitting (exam_id=exam.exam_id, sitting_id=sitting.sitting_id) result.append (render_room_index (rooms, "../../room/", room_extra_count_columns)) result.append (html.p (html.a ('Edit Rooms…', href="edit"))) return result
[docs]def format_exam_deadline (cursor, exam, business_days=3, recommended=False): """Format an assessment print deadline date :param cursor: DB connection cursor. :param exam: A database row representing an assessment. :param business_days: Number of business days previous to assessment start time required. :param deadline: Whether or noy to display the (Recommended) option for non ro exams. :return: A formatted date representing the deadline for an assessment. """ exam_deadline = format_date (cursor.exam_print_deadline (business_days=business_days, exam_id=exam.exam_id)) if recommended and exam_deadline: return exam_deadline + ' (Recommended)' return exam_deadline
[docs]def format_exam_timeline (cursor, exam): """Formats the timeline for an assessment :param cursor: DB connection cursor. :param exam: A database row representing an assessment. :return: An html result containing the timeline section outlining seat assignment and printing dates for a particular assessment. """ return [ html.p ('Please check that sittings, rooms, and exam details are all up to date and accurate. Please ensure that masters are uploaded and approved to leave time for exam processing.'), html.p (html.b ('Timeline:'), ' Seat finalization and printing will automatically occur on ', html.b (format_exam_deadline (cursor, exam)), '.'), None if exam.master_approved else html.p (html.b ('Warning:'), ' Printing cannot happen until masters are approved.'), html.p ('Seat finalization requires that all sittings have adequate capacity. Email notifications will be sent out if seat finalization cannot be completed. Printing and delivery is typically done one business day before the exam, but depends on the availability of %s.' % (printing_service)), ]
[docs]def format_permitted_aids (cursor, exam): if exam_may_edit_cover (exam): aids_link = html.a ('Edit Permitted Aids', href="edit-aids") else: aids_link = None return [ExamAidsEditor (cursor, exam).render_aids (), aids_link]
[docs]def format_masters (cursor, exam, exam_permissions, sitting): if exam.master_approved is None: return None else: result = [ format_datetime (exam.master_approved), ' by ', format_person (cursor, exam.master_approver_person_id), ] if sitting is None and 'ISC' in exam_permissions: result.append (' (') result.append (html_join ((html.a (em.master_description, href="download?type_code=%s&version_seq=%s" % (em.type_code, em.version_seq)) for em in cursor.exam_masters_by_exam (exam_id=exam.exam_id) if em.master_pages_net is not None), ', ')) result.append (')') result.append (html.br ()) if exam.master_accepted is None: result.append ('Masters not yet sent for printing.') else: result.append ('Masters sent for printing %s.' % format_datetime (exam.master_accepted)) return result
[docs]def format_printing (cursor, exam, exam_permissions, sitting, user_is_author): '''Format printing information. :param cursor: DB connection cursor. :param exam: A database row representing an assessment. :param set exam_permissions: Set of active roles for user. :param sitting: A database row representing a sitting. :param bool user_is_author: Whether the user is the current authorized uploader. ''' result = [] upload_masters_link = html.span() if sitting is None: if user_is_author: upload_masters_link.append(html.a ('(Upload Masters)', href="../../../../../upload/%s/" % exam.exam_id)) elif ('ISC' in exam_permissions or 'INST' in exam_permissions): upload_masters_link.append(html.a ('(Upload Masters)', href="override-uploader")) print_account = cursor.callproc_required_value ("exam_print_account", exam.exam_id) if not exam.administer_admin_id else None result.append (format_fields (( ('Submission Deadline', format_exam_deadline (cursor, exam, business_days=4, recommended=exam.administer_admin_id is None)), ('Authorized Uploader', format_person (cursor, exam.exam_author_person_id)), ('Permitted Aids & Instructions', format_permitted_aids (cursor, exam)), ('Master Versions', (exam.master_count, ' ', upload_masters_link)), ('Masters Approved', format_masters (cursor, exam, exam_permissions, sitting)), ('Print Billing', format_aff (print_account)), ))) if sitting is None: if 'ISC' in exam_permissions and exam_may_edit_cover (exam): result.append ( html.p ( html.a ( 'Edit Printing', href="edit-printing"), ' — Edit the authorized uploader and master versions', ) ) if exam.master_count > 1: result.append (version_assignment_section (cursor, exam, edit=False)) return result
[docs]def format_scanning (cursor, exam, exam_permissions, sitting): '''Format scanning information. :param cursor: DB connection cursor. :param exam: A database row representing an assessment. :param set exam_permissions: Set of active roles for user. :param sitting: A database row representing a sitting. ''' result = [] exam_scan = cursor.execute_optional_tuple ("select * from exam_exam_scan natural left join exam_scan_type where exam_id = %(exam_id)s", exam_id=exam.exam_id) if exam_scan: if exam_scan.scan_type == 'wprint': exam_scan_date = ('Drop Off Date', format_datetime (exam_scan.scan_pickup_when)) exam_scan_location = ('Drop Off Location', exam_scan.scan_type_description) elif exam_scan.scan_type == 'pickup': exam_scan_date = ('Courier Pick Up', format_datetime (exam_scan.scan_pickup_when)) exam_scan_location = ('Courier Pick Up Location', exam_scan.scan_pickup_where) else: exam_scan_date = ('Drop Off Date', None) exam_scan_location = ('Drop Off Location', exam_scan.scan_type_description) if exam.scanning_integration == 'N': result.append (html.p ('This assessment is not configured for Crowdmark or Markbox.')) result.append (format_fields (( ('Integration', format_integration (exam)), ('Marking Start Date', format_datetime (exam_scan.exam_marking_start)), ('Request Sample', bool_to_yes_no (exam_scan.exam_scan_request_sample)), exam_scan_date, exam_scan_location, ('Return Date', format_datetime (exam_scan.scan_return_when)), ('Return Location', exam_scan.scan_return_where), ))) else: result.append (html.p ('This assessment is not configured for scanning.')) if 'ISC' in exam_permissions and exam_may_edit_scan (exam): result.append ( html.p ( html.a ( 'Edit Scanning', href="edit-scanning"), ' — Edit exam drop-off/pick-up information for %s.' % printing_service, ) ) return result
[docs]def render_exam (cursor, remote_identity, exam, exam_permissions, sitting=None): '''Render the specified assessment as HTML. :param cursor: DB connection cursor. :param remote_identity: User remote identity. :param exam: A database row representing an assessment. :param set exam_permissions: Set of active roles for user. :param sitting: A database row representing a sitting. :return: An HTML page with information about the assessment. The resulting page will be formatted appropriately based on the user's permissions. Includes basic information about the assessment as well as sequence and seat assignment. If the sitting is specified, only include information relevant to the given sitting. If sitting is not specified, include information about sittings and rooms being used for the assessment. ''' result = [ html.h2 ('General Information'), format_general_information (cursor, exam, exam_permissions, sitting), format_tabs ([ ('Seating', 'seating', format_section_seating (cursor, exam, exam_permissions, sitting)), ('Printing', 'printing', format_printing (cursor, exam, exam_permissions, sitting, remote_identity.person_id == exam.exam_author_person_id)), ('Scanning', 'scanning', format_scanning (cursor, exam, exam_permissions, sitting)) ]) ] return result
exam_basic_count_columns = ( (attrgetter ('count_selected_candidates'), 'Selected', ['count_selected_candidates']), (attrgetter ('count_unallocated_candidates'), 'Unallocated', ['count_unallocated_candidates']), (attrgetter ('count_reserved_a_candidates'), 'Reserved', ['count_reserved_a_candidates']), ) exam_extra_count_columns = ( (lambda r: format_allocated_count (r.count_allocated_candidates, r.count_reserved_d_candidates, r.count_undesignated_candidates), 'Allocated', ['count_allocated_candidates', 'count_reserved_d_candidates', 'count_undesignated_candidates']), (attrgetter ('count_rush_candidates'), 'Not Assigned', ['count_rush_candidates']), (attrgetter ('count_assigned_candidates'), 'Assigned', ['count_assigned_candidates']), (attrgetter ('count_unseated_candidates'), 'Unseated', ['count_unseated_candidates']), ) exam_all_count_columns = exam_basic_count_columns + exam_extra_count_columns
[docs]def render_exam_index (cursor, exams, url_prefix, columns, sitting=None): """Render the given list of assessments as HTML. Parameters: cursor -- DB connection cursor; exams -- the relevant assessments, as DB result rows; url_prefix -- the URL prefix for links to the assessments; columns -- some column specifications represented as 3-tuples; sitting -- the relevant sitting, if any, as a DB result row. """ fields = combine_column_fields (columns) accum = AttrDict ((field, 0) for field in fields) accum['duration'] = datetime.timedelta () 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 if r.exam_duration is not None: accum['duration'] = max (accum['duration'], r.exam_duration) if sitting is None: return (make_table_format ( ('Title', lambda r: html.a (r.title, href=url_prefix + "%d/" % r.exam_id)), ('Start Time', lambda r: format_datetime (r.primary_start_time)), ('Duration', lambda r: format_duration (r.exam_duration)), *[(title, valuegetter) for valuegetter, title, _ in columns] ) (exams)) else: hues = cursor.sitting_exam_hues (sitting.sitting_id) return (make_table_format ( (None, lambda r: None, lambda r: {'style': "background-color: %s;" % rgb2html (hsv2rgb ((hues[r.exam_id], 0.7, 1)))}), ('Assessment', lambda r: html.a (r.full_title, href=url_prefix + "%d/" % r.exam_id)), ('Duration', lambda r: format_duration (r.exam_duration)), after_row=update, footer_row=lambda: html.tr ( html.th ('    '), html.th ('Combined:'), html.td (format_duration (accum['duration'])), [html.td (html.b (valuegetter (accum))) for valuegetter, _, _ in columns] ), row_attributor=lambda r: {'style': "background-color: %s;" % rgb2html (hsv2rgb ((hues[r.exam_id], 0.4, 1)))}, *[(title, valuegetter) for valuegetter, title, _ in columns] ) (exams))