"""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 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 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