Source code for uw.local.teaching.webui.ta.student

"""Student-related TA assignment display routines.

These are functions related to displaying TA information for students.
"""

from operator import attrgetter
from itertools import groupby, islice
from re import match
from datetime import date

from ll.xist.ns import html

from uw.dbtools import copy_to_csv

from uw.web.html.form import render_select, render_hidden, render_checkbox
from uw.web.html.format import make_table_format, format_return
from uw.web.html.join import html_join
from uw.web.html.sorttable import make_table_format_sortable, table_column

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, return_csv
from uw.web.wsgi.status import HTTPFound, HTTPForbidden

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

from .entitlement import render_entitlements
from .ui import format_units, format_actual, format_entitlement

[docs]def format_plans (cursor, term_id, person_id): """Format a student's plans in a convenient form. :param cursor: DB connection cursor. Displays all the plan codes, with each equipped with a title attribute to allow seeing the plan description when hovering the mouse cursor over it. """ plans = cursor.execute_tuples ("select plan_code, plan_transcript_description from std_combined_term_plan natural join std_combined_term_history natural join pps_plan where (uw_id, request_term_id) = ((select uw_id from person_identity_complete where person_id = %(person_id)s)::quest_uw_id, %(term_id)s) order by plan_order", person_id=person_id, term_id=term_id) return html_join ((html.span (plan.plan_code, title=plan.plan_transcript_description) for plan in plans), sep=' ')
[docs]def format_supervision (cursor, term_id, person_id): """Format a student's supervisors in a convenient form. :param cursor: DB connection cursor. :param term_id: The academic term of interest. :param person_id: The identity of the student. A better-formatted version of the query: | WITH t AS ( | SELECT * | FROM std_term_grad_supervision NATURAL JOIN std_grad_supervisor | WHERE (term_id, uw_id) = ([[term_id]], (select uw_id::quest_uw_id from person_identity_complete where person_id = [[person_id]])) | ) | SELECT t.*, person_id AS supervisor_person_id | FROM t LEFT JOIN person_identity_complete AS pic ON (pic.uw_id = t.supervisor_uw_id::text); | ORDER BY supervisor_seq """ supervisors = cursor.execute_tuples ("with t as (select * from std_term_grad_supervision natural join std_grad_supervisor where (term_id, uw_id) = (%(term_id)s, (select uw_id::quest_uw_id from person_identity_complete where person_id = %(person_id)s))) select t.*, person_id as supervisor_person_id from t left join person_identity_complete as pic on (pic.uw_id = t.supervisor_uw_id::text) order by supervisor_seq", person_id=person_id, term_id=term_id) return html_join (('[[%08d]]' % sv.supervisor_uw_id if sv.supervisor_person_id is None else format_person (cursor, sv.supervisor_person_id) for sv in supervisors), sep=html.br ())
[docs]def format_citizenship (r): """Format a citizenship status in a convenient form. :param r: a database row representing a student, as passed to format_students. All that matters for TA assignment is whether the person is here on a student permit. Accordingly, we display "Visa" if so, otherwise the column remains blank. """ status = r.citizenship_status if status in ('CTZ', 'PRR'): return None if status in ('CAN-SP',): return 'Visa' return '?? (%s)' % status
[docs]def cannot_remove (r): """Determine whether a student can be removed from the list. :param r: a database row representing a student, as passed to format_students. This attempts to require a student to be in some sense "blank" before they can be removed to avoid accidentally deleting important information. """ return r.student_comment is not None or r.total_assigned_units
[docs]def format_students (cursor, unit, prefix, students, roles, current_term=None): """Format a table of graduate students with TA assignment information. :param cursor: DB connection cursor. :param prefix: URL prefix for links to individual student pages. :param students: the student records, typically row objects obtained from the database. :param roles: A list of user roles for a particular unit :param current_term: the current term, or None if the table should leave out the columns that are relative to a current term. """ table_columns = [] if 'TAMAN' in roles: table_columns.append (table_column (col=html.col (style="width: 5em;"), head='Remove', content=lambda r: None if cannot_remove (r) else render_checkbox ("student", value="%s-%s" % (r.unit_code, r.person_id)), sort_style="none")) table_columns.extend ([ table_column (col=html.col (style="width: 25em;"), head='Student', content=lambda r: format_person (cursor, r.person_id)), table_column (col=html.col (style="width: 10em;"), head='Plan', content=lambda r: format_plans (cursor, r.term_id, r.person_id)), table_column (col=html.col (style="width: 20em;"), head='Supervisor', content=lambda r: format_supervision (cursor, r.term_id, r.person_id)), table_column (col=html.col (style="width: 5em; border-right: 2px solid black;"), head='Visa', content=format_citizenship), ]) if current_term is not None: start_term = current_term - 6 terms = list (islice (start_term.countFrom (), 6)) unit_totals = {} for person_id, r in groupby (cursor.execute_tuples ("select term_id, person_id, total_assigned_units from ta_student_term_plus where unit_code = %(unit_code)s and term_id >= %(term_id)s and person_id = any (%(students)s) order by person_id", unit_code=unit.unit_code, term_id=start_term.code (), students=[s.person_id for s in students]), attrgetter ('person_id')): unit_totals[person_id] = dict ((s.term_id, s.total_assigned_units) for s in r) def make_column (term): code = term.numericCode () return table_column (col=html.col (style="width: 4em;"), head=term.abbreviation (), content=lambda r: format_units (unit_totals[r.person_id].get (code))) for term in terms: table_columns.append (make_column (term)) def make_job_column (term, border=False): def column_content (r): info = cursor.execute_tuples ("select distinct admin_description, ' ' as space, string_agg(job_code, '/' order by job_code), buyout_id from ta_position_assignment natural join teaching_admin natural left join (select admin_id, person_id as buyout_id from teaching_admin_person) t where (unit_code, term_id, person_id) = (%(unit_code)s, %(term_id)s, %(person_id)s) group by (admin_description, buyout_id) order by admin_description", unit_code=unit.unit_code, term_id=term.code (), person_id=r.person_id) return html_join ((('Buyout: ', format_person (cursor, r.buyout_id)) if r.string_agg == 'BO' and r.buyout_id != None else r for r in info), html.br ()) return table_column (col=html.col (style="width: 5em;" + (" border-left: 2px solid black;" if border else "")), head=term.abbreviation () + ' Job', content=column_content) table_columns.append (make_job_column (terms[-1], border=True)) table_columns.append (table_column (col=html.col (style="width: 5em;"), head='Entitlement Before', content=lambda r: format_entitlement (r.entitled_units), sort_style="float")) table_columns.extend ([ table_column (col=html.col (style="width: 5em;"), head='Offered', content=lambda r: format_units (r.student_offered_units)), table_column (col=html.col (style="width: 10em;"), head='Assigned', content=lambda r: format_actual (r.total_assigned_units, r.student_offered_units), sort_order=lambda r: format_units (r.total_assigned_units), sort_style="float"), ]) if current_term is not None: table_columns.append (make_job_column (current_term)) table_columns.append (table_column (col=html.col (style="width: 5em; border-right: 2px solid black;"), head='Entitlement After', content=lambda r: None if r.entitled_units is None else format_entitlement (r.entitled_units - r.total_assigned_units), sort_style="float")) def render_comment_column (r): comment_id = str (r.person_id) + "-" + r.unit_code divs = [html.div ( html.span (r.student_comment, class_="comment-text", id="comment-text-" + comment_id), html.i (class_="fa fa-pencil comment-toggle comment-edit", id="comment-edit-" + comment_id), class_="comment-display", id="comment-display-" + comment_id)] if 'TAMAN' in roles: divs.append (html.div ( html.input (type="text", class_="comment-input", id="comment-input-" + comment_id, value=r.student_comment), html.i (class_="fa fa-check comment-save", id="comment-save-" + comment_id), html.i (class_="fa fa-times comment-toggle comment-close", id="comment-close-" + comment_id), class_="comment-editable", id="comment-editable-" + comment_id)) return html.div (divs) if 'TAMAN' in roles: link = 'Edit…' else: link = 'View…' table_columns.extend ([ table_column (col=html.col (style="width: 5em;"), head='Actions', content=lambda r: html.a (link, href=prefix + "%d/" % r.person_id), sort_style="none"), table_column (col=html.col (style="width: 15em;"), head='Comments', content=render_comment_column), ]) return make_table_format_sortable (*table_columns) (students)
[docs]def get_students (cursor, unit, term, where=None): if where is None: where = "" else: where = " and (%s)" % where sql = "select tst.*, ta_student_term_entitlement (unit_code, term_id, person_id) as entitled_units, _quest.std_citizenship_status (uw_id::quest_uw_id) as citizenship_status from ta_student_term_plus as tst natural join person_identity_complete pic where (term_id, unit_code) = (%%(term_id)s, %%(unit_code)s)%s order by lower (surname), lower (givennames), pic.uw_id" % where return cursor.execute_tuples (sql, term_id=term.code (), unit_code=unit.unit_code)
@return_html def student_get_index (cursor, unit, term, roles): result = [format_return ('Term')] students = get_students (cursor, unit, term) result.append (html.form ( format_students (cursor, unit, "", students, roles, current_term=term), html.p ( html.input (type="submit", name="!remove", value="Remove Selected Students!"), html.br (), 'Only students with no job assignments can be removed. To remove students with job assignments, remove the job assignments first.', ) if 'TAMAN' in roles else None, method="post", action="" )) result.append (html.a ('Download Students as CSV', href="../tamuo")) if 'TAMAN' in roles: result.append (html.h2 ('Add Student')) result.append (html.form ( html.table ( html.tr (html.th ('UW ID:'), html.td (html.input (type="text", name="uwid", size=9, maxlength=8))), html.tr (html.th ('Userid:'), html.td (html.input (type="text", name="userid", size=9, maxlength=8))), html.tr (html.th ('Units offered:'), html.td (html.input (type="text", name="units", size=5, value="1.00"))), ), html.p (html.input (type="submit", name="!add", value="Add Student!")), method="post", action="" )) return '%s Students' % term.description (), result @delegate_get_post @return_csv def student_tamuo_handler (cursor, unit, term, roles): """Generate a list of TAs specifically for the Math Undergrad Office (MUO). """ unit_code = cursor.callproc_required_value ("quote_literal", unit.unit_code) result = copy_to_csv (cursor, "select to_char (uw_id) as uw_id, surname, givennames, plan_code, ta_jobs, ta_units, gender_code, email from (select uw_id::quest_uw_id, surname, givennames, userid || '@uwaterloo.ca' as email, term_id, unit_code, string_agg (admin_description || ' ' || job_code, '; ') as ta_jobs, sum (assigned_units) as ta_units from ta_position_assignment natural join teaching_admin natural join person_identity_complete group by term_id, unit_code, uw_id, surname, givennames, userid) as t natural join (select term_id, uw_id, string_agg (plan_code, ',') as plan_code from std_term_plan group by (term_id, uw_id)) as s natural join (select uw_id, gender_code from std_student) as q where (term_id, unit_code) = (%d, %s) order by lower (surname), lower (givennames), uw_id" % (term.numericCode (), unit_code)) return [result.getvalue (), '%s TA List (%s).csv' % (term.description (), unit.unit_code)] @use_form_param @return_html def student_post_index (cursor, unit, term, form, roles): if not 'TAMAN' in roles: raise HTTPForbidden () if "!add" in form: userid = parse_form_value (form.optional_field_value ("userid"), str) uw_id = parse_form_value (form.optional_field_value ("uwid"), str) if userid is None or uw_id is None: return 'Error: Userid or UW ID not given', html.p ( 'Please go back and fill in both the userid and UW ID.', ) result = cursor.execute_optional_tuple ("select * from person_identity_complete where userid = %(userid)s", userid=userid) if result is None: numeric_userid = match ('^[0-9]+$', userid) is not None return 'Error: Student not found', html.p ( 'Nobody with userid ', html.tt (userid), ' was found.', ' That does not look like a userid. Is it really a UW ID?' if numeric_userid else None, ) if result.uw_id != uw_id: return 'Error: Student UW ID mismatch', html.p ( 'Somebody with userid ', html.tt (userid), ' was found, but they do not have %s as their primary UW ID.' % uw_id ) success = cursor.callproc_required_value ("ta_add_student", unit.unit_code, term.code (), result.person_id) if not success: return 'Error: Student already enabled', html.p ( format_person (cursor, result.person_id), ' is already enabled for ', term.description (), '.', ) units = form.optional_field_value ("units") try: units = parse_form_value (units, float) except ValueError as x: return 'Error: Cannot understand number of units', html.p ( '“%s” does not look like a number of units.' % units ) cursor.callproc_none ("ta_edit_student", unit.unit_code, term.code (), result.person_id, units) raise HTTPFound ("%s/" % result.person_id) elif "!remove" in form: for student in form.multiple_field_value ("student"): unit_code, person_id = student.split ('-') cursor.callproc_none ("ta_drop_student", unit_code, term.code (), person_id) raise HTTPFound ("") student_index = delegate_get_post (student_get_index, student_post_index)
[docs]def render_assignment_controls (cursor, unit, term, person, student, table): result = [] result.append (html.form ( table, html.p ( html.input (type="submit", name="!update", value="Update Assignments!"), ' (set to 0 or blank to remove)', ), method="post", action="" )) result.append (html.form ( html.p ( 'Change offered units to ', html.input (type="text", name="units", value=format_units (student.student_offered_units)), ), html.p ( 'Update comment: ', html.input (type="text", name="comment", value=student.student_comment), ), html.p ( html.input (type="submit", name="!edit", value="Update Offer and Comment!"), ), method="post", action="" )) result.append (html.h2 ('New Job Assignment')) result.append (html.form ( html.p ( 'Assign this student to ', render_select ("position", cursor.execute_tuples ("select admin_id || '-' || job_code as code, admin_description || ' — ' || job_description as label from ta_position_term_relevant_plus left join (select * from ta_position_assignment where person_id = %(person_id)s) as t using (unit_code, term_id, admin_id, job_code) where (unit_code, term_id) = (%(unit_code)s, %(term_id)s) and position_active and exists and t is null and job_code <> 'BO' order by admin_description, job_description", unit_code=unit.unit_code, term_id=term.code (), person_id=person.person_id)), ' or record buyout of this student by ', render_select ("position", cursor.execute_tuples ("select distinct admin_id || '-BO' as code, surname || ', ' || givennames as label from teaching_admin_person join person_identity_complete using (person_id) join ta_position_term using (admin_id) where term_id between uw_term_offset (%(term_id)s, -6) and %(term_id)s and unit_code = %(unit_code)s order by label", term_id=term.code (), unit_code=student.unit_code) + [('OTHER', 'Other…')]), ' for ', html.input (type="text", name="units", value="1.00"), ' units. ', html.input (type="submit", name="!assign", value="Assign!"), ), method="post", action="" )) return result
@return_html def student_get_handler (cursor, unit, term, person, roles): result = [format_return ('Term', 'Student List')] editable = 'TAMAN' in roles and cursor.execute_required_value ("select %(term_id)s >= uw_term_from_time (current_date)::quest_term_id", term_id=term.code ()) student = cursor.execute_required_tuple ("select tst.* from ta_student_term_plus as tst where (term_id, person_id) = (%(term_id)s, %(person_id)s)", term_id=term.code (), person_id=person.person_id) if student.student_offered_units == student.total_assigned_units: units = format_units (student.student_offered_units, True) else: units = '%s offered, %s assigned' % (format_units (student.student_offered_units, True), format_units (student.total_assigned_units, True)) result.append (html.p (format_person (cursor, person.person_id), ' — ', units)) if student.student_comment is not None: result.append (html.p ('Comment: ', student.student_comment)) table_columns = [ html.col (style="width: 13em;"), html.col (style="width: 15em;"), html.col (style="width: 5em;"), ] table_rows = [ html.th ('Course'), html.th ('Job'), html.th ('Units'), ] if editable: table_columns.append (html.col (style="width: 7em;")) table_rows.append (html.th ('Update')) table = html.table ( html.colgroup (table_columns), html.tr (table_rows) ) admins = groupby (cursor.execute_tuples ("select tpa.*, admin_description, job_description from ta_position_assignment tpa join teaching_admin using (admin_id) join ta_job using (job_code) where (term_id, person_id) = (%(term_id)s, %(person_id)s)", term_id=term.code (), person_id=person.person_id), attrgetter ('admin_id')) for admin_id, assignments in admins: assignments = list (assignments) jobs = [] for a in assignments: job_columns = [ html.td (a.job_description), html.td (format_units (a.assigned_units)), ] if editable: job_columns.append (html.td (html.input (type="text", name="%s-%s" % (admin_id, a.job_code), value=format_units (a.assigned_units)))) jobs.append (html.tr (job_columns)) a = assignments[0] jobs[0].insert (0, html.td (html.a (a.admin_description, href="../../admin/%d/" % a.admin_id), rowspan=len (jobs))) table.append (jobs) if editable: result.append (render_assignment_controls (cursor, unit, term, person, student, table)) else: result.append (table) result.append (html.h2 ('TA Entitlement')) result.append (render_entitlements (cursor, unit, term, person, roles if editable else set (), "entitle")) return '%s Student Details for %s, %s (%s)' % (term.description (), person.surname, person.givennames, person.userid), result @use_form_param @return_html def student_post_handler (cursor, unit, term, person, form, roles): if not 'TAMAN' in roles: raise HTTPForbidden () if "!edit" in form: units = form.optional_field_value ("units") if units is not None: try: units = parse_form_value (units, float) except ValueError as x: return 'Error: Cannot understand number of units', html.p ( '“%s” does not look like a number of units.' % units ) cursor.callproc_none ("ta_edit_student", unit.unit_code, term.code (), person.person_id, units) comment = form.optional_field_value ("comment") if comment is not None: cursor.callproc_none ("ta_edit_student_comment", unit.unit_code, term.code (), person.person_id, comment) elif "!assign" in form: units = form.optional_field_value ("units") try: units = parse_form_value (units, float) or 0 except ValueError as x: return 'Error: Cannot understand number of units', html.p ( '“%s” does not look like a number of units.' % units ) position = [x for x in form.multiple_field_value ("position") if x] if not position: return 'Error: No position selected', html.p ( 'Please go back and select a position to assign.' ) if len (position) > 1: return 'Error: Multiple positions selected', html.p ( 'Please go back and select either a position or a buyout faculty member, not both.' ) position, = position if position == 'OTHER': return 'Select faculty member', html.form ( html.p ( render_hidden ("!buyout", ""), render_hidden ("units", value=units), 'Faculty member userid: ', html.input (type="text", name="userid", size=9, maxlength=8), ), method="post", action="" ) elif position == 'BUYOUT': person_id = form.required_field_value ("person_id") admin_id = cursor.callproc_required_value ("teaching_admin_person_require", person_id) job_code = 'BO' cursor.callproc_none ("ta_activate_position", unit.unit_code, admin_id, job_code, term.code ()) else: admin_id, job_code = position.split ('-') if job_code == 'BO': cursor.callproc_none ("ta_add_position", unit.unit_code, term.code (), admin_id, job_code) cursor.callproc_none ("ta_assign_position", unit.unit_code, term.code (), admin_id, job_code, person.person_id, units) elif "!buyout" in form: units = form.optional_field_value ("units") userid = form.required_field_value ("userid") person_id = cursor.execute_optional_value ("select person_id from person_identity_complete where userid = %(userid)s", userid=userid) if person_id is None: return 'Error: Person not found', html.p ( 'Nobody with userid ', html.tt (userid), ' was found.' ) else: return 'Confirm Buyout', html.form ( html.p ( render_hidden ("!assign", ""), render_hidden ("units", value=units), render_hidden ("position", "BUYOUT"), render_hidden ("person_id", person_id), 'Please confirm that this is the correct faculty member: ', format_person (cursor, person_id), ' ', html.input (type="submit", name="!assign", value="Assign!"), ), method="post", action="" ) elif "!update" in form: for position in cursor.execute_tuples ("select tpa.*, admin_description, job_description from ta_position_assignment tpa join teaching_admin using (admin_id) join ta_job using (job_code) where (term_id, person_id) = (%(term_id)s, %(person_id)s)", term_id=term.code (), person_id=person.person_id): admin_id = position.admin_id job_code = position.job_code units = form.optional_field_value ("%s-%s" % (admin_id, job_code)) try: units = parse_form_value (units, float) or 0 except ValueError as x: return 'Error: Cannot understand number of units', html.p ( '“%s” does not look like a number of units.' % units ) cursor.callproc_none ("ta_assign_position", unit.unit_code, term.code (), admin_id, job_code, person.person_id, units) raise HTTPFound ("") student_handler = delegate_get_post (student_get_handler, student_post_handler) @delegate_get_post @return_html def student_misallocated_handler (cursor, unit, term, roles): result = [format_return (dot='Term')] students = get_students (cursor, unit, term, where="student_offered_units <> total_assigned_units") under = [] over = [] for student in students: if student.student_offered_units > student.total_assigned_units: under.append (student) else: over.append (student) result.append (html.h2 ('Underallocated')) result.append (format_students (cursor, unit, "student/", under, roles, current_term=term)) result.append (html.h2 ('Overallocated')) result.append (format_students (cursor, unit, "student/", over, roles, current_term=term)) return '%s Misallocated Students' % term.description (), result @delegate_get_post @return_html def student_nonstandard_handler (cursor, unit, term, roles): result = [format_return ('Term')] students = get_students (cursor, unit, term, where="coalesce (student_offered_units, 0) <> 1.0") result.append (format_students (cursor, unit, "student/", students, roles, current_term=term)) return '%s Students with Non-standard Offer' % term.description (), result
[docs]def student_render_assignments (cursor, remote_identity, show_ta_history): result = None if show_ta_history: time_condition = "" else: time_condition = " and term_id >= uw_term_from_time (now () - '1 year'::interval)" assignments = cursor.execute_tuples ("select *, ta_student_term_entitlement (unit_code, term_id, person_id) as entitled_units, assignments_finalized is not true as tentative from ta_position_assignment natural join ta_student_term_plus natural join ta_job natural join teaching_admin natural join ta_term_unit_plus natural left join ta_eval_complete left join (select admin_id, person_id as admin_person_id from teaching_admin_person) as tap using (admin_id) where assignments_tentative and person_id = %(person_id)s" + time_condition + " order by term_id desc", person_id=remote_identity.person_id) if assignments: result = [html.h2 ('My TA Assignments')] if not show_ta_history else [] table = html.table ( html.tr ( html.th ('Term'), html.th ('Course'), html.th ('Job'), html.th ('Entitlement Before') if show_ta_history else None, html.th ('Assigned Units'), html.th ('Entitlement After') if show_ta_history else None, html.th ('Evaluation'), ) ) for assigned in assignments: show_eval = assigned.eval_approved and assigned.due row = [] row.append (html.tr ( html.td (fromCode (assigned.term_id).description ()), html.td (format_entitlement (assigned.entitled_units)) if show_ta_history else None, html.td (format_units (assigned.assigned_units) + (' (Tentative)' if assigned.tentative else '')), html.td (None if assigned.entitled_units is None else format_entitlement (assigned.entitled_units - assigned.total_assigned_units)) if show_ta_history else None, html.td (html.a ('View…', href="./eval/%s" % assigned.eval_id) if show_eval else None) )) if assigned.job_code == 'BO': row[0].insert(1, html.td ('Buyout: ', format_person (cursor, assigned.admin_person_id), colspan=2)) else: row[0].insert(1, html.td (assigned.admin_description)) row[0].insert(2, html.td (assigned.job_description)) table.append (row) result.append (table) return result