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

"""Format a single room or list of rooms as HTML.

Contains functions for producing the HTML version of a room map or list of
rooms.
"""

from collections import defaultdict
from operator import attrgetter

from ll.xist.ns import html

from uw.color import rgb2html, hsv2rgb, hues

from uw.web.html.form import render_select, render_checkbox, render_hidden
from uw.web.html.format import make_table_format

from .ui import combine_column_fields, AttrDict, format_allocated_count

REMOVE_DIVISION_VALUE = '@'

[docs]class SeatHues (object): """This is a simple class to compute colours for room maps. It allows specifying a hue (or None for colour grey), then obtaining bright, pale, and faint versions of that hue. :param hue: A numeric value representing a hue value """ def __init__ (self, hue): """Constructor method.""" if hue is None: self.__bright = "#999999" self.__pale = "#BBBBBB" self.__faint = "#DDDDDD" else: self.__bright = rgb2html (hsv2rgb ((hue, 0.7, 1))) self.__pale = rgb2html (hsv2rgb ((hue, 0.4, 1))) self.__faint = rgb2html (hsv2rgb ((hue, 0.2, 1))) @property def bright (self): """Return the bright version of object's hue.""" return self.__bright @property def pale (self): """Return the pale version of object's hue.""" return self.__pale @property def faint (self): """Return the faint version of object's hue.""" return self.__faint
[docs]def wrap_table_cell (content): """Convert arbitrary content into a table cell. :param content: The HTML content to convert into a table cell :return: An HTML table cell generated from the parameter :rtype: HTML table cell (<td>, <th>) If the parameter is None, return an empty <td> styled to suppress the border. Otherwise return the parameter wrapped in <td> unless it is a table cell which can be returned directly. """ if content is None: return html.td (class_="room_map_empty") if content.__class__ not in (html.td, html.th): content = html.td (content) return content
[docs]def format_room_map (room_width, room_depth, format_seat, form): """Render a room map as an HTML table. :param room_width: The number of columns to show :param room_depth: The number of rows to show :param format_seat: The routine to invoke when rendering a seat :param form: Flag indicating whether map is part of a form :return: An HTML table configured by the parameters :rtype: HTML table (<table>) The returned value is a <table> of the specified size, and each cell is populated according to the result of format_seat. The returned value also includes some CSS and a <colgroup> appropriate to room maps. """ if form: def double_check (name, value): return html.td ( render_checkbox (name, value=value, checked=True, class_="uw-sel"), render_checkbox (name, value=value, checked=False, class_="uw-sel"), class_="room_map_sel") return html.table ( html.tr ( double_check ("a", ""), (double_check ("c", "%d" % col) for col in range (room_width)), double_check ("a", "") ), html.colgroup (width="1*", span=room_width), (html.tr ( double_check ("r", "%d" % row), (wrap_table_cell ( format_seat (col + 1, row + 1) ) for col in range (room_width)), double_check ("r", "%d" % row), ) for row in range (room_depth)), html.tr ( double_check ("a", ""), (double_check ("c", "%d" % col) for col in range (room_width)), double_check ("a", "") ), class_="room_map", ) else: return html.table ( html.colgroup (width="1*", span=room_width), (html.tr ( (wrap_table_cell ( format_seat (col + 1, row + 1) ) for col in range (room_width)), ) for row in range (room_depth)), class_="room_map", )
[docs]def dict_from_seats (seats): """Convert seats into a dictionary. :param seats: An iterable of seat information :return: A dictionary generated from the parameter :rtype: dict The seat information must have at least seat_col and seat_row fields. The result is a dictionary from (seat_col, seat_row) tuples to the original seat information values. """ return dict (((row.seat_col, row.seat_row), row) for row in seats)
[docs]def format_seats (seat_data, format_seat=attrgetter ('seat_code')): """Create and return a routine for formatting seats from given seat data. :param seat_data: A dictionary from (seat_col, seat_row) tuples to seat information rows :param format_seat: The routine to format a seat, given row of information about that seat :return: A function that returns a formatted seat given column and row :rtype: function The default format_seat routine simply formats the seat as the seat_code which is expected to be an attribute present in the provided seat data. The result function takes a column and row, looks them up in the seat_data provided to this function, and invokes the given format_seat routine on the resulting seat information. """ def format_location (col, row): seat = seat_data.get ((col, row)) if seat is None: return None else: return format_seat (seat) return format_location
[docs]def render_loading_standard_text (allow_tablet_seats=False, allow_single_seating=False, checkerboard=False): """Return the loading standard as an English sentence. :param allow_tablet_seats: Whether tablet seats are allowed. :param allow_single_seating: Whether single-seating is allowed. :param checkerboard: Whether we are showing the checkerboard usage pattern. :return: The loading standard as an English sentence. :rtype: string """ return 'The capacity shown is based on your settings: %s tablet-arm seats and%s.' % ( 'using' if allow_tablet_seats else 'avoiding', ' not skipping every other seat' if allow_single_seating else ', where needed, skipping every other seat' + (' using a checkerboard pattern' if checkerboard else '') )
[docs]def render_href (allow_tablet_seats, allow_single_seating, checkerboard): """Render the query portion of a URL corresponding to a loading standard. :param allow_tablet_seats: Whether tablet seats are allowed. :param allow_single_seating: Whether single-seating is allowed. :param checkerboard: Whether we are showing the checkerboard usage pattern. :return: The query of a URL generated from the loading standard. :rtype: string Writes the loading standard as the query portion of a URL, beginning with a question mark and including ts=, ss=, and/or cb= as appropriate, for allow_tablet_seats, allow_single_seating, and checkerboard respectively. """ return "?" + "&".join ((["ts="] if allow_tablet_seats else []) + (["ss="] if allow_single_seating else []) + (["cb="] if checkerboard else []))
[docs]def render_allow_tablet_seats (allow_tablet_seats): """Return whether tablet seats are allowed as an English sentence.""" return 'Use Tablet Seats' if allow_tablet_seats else 'Avoid Tablet Seats'
[docs]def render_allow_single_seating (allow_single_seating): """Return whether single-seating is allowed as an English sentence.""" return 'Use Every Seat' if allow_single_seating else 'Skip Seats Where Needed'
[docs]def render_checkerboard (checkerboard): return 'Use checkerboard pattern' if checkerboard else 'Use normal pattern'
[docs]def render_room (cursor, room, sitting=None, exam=None, form=False, allow_tablet_seats=False, allow_single_seating=False, checkerboard=False): """Render a room map. :param cursor: DB connection cursor. :param room: The relevant room as a DB result row. :param sitting: The relevant sitting, if any, as a DB result row. :param exam: The relevant examination, if any, as a DB result row. :param form: A flag indicating whether map is part of a form. :param allow_tablet_seats: Whether tablet seats are allowed. :param allow_single_seating: Whether single-seating is allowed. :param checkerboard: Whether we are showing the checkerboard usage pattern. :return: A single or a list of HTML elements for rendering a room map. :rtype: HTML or list of HTML The loading standard (allow_tablet_seats, allow_single_seating, checkerboard) is only used when the sitting is not specified. If form is given as True, the result includes a checkbox on each seat and is wrapped in an HTML <form> to allow changes to the seats to be specified. """ if sitting is None: seats = dict_from_seats (cursor.callproc_tuples ("exam_assign_room_map_seats", room.room_id, allow_tablet_seats, allow_single_seating, checkerboard)) else: seats = dict_from_seats (cursor.room_sitting_seats (sitting_id=sitting.sitting_id, room_id=room.room_id)) if exam is None: seathues = dict ((exam_id, SeatHues (hue)) for exam_id, hue in cursor.sitting_exam_hues (sitting.sitting_id).items ()) seathues[None] = SeatHues (None) else: cover_division = cursor.execute_optional_tuple ("select * from division_division_exam_cover_display natural join division_division_exam natural join division_division where exam_id = %(exam_id)s", exam_id=exam.exam_id) divisions = cursor.execute_tuples ("select division_value, format ('%%s (%%s)', division_value, count(*)) from division_division_exam_cover_display natural join division_division_exam natural join division_student where exam_id = %(exam_id)s group by division_value order by division_value", exam_id=exam.exam_id) seathues = dict (zip ([None] + [division.division_value for division in divisions], (SeatHues (h) for h in hues ()))) if sitting is None: def format_seat (seat): content = "%s %s" % (seat.seat_code, seat.seat_type_code) if seat.assignable: content = html.td (content, style="background-color: pink;") return content else: seat_counts = defaultdict (lambda: 0) seat_count_assigned = defaultdict (lambda: 0) seat_count_assigned_inuse = defaultdict (lambda: 0) seat_count_rush = defaultdict (lambda: 0) seat_count_rush_inuse = defaultdict (lambda: 0) def format_seat (seat): if exam is None: seat_hue = seathues[seat.exam_id] else: seat_hue = seathues[seat.division_value] if seat.exam_id == exam.exam_id else SeatHues (None) seat_counts[seat.division_value] += 1 if seat.seat_chosen: if seat.seat_assigned is None: colour = seat_hue.faint else: if seat.seat_assigned: seat_count_assigned[seat.division_value] += 1 if seat.seat_in_use: seat_count_assigned_inuse[seat.division_value] += 1 else: seat_count_rush[seat.division_value] += 1 if seat.seat_in_use: seat_count_rush_inuse[seat.division_value] += 1 if seat.seat_in_use: colour = seat_hue.bright else: colour = seat_hue.pale if form and (exam is None or exam.exam_id == seat.exam_id): checkbox = render_checkbox ("s", value="%d-%d" % (seat.seat_col, seat.seat_row), class_="a c%d r%d" % (seat.seat_col, seat.seat_row)) else: checkbox = None content = [seat.seat_code, ' ', checkbox] if exam is None or seat.exam_id == exam.exam_id: content.extend ([ seat.sequence_text, ' ', html.b ('Rv') if seat.seat_reserved else html.b ('Rh') if seat.seat_assigned is False else (html.tt (seat.userid) if seat.userid else None)]) if form: content = html.label (content) return html.td ( content, style="background-color: %s;" % colour, ) else: return seat.seat_code result = format_room_map (room.room_width, room.room_depth, format_seats (seat_data=seats, format_seat=format_seat), form) if exam is None: # Sitting view def controls (): return html.li ( 'Allocation: ', render_select ("allocate", [('u', 'Unallocated')] + [(r.exam_id, r.full_title) for r in cursor.exams_by_sitting_room (sitting_id=sitting.sitting_id, room_id=room.room_id)] ) ) elif cover_division is None: # No seat designation by division controls = lambda: None else: # Seats possibly designated by division def controls (): return html.li ( '%s: ' % cover_division.division_description, render_select ("division_value", divisions + [(REMOVE_DIVISION_VALUE, 'Remove')]), render_hidden ("division_seq", cover_division.division_seq), ) division_rows = [dv for dv in [d.division_value for d in divisions] + [None] if dv in seat_counts] # Insert seat designation legend in front of room map if needed if any (dv is not None for dv in division_rows): result = [ make_table_format ( (cover_division.division_description, lambda r: '[ANY]' if r is None else r), ('Allocated', lambda r: seat_counts[r]), ('Not Assigned', lambda r: '%d/%d' % (seat_count_rush_inuse[r], seat_count_rush[r])), ('Assigned', lambda r: '%d/%d' % (seat_count_assigned_inuse[r], seat_count_assigned[r])), row_attributor=lambda r: {'style': "background-color: %s;" % seathues[r].pale} ) (division_rows), result ] if form: result = html.form ( result, html.p ('You may make any of the following changes to the selected seats:'), html.ul ( controls (), html.li ('Reservation: ', render_select ("reserve", [('r', 'Reserved'), ('u', 'Not Reserved')])), html.li ('Unseat: ', render_select ("unseat", [('y', 'Yes')])), ), html.p (html.input (type="submit", name="!update", value="Update!")), method="post", action="" ) return result
[docs]def room_format_unallocated (r): """Format information about unallocated and reserved seats in a room. :param r: Room seat information :return: Formatted room information about unallocated and reserved seats :rtype: string """ result = '%s' % r.count_unallocated_seats if r.count_reserved_a_seats != 0: result += ' (%s reserved)' % r.count_reserved_a_seats return result
[docs]def room_format_capacity (r): """Format capacity information about a room's seats. :param r: Room seat information, a row from exam_exam_plus or its friends :return: Formatted room information about total and unreserved capacity :rtype: string Formats the total capacity and the unreserved capacity in a convenient form. If the two capacities are the same, simply show that number. Otherwise, show the total capacity, followed by the unreserved capacity, marked as such, in parentheses. """ result = '%s' % r.allocated_capacity if r.allocated_capacity != r.unreserved_capacity: result += ' (%s unreserved)' % r.unreserved_capacity return result
room_capacity_columns = ( (attrgetter ('count_all_seats'), 'Seats', ['count_all_seats']), (attrgetter ('room_capacity'), 'Capacity', ['room_capacity']), ) room_selected_columns = ( room_capacity_columns[0], (attrgetter ('count_selected_seats'), 'Selected', ['count_selected_seats']), (room_format_unallocated, 'Unallocated', ['count_unallocated_seats', 'count_reserved_a_seats']), ) room_extra_count_columns = ( (attrgetter ('count_allocated_seats'), 'Allocated', ['count_allocated_seats']), (attrgetter ('cram_limit'), 'Cram', ['cram_limit']), (room_format_capacity, 'Capacity', ['allocated_capacity', 'unreserved_capacity']), (lambda r: '%s/%s' % (r.count_used_rush_seats, r.count_rush_seats), 'Not Assigned', ['count_used_rush_seats', 'count_rush_seats']), (lambda r: '%s/%s' % (r.count_occupied_seats, r.count_assigned_seats), 'Assigned', ['count_occupied_seats', 'count_assigned_seats']), (lambda r: r.unreserved_capacity - r.count_rush_seats - r.count_assigned_seats, 'Available', ['unreserved_capacity', 'count_rush_seats', 'count_assigned_seats']), (attrgetter ('spare_count'), 'Spare', ['spare_count']), ) room_all_count_columns = room_selected_columns + room_extra_count_columns
[docs]def render_room_index (rooms, url_prefix, columns, addroom_cursor=None, addroom_url=None, grand_accum=None, allow_tablet_seats=False, allow_single_seating=False, checkerboard=False): """Render the given list of rooms as HTML. :param rooms: List of room information rows to render. :param url_prefix: The URL prefix for links to the rooms. :param columns: Table columns to include. :param addroom_cursor: DB connection cursor to use to obtain buildings list if needed. :param addroom_url: Form action for adding rooms. :param grand_accum: A dictionary from fields to cumulative sums. :param allow_tablet_seats: Whether tablet seats are allowed. :param allow_single_seating: Whether single-seating is allowed. :param checkerboard: Whether we are showing the checkerboard usage pattern. :return: An HTML table to render the given list of rooms :rtype: HTML table <table> Specifying addroom_url also indicates that the result should be a form which offers the selection of a building from which rooms may be added to the list. The loading standard (allow_tablet_seats, allow_single_seating) is used to generate URLs to the appropriate versions of the room maps when applicable. """ if addroom_url is None: addroom_row = None else: addroom_row = html.tr ( html.td ( html.form ( 'Add rooms in ', render_select ("building", ((b, b) for b in addroom_cursor.buildings ())), ' ', html.input (type="submit", value="Add…"), method="get", action=addroom_url + "add-room" ), colspan=len (columns) + 1 ) ) fields = combine_column_fields (columns) accum = AttrDict ((field, 0) for field in fields) 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 table = make_table_format ( ('Room', lambda r: html.a ('%s %s' % (r.building_code, r.room_code), href=url_prefix + "%d/" % r.room_id + render_href (allow_tablet_seats, allow_single_seating, checkerboard))), after_row=update, footer_row=lambda: [ html.tr ( html.th ('Total:'), [html.td (None if fields is None else html.b (valuegetter (accum))) for valuegetter, _, fields in columns] ), addroom_row ], *[(title, valuegetter) for valuegetter, title, _ in columns] ) (rooms) if grand_accum is not None: for field in fields: if accum[field] is None or grand_accum[field] is None: grand_accum[field] = None else: grand_accum[field] += accum[field] return table