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