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

"""Assessment editor pages.

This implements the pages for editing examination details.
"""

from datetime import date, time, datetime

from ll.xist.ns import html

from uw.local.teaching.webui.ui import render_tooltip

from uw.web.html.form import render_select, render_radio, render_checkbox, parse_date_2, parse_time_2, parse_datetime
from uw.web.html.format import format_return, format_datetime

from uw.web.wsgi import status
from uw.web.wsgi.delegate import delegate_get_post
from uw.web.wsgi.form import use_form_param, parse_form_value
from uw.web.wsgi.function import return_html

from .ui import nullif
from .exam_schedule import bool_choices, bool_write, bool_read, integration_choices

[docs]def write_setup_controls (cursor, term, admin, exam=None): '''Write form controls for editing the basic setup of an examination. :param cursor: DB connection cursor. :param term: In-context academic term. :param admin: In-context admin unit. :param exam: A database row representing an examination. :return: List of control titles and form controls. :rtype: list Returns controls for editing the start time and duration of the examination. ''' result = [] if exam is None or exam_may_edit_cover (exam): if exam is None or exam.primary_start_time is None: # Allow picking start time start_time_select = term.render_time_selector ("start", 8, 13) else: # Allow choosing primary sitting sittings = cursor.execute_tuples ("select sitting_id, start_time from exam_exam_sitting as ees join exam_sitting as es using (sitting_id) where (sitting_admin_id, exam_id) = (%(admin_id)s, %(exam_id)s) order by start_time", admin_id=admin.admin_id, exam_id=exam.exam_id) primary_sitting_id = exam.primary_sitting_id start_time_select = render_select ("primary", [(r.sitting_id, format_datetime (r.start_time)) for r in sittings], primary_sitting_id, True) result.append (('Primary Start Time', start_time_select)) valid_admin = False if exam is None else cursor.execute_required_value("select exists (select from auth_offering_personnel_complete where (term_id, admin_id, person_id) = (%(term_id)s, %(admin_id)s, %(person_id)s) and role_current='t')", term_id=exam.term_id, admin_id=exam.schedule_admin_id, person_id=exam.exam_author_person_id) default_duration = None if exam is None else exam.exam_duration # Requestor has privileges related to schedule_admin_id if exam is not None and exam.schedule_admin_id is not None and valid_admin is True: duration_choices = cursor.execute_tuples ("select extract (epoch from duration)::integer as duration_seconds, exam_time_format_duration (duration) as formatted from teaching_admin_scheduled_duration where admin_id=%(admin_id)s", admin_id=exam.schedule_admin_id) # Self scheduled exam else: if default_duration is not None: default_duration = str (default_duration.seconds) duration_choices = cursor.execute_tuples ("select extract (epoch from duration)::text as duration_seconds, exam_time_format_duration (duration) as formatted from (select d * interval '10 minutes' as duration from generate_series (2, 12) as t (d) union select d * interval '30 minutes' as duration from generate_series (5, 10) as s (d) union select %(duration)s) t where duration is not null order by duration", duration=default_duration) total_seconds = exam and exam.exam_duration and int (exam.exam_duration.total_seconds ()) result.append (('Duration', [ render_select ("duration", duration_choices + [('other', 'Other...')], default_duration, class_="uw-ofs"), html.div (['\t', 'Hours: ', render_select ('%sh' % "duration-other", [(x, x) for x in range (0, 7)], None if not exam or not exam.exam_duration else total_seconds//3600), '\t', 'Minutes: ', render_select ('%sm' % "duration-other", [(x, x) for x in ('%02d' % x for x in range (0, 60, 5))], None if not exam or not exam.exam_duration else '%02d' % ((total_seconds - ((total_seconds)//3600)*3600)//60))], class_="uw-ofs-duration-other") ])) return result
[docs]def write_seating_controls (cursor, term, admin, exam=None): '''Write form controls for editing the seating options of an examination. :param cursor: DB connection cursor. :param term: In-context academic term. :param admin: In-context admin unit. :param exam: A database row representing an examination. :return: List of control titles and form controls. :rtype: list Returns controls for editing the seating options of the examination. ''' result = [] if exam is not None and exam_may_edit_seat (exam): sequence_choices = cursor.exam_default_sequence_choices (exam_id=exam.exam_id) result.append (('First Sequence', [ render_select ("first", sequence_choices, exam.first_sequence, blank=True), ' (do not fill in unless specifically required)' ])) if exam is None or exam_may_edit_seat (exam): if exam is None: exam_assigned = cursor.execute_required_value ("select same_agg (exam_assigned) from exam_exam where (term_id, admin_id) = (%(term_id)s, %(admin_id)s) and exam_assigned is not null", term_id=term.code (), admin_id=admin.admin_id) allow_tablet_seats = False allow_single_seating = False else: exam_assigned = exam.exam_assigned allow_tablet_seats = exam.allow_tablet_seats allow_single_seating = exam.allow_single_seating result.append (('Seating', [ 'Assigned Seating ', render_select ("assigned", bool_choices, value=bool_write[exam_assigned]), html.br (), html.label ('Use Tablet Seats ', render_tooltip ('Seats with a tablet arm attached, unused by default'), ': ', render_checkbox ("tablet", checked=allow_tablet_seats or None)), html.br (), html.label ('Use Every Seat ', render_tooltip ('Use every seat for classrooms (not PAC, labs, etc.), by default uses every other seat'), ': ', render_checkbox ("single", checked=allow_single_seating or None)), ])) return result
[docs]def write_printing_controls (cursor, term, admin, exam=None, remote_identity=None): '''Write form controls for editing the printing options of an examination. :param cursor: DB connection cursor. :param term: In-context academic term. :param admin: In-context admin unit. :param exam: A database row representing an examination. :return: List of control titles and form controls. :rtype: list Returns controls for editing the printing options of the examination. ''' result = [] if exam is None or exam.master_accepted is None: default_uploader = remote_identity.person_id if exam is None else exam.exam_author_person_id result.append (('Authorized Uploader', render_select ("uploader", cursor.offering_uploader_choices (term_id=term.code (), admin_id=admin.admin_id), default_uploader, blank=True))) if exam is None or exam.master_accepted is None: master_count = 1 if exam is None else exam.master_count result.append (('Master Versions', html.input (name="master_count", value=master_count, size=2, maxlength=2))) if exam is None: scanning_integration = cursor.execute_required_value ("select same_agg (scanning_integration) from exam_exam_plus where (term_id, admin_id) = (%(term_id)s, %(admin_id)s)", term_id=term.code (), admin_id=admin.admin_id) result.append (('Scanning Integration', render_select ("scanning", [i[:2] for i in integration_choices], value=scanning_integration), )) return result
[docs]def write_create_controls (cursor, term, admin, remote_identity): '''Write form controls for creating an examination. :param cursor: DB connection cursor. :param term: In-context academic term. :param admin: In-context admin unit. :param remote_identity: The remote identity of the Web user. :return: List of control titles and form controls. :rtype: list Returns controls for editing the various characteristics of the examination which can be set at creation time. ''' return write_setup_controls (cursor, term, admin) + write_seating_controls (cursor, term, admin) + write_printing_controls (cursor, term, admin, remote_identity=remote_identity)
[docs]def write_edit_form (control_rows, prefix=None, submit=None): '''Write an HTML form based on control titles and controls. :param list control_rows: (title, controls) pairs. :param prefix: Additional HTML to insert at the beginning of the form. :param submit: An override submit button to use instead of the default. :return: An HTML form for editing the examination. The form is formatted as a 2-column table with the form control titles in the left column and the actual controls in the right column. A link to relevant documentation is included as well as a submit button. The form action is "?" so that it works with the examination creation form, which requires removing the series code from the GET parameters upon submission. ''' return html.form ( prefix, html.p (html.a ('Explanation of terms', href="https://uwaterloo.ca/odyssey/instruct/exams/edit-examinations")), html.table (html.tr (html.th (title), html.td (controls)) for title, controls in control_rows), html.p (html.input (type="submit", name="!update", value="Update!") if submit is None else submit), method="post", action="?" )
[docs]def handle_exam_edit_setup_form (cursor, form, exam): '''Change the basic setup of an examination based on form results. :param cursor: DB connection cursor. :param form: CGI form results. :param exam: A database row representing an examination. :return: An error result or None if successful. Updates the start time and duration of the examination according to the form results. ''' valid_admin = False if exam is None else cursor.execute_required_value("select exists (select from auth_offering_personnel_complete where (term_id, admin_id, person_id) = (%(term_id)s, %(admin_id)s, %(person_id)s) and role_current='t')", term_id=exam.term_id, admin_id=exam.schedule_admin_id, person_id=exam.exam_author_person_id) if exam.schedule_admin_id is not None and valid_admin is False: return 'Error: Assessment is scheduled.', html.p ('Start time and duration for scheduled assessments cannot be changed using this form.') if not exam_may_edit_cover (exam): return 'Error: Masters already approved', html.p ('The assessment masters have already been approved for printing. No further changes to the primary start time or duration can be made.') primary_sitting = form.optional_field_value ("primary") if primary_sitting is not None: # Map empty string to None, digits to integer primary_sitting = parse_form_value (primary_sitting, int) cursor.callproc_none ("exam_update_primary_sitting", exam.exam_id, primary_sitting) else: start_time = parse_datetime (form, 'start', parse_date_2, parse_time_2) if start_time is not None: # Set start time and create primary sitting cursor.callproc_none ("exam_create_primary_sitting", exam.exam_id, start_time) exam_duration = nullif (form.optional_field_value ('duration')) if exam_duration == "other": exam_duration = parse_time_2 (form, 'duration-other') values = { 'exam_id': exam.exam_id, 'exam_duration': exam_duration, } # Set examination duration; masters must not have been approved cursor.execute_none ("select exam_edit_set_duration (exam_id, %(exam_duration)s) from exam_exam where exam_id = %(exam_id)s and master_approved is null", **values)
[docs]def handle_exam_edit_seating_form (cursor, form, exam): '''Change the seating options of an examination based on form results. :param cursor: DB connection cursor. :param form: CGI form results. :param exam: A database row representing an examination. :return: An error result or None if successful. Updates the first sequence and seating options of the examination according to the form results. ''' values = { 'exam_id': exam.exam_id, 'first': nullif (form.optional_field_value ("first")), 'exam_assigned': bool_read.get (form.optional_field_value ("assigned")), 'allow_tablet_seats': "tablet" in form, 'allow_single_seating': "single" in form, } if values['exam_assigned'] is None: return 'Error: No assigned seating option chosen', html.p ('Please select whether or not you want assigned seating and try again.') if not exam_may_edit_seat (exam): return 'Error: Seating already finalized', html.p ('Seating details have already been finalized for this assessment. No further changes to seating options can be made.') # In addition to explicit check, write query to ensure that seating option # changes do not take place after seating finalization. cursor.execute_none ("update teaching_admin_term set admin_assigned = %(exam_assigned)s where (term_id, admin_id) = (%(term_id)s, %(admin_id)s) and admin_assigned is null", exam_assigned=values['exam_assigned'], term_id=exam.term_id, admin_id=exam.admin_id) cursor.execute_none ("update exam_exam set first_sequence = %(first)s, exam_assigned = %(exam_assigned)s, allow_tablet_seats = %(allow_tablet_seats)s, allow_single_seating = %(allow_single_seating)s where exam_id = %(exam_id)s and sequence_assigned is null", **values) cursor.callproc_none ("exam_candidate_update", exam.exam_id) cursor.callproc_none ("exam_assign_designate_seats", exam.exam_id)
[docs]def handle_exam_edit_printing_form (cursor, form, exam): '''Change the printing options of an examination based on form results. :param cursor: DB connection cursor. :param form: CGI form results. :param exam: A database row representing an examination. :return: An error result or None if successful. Updates the authorized uploaded and master version count options of the examination according to the form results. ''' exam_author_person_id = nullif (form.optional_field_value ('uploader')) if exam_author_person_id != exam.exam_author_person_id: if exam.master_accepted is None: cursor.execute_none ("update exam_exam set exam_author_person_id = %(exam_author_person_id)s where exam_id=%(exam_id)s and master_accepted is null", exam_author_person_id=exam_author_person_id, exam_id=exam.exam_id) else: return 'Error: Masters already accepted', html.p ('The assessment masters have already been accepted for printing. No further changes to the authorized uploader can be made.') master_count = parse_form_value (form.optional_field_value ("master_count"), int) if master_count != exam.master_count: if exam_may_edit_seat (exam): cursor.callproc_none ("exam_exam_set_master_count", exam.exam_id, master_count) else: return 'Error: Seating already finalized', html.p ('Seating details have already been finalized for this assessment. No further changes to the number of versions can be made.')
[docs]def handle_exam_create_form (cursor, form, exam): '''Adjust settings of a newly-created examination based on form results. :param cursor: DB connection cursor. :param form: CGI form results. :param exam: A database row representing an examination. :return: An error result or None if successful. Run the handlers for basic setup, seating, and printing. If any of them reports an error, report that error. ''' for handler in handle_exam_edit_setup_form, handle_exam_edit_seating_form, handle_exam_edit_printing_form: result = handler (cursor, form, exam) if result is not None: return result
[docs]def exam_may_edit_cover (exam): """Determine whether exam details related to the cover can be edited. :param exam: A database row representing an examination. :return: Represents whether exam cover details can be edited. :rtype: bool """ return ((exam.primary_sitting_id is None or exam.primary_start_time is None or exam.primary_start_time > datetime.now ()) and exam.master_approved is None)
[docs]def exam_may_edit_seat (exam): """Determine whether exam details related to seat assignment can be edited. :param exam: A database row representing an examination. :return: Represents whether exam seating assignment details can be edited. :rtype: bool """ return exam.sequence_assigned is None
@return_html def exam_editor_setup_get_handler (cursor, term, admin, roles, exam): '''Assessment editing form GET URL handler. Displays the form for editing basic setup of the current examination. ''' result = [format_return ('Main Menu', None, None, 'Offering', None, dot='Assessment')] result.append (write_edit_form (write_setup_controls (cursor, term, admin, exam))) return "%s: Edit Setup Options" % exam.full_title, result @use_form_param @return_html def exam_editor_setup_post_handler (cursor, term, admin, roles, form, exam): if not 'ISC' in roles: raise status.HTTPForbidden () result = handle_exam_edit_setup_form (cursor, form, exam) if result is None: raise status.HTTPFound (".") else: return result exam_editor_setup_handler = delegate_get_post (exam_editor_setup_get_handler, exam_editor_setup_post_handler) @return_html def exam_editor_seating_get_handler (cursor, term, admin, roles, exam): '''Assessment editing form GET URL handler. Displays the form for editing seating options of the current examination. ''' result = [format_return ('Main Menu', None, None, 'Offering', None, dot='Assessment')] result.append (write_edit_form (write_seating_controls (cursor, term, admin, exam))) return "%s: Edit Seating Options" % exam.full_title, result @use_form_param @return_html def exam_editor_seating_post_handler (cursor, term, admin, roles, form, exam): if not 'ISC' in roles: raise status.HTTPForbidden () result = handle_exam_edit_seating_form (cursor, form, exam) if result is None: raise status.HTTPFound (".") else: return result exam_editor_seating_handler = delegate_get_post (exam_editor_seating_get_handler, exam_editor_seating_post_handler) @return_html def exam_editor_printing_get_handler (cursor, term, admin, roles, exam): '''Assessment editing form GET URL handler. Displays the form for editing printing options of the current examination. ''' result = [format_return ('Main Menu', None, None, 'Offering', None, dot='Assessment')] result.append (write_edit_form (write_printing_controls (cursor, term, admin, exam))) return "%s: Edit Printing Options" % exam.full_title, result @use_form_param @return_html def exam_editor_printing_post_handler (cursor, term, admin, roles, form, exam): if not 'ISC' in roles: raise status.HTTPForbidden () result = handle_exam_edit_printing_form (cursor, form, exam) if result is None: raise status.HTTPFound (".") else: return result exam_editor_printing_handler = delegate_get_post (exam_editor_printing_get_handler, exam_editor_printing_post_handler) @use_form_param @return_html def exam_sitting_add_handler (cursor, term, admin, roles, form, exam): """Assessment special sitting creation POST URL handler. Handles the form for adding a new sitting for an examination. """ if not 'ISC' in roles: raise status.HTTPForbidden () if '!add-sitting' in form and exam.primary_sitting_id is not None: start_time = parse_datetime (form, 'ns', parse_date_2, parse_time_2) if start_time is None: return "Error: Start time not given", [html.p ('Please go back and fill in the start time.')] else: cursor.execute_none ("select exam_require_special_sitting (%(exam_id)s, %(start_time)s)", exam_id=exam.exam_id, start_time=start_time) raise status.HTTPFound ("./") exam_sitting_add_handler = delegate_get_post (None, exam_sitting_add_handler)