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

"""Display assessment schedule for a student.

Implements the page for students to look up their own assessment schedule, as
well as the page allowing staff to see any student's schedule.
"""

from operator import attrgetter

from ll.xist.ns import html, specials

from uw.web.wsgi.authority import check_authority
from uw.web.wsgi.delegate import *
from uw.web.wsgi.form import use_form_param
from uw.web.wsgi.function import return_html, return_json
from uw.web.wsgi.status import HTTPFound, HTTPNotFound, HTTPForbidden

from uw.web.html.format import make_table_format, format_datetime, format_time

from .authority import check_global_authority
from ...util.identity import use_remote_identity
from .ui import format_duration

portal_url = "https://portal.uwaterloo.ca/Index#/Home/exams"

[docs]def format_pac_instructions (exam): """Format instructions for a candidate to reach their seat in the PAC. :param exam: row containing at least room_code, seat_col, seat_row, seat_code :return: HTML <ol> of PAC instructions :rtype: HTML list <ol> Returns step-by-step instructions for finding the seat in the specified assessment information. The steps involve (1) going to the PAC; (2) finding the correct corner of the building (e.g., Blue North); (3) finding the correct floor; (4) entering and finding the specific seat. """ if exam.room_code == 'GYM' and exam.seat_row <= 12: compass = 'SOUTH' else: compass = 'WEST' if exam.room_code == 'GYM': stairs = 'down all the way to level 1' else: stairs = 'up all the way to level 3' return html.ol ( html.li ('Go to the PAC (Physical Activities Complex)'), html.li ('Enter at the %s entrance' % compass), html.li ('Go %s' % stairs), html.li ('When allowed by proctors, enter and find seat %s %s' % (exam.room_code, exam.seat_code)), )
[docs]def special_sitting (exam): """Determine whether special formatting is required for a candidate. :param exam: database row of candidate assessment information :return: whether building code is PAC as that is the only building with special formatting of the assessment instructions :rtype: bool """ return exam.building_code == 'PAC'
[docs]def format_duration (exam): """Format the duration for the given candidate assessment information. :param exam: database row of candidate assessment information :return: the duration formatted for display :rtype: str **TODO: If the candidate has duration accommodation, the usual duration should also be displayed.** """ return exam.duration_text
[docs]def format_times (exam): """Format the times for the given candidate assessment information. :param exam: database row of candidate assessment information :return: the times formatted for display. If the start time is unknown, this will be None. Otherwise, if the duration is unknown, just the start time will be shown, else the range of times :rtype: str """ if exam.start_time is None: return None result = format_datetime (exam.start_time) if exam.nonsynchronous: result += ' (see instructor)' elif exam.duration is not None: result += '\N{EN DASH}' + format_time ((exam.start_time + exam.duration).time ()) return result
[docs]def format_room (exam): """Format the room column information for an assessment candidate. :param exam: database row of candidate assessment information :return: a regular table cell unless special formatting is required, in which it will span two columns, taking up the seat column as well :rtype: HTML table cell <td> """ if special_sitting (exam): return html.td (format_pac_instructions (exam), colspan=2) else: return html.td (exam.building_code, ' ', exam.room_code)
[docs]def format_seat (exam): """Format the seat column information for an assessment candidate. :param exam: database row of candidate assessment information :return: a regular table cell unless special formatting is required, in which it returns None to work properly with the two-column cell returned by format_room() :rtype: HTML table cell <td> """ if special_sitting (exam): return None else: return html.td (exam.seat_code)
[docs]def include_sequence (exam): """Determine whether sequence number information should be included. :param exam: candidate information for a single assessment :return: whether the sequence number should be listed. In order to be shown the sequence number has to be assigned and the start time has to be in the past :rtype: bool """ return exam.sequence_text is not None and exam.start_time is not None and exam.start_time < datetime.datetime.now ()
[docs]def format_exam_schedule (exams): """Format a person's assessment schedule information. :param exam: candidate information for a single assessment :return: HTML table with one row per provided assessment :rtype: list """ result = [ ] room_note = False columns = [html.th ('Exam'), html.th ('Duration'), html.th ('When'), html.th ('Room'), html.th ('Seat'), html.th ('Sequence'), ] for exam in exams: if exam.room_note: room_note = True columns.append (html.th ('Room Note')) break table = html.table (html.tr (columns),) for exam in exams: row = html.tr ( html.td (exam.exam_full_title), html.td (format_duration (exam)), html.td (format_times (exam)), ) if exam.candidate_assigned: row.append (format_room (exam)) row.append (format_seat (exam)) else: row.append (html.td (exam.rooms, colspan=2)) row.append ( html.td (exam.sequence_text if include_sequence (exam) else None), ) if room_note: row.append (html.td (None if exam.room_note is None else specials.literal (exam.room_note))) table.append (row) result.append (table) return result
@delegate_get_post @use_remote_identity @return_html def own_schedule_handler (cursor, remote_identity): """Student assessment schedule URL handler. :param cursor: DB connection cursor :return: tuple of (title, HTML for currently-scheduled assessments for the relevant person) :rtype: (str, list) """ result = [] exams = cursor.exams_by_candidate (person_id=remote_identity.person_id) if exams: result.append (html.p ('The following assessments are currently scheduled in this system for you:')) result.append (format_exam_schedule (exams)) else: result.append (html.p ('No assessments are currently scheduled in this system for you.')) return 'Assessment Schedule (%s)' % remote_identity.userid, result @delegate_get_post @use_remote_identity @use_form_param @return_html def candidate_choice_handler (cursor, form, remote_identity, global_roles): """Candidate choice URL handler. :param cursor: DB connection cursor :param form: CGI form results :return: tuple of (title, HTML form to choose (enter in id of) a candidate) :rtype: (str, list) """ if 'candidate' in form: candidate_input = form.required_field_value ("candidate") candidate_query = "select userid from person_identity_complete where %s = %%(candidate)s" % ("uw_id" if candidate_input.isdigit () else "userid") print (candidate_query) candidate = cursor.execute_optional_value (candidate_query, candidate=candidate_input) if candidate: raise HTTPFound (candidate) else: return 'Error: Student not found', [html.p ('Error student %s could not be found. Please go back and try again.' % candidate_input)] result = [] result.append ([html.p ('You have permission to lookup student schedules. Please fill in either the UWID or userid of the desired student.'), html.form (html.p ('Schedule Lookup: ', html.input (maxlength=8, type="text", name="candidate", size=9)), action="" , method="get" )]) return 'Student Assessment Schedule Lookup', result @delegate_get_post @return_html def candidate_schedule (cursor, candidate, global_roles): """Staff student assessment schedule URL handler. :param cursor: DB connection cursor :param candidate: candidate for assessment :return: tuple of (title and userid, currently-scheduled assessments for the in-context userid) :rtype: (str, list) The URL corresponding to this handler must be controlled by web server configuration. This allows authorized staff members to append a userid to the candidate schedule URL in order to see the schedule of the person with that userid. """ result = [] candidate_identity = cursor.execute_optional_tuple ("select * from person_identity_complete where userid=%(userid)s", userid=candidate) if candidate_identity is None: raise HTTPNotFound () exams = cursor.exams_by_candidate (person_id=candidate_identity.person_id) if exams: result.append (html.p ('The following assessments are currently scheduled in this system for %s:' % candidate_identity.userid)) result.append (format_exam_schedule (exams)) else: result.append (html.p ('No assessments are currently scheduled in this system for %s.' % candidate_identity.userid)) return 'Assessment Schedule (%s)' % candidate_identity.userid, result @delegate_get_post @use_remote_identity @return_json def candidate_schedule_json (cursor, candidate, remote_identity): """Portal student assessment schedule URL handler. :param cursor: DB connection cursor :param candidate: candidate for assessment :return: currently-scheduled assessments for the in-context userid as a JSON array :rtype: list """ if remote_identity.userid != '_instruct_api_schedule': raise HTTPForbidden () result = [] candidate_identity = cursor.execute_optional_tuple ("select * from person_identity_complete where userid=%(userid)s", userid=candidate) if candidate_identity is None: raise HTTPNotFound () exams = cursor.exams_by_candidate (person_id=candidate_identity.person_id) result = [] for exam in exams: d = {} d['exam_full_title'] = exam.exam_full_title if exam.start_time is not None: d['start_time'] = exam.start_time.isoformat () if exam.duration is not None: d['duration_text'] = exam.duration_text d['duration_minutes'] = exam.duration.seconds // 60 if exam.candidate_assigned: if special_sitting (exam): d['seating_instructions'] = format_pac_instructions (exam).string () else: d['building_code'] = exam.building_code d['room_code'] = exam.room_code d['seat_code'] = exam.seat_code if exam.room_note is not None: d['room_note'] = exam.room_note else: d['rooms'] = exam.rooms if include_sequence (exam): d['sequence_text'] = exam.sequence_text result.append (d) return result schedule_handler = delegate_value ('candidate', check_authority (check_global_authority (['SCHED']), candidate_choice_handler), always (check_authority (check_global_authority (['SCHED']), candidate_schedule)) , own_schedule_handler)