diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index 7581b8758..369a6dfec 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -927,12 +927,11 @@ class EditTests(TestCase): q = PyQuery(r.content) room = Room.objects.get(meeting=meeting, session_types='regular') - self.assertTrue(q("th:contains(\"{}\")".format(room.name))) - self.assertTrue(q("th:contains(\"{}\")".format(room.capacity))) + self.assertTrue(q("h5:contains(\"{}\")".format(room.name))) + self.assertTrue(q("h5:contains(\"{}\")".format(room.capacity))) timeslots = TimeSlot.objects.filter(meeting=meeting, type='regular') - self.assertTrue(q("td:contains(\"{}\")".format(timeslots[0].time.strftime("%H:%M")))) - self.assertTrue(q("td.timeslot[data-timeslot=\"{}\"]".format(timeslots[0].pk))) + self.assertTrue(q("div.timeslot[data-timeslot=\"{}\"]".format(timeslots[0].pk))) sessions = Session.objects.filter(meeting=meeting, type='regular') for s in sessions: diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index 8d99ed62e..3876ccbca 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -11,6 +11,7 @@ import glob import io import itertools import json +import math import os import pytz import re @@ -456,14 +457,8 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None): assignments = get_all_assignments_from_schedule(schedule) - # FIXME - #areas = get_areas() - #ads = find_ads_for_meeting(meeting) - - css_ems_per_hour = 1.5 - rooms = meeting.room_set.filter(session_types__slug='regular').distinct().order_by("capacity") - timeslots_qs = meeting.timeslot_set.filter(type='regular').prefetch_related('type', 'sessions').order_by('time', 'location', 'name') + timeslots_qs = meeting.timeslot_set.filter(type='regular').prefetch_related('type', 'sessions').order_by('location', 'time', 'name') sessions = add_event_info_to_session_qs( Session.objects.filter( @@ -511,25 +506,106 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None): for a in assignments: assignments_by_session[a.session_id].append(a) - # prepare timeslot matrix - times = {} # start time -> end time - timeslots = {} - timeslots_by_pk = {} - for ts in timeslots_qs: - ts_end_time = ts.end_time() - if ts_end_time < times.get(ts.time, datetime.datetime.max): - times[ts.time] = ts_end_time + # Prepare timeslot layout. We arrange time slots in columns per + # room where everything inside is grouped by day. Things inside + # the days are then layouted proportionally to the actual time of + # day, to ensure that everything lines up, even if the time slots + # are not the same in the different rooms. - timeslots[(ts.location_id, ts.time)] = ts - timeslots_by_pk[ts.pk] = ts - ts.session_assignments = [] + def timedelta_to_css_ems(timedelta): + css_ems_per_hour = 1.8 + return timedelta.seconds / 60.0 / 60.0 * css_ems_per_hour - timeslot_matrix = [ - (start_time, end_time, (end_time - start_time).seconds / 60.0 / 60.0 * css_ems_per_hour, [(r, timeslots.get((r.pk, start_time))) for r in rooms]) - for start_time, end_time in sorted(times.items()) - ] + # time labels column + timeslots_by_day = defaultdict(list) + for t in timeslots_qs: + timeslots_by_day[t.time.date()].append(t) + + day_min_max = [] + for day, timeslots in sorted(timeslots_by_day.iteritems()): + day_min_max.append((day, min(t.time for t in timeslots), max(t.end_time() for t in timeslots))) + + time_labels = [] + for day, day_min_time, day_max_time in day_min_max: + day_labels = [] + + hourly_delta = 2 + + first_hour = int(math.ceil((day_min_time.hour + day_min_time.minute / 60.0) / hourly_delta) * hourly_delta) + t = day_min_time.replace(hour=first_hour, minute=0, second=0, microsecond=0) + + last_hour = int(math.floor((day_max_time.hour + day_max_time.minute / 60.0) / hourly_delta) * hourly_delta) + end = day_max_time.replace(hour=last_hour, minute=0, second=0, microsecond=0) + + while t <= end: + day_labels.append((t, 'top', timedelta_to_css_ems(t - day_min_time), 'left')) + t += datetime.timedelta(seconds=hourly_delta * 60 * 60) + + if not day_labels: + day_labels.append((day_min_time, 'top', 0, 'left')) + + time_labels.append({ + 'day': day, + 'height': timedelta_to_css_ems(day_max_time - day_min_time), + 'labels': day_labels, + }) + + # room columns + timeslots_by_room_and_day = defaultdict(list) + for t in timeslots_qs: + timeslots_by_room_and_day[(t.location_id, t.time.date())].append(t) + + room_columns = [] + for r in rooms: + room_days = [] + + for day, day_min_time, day_max_time in day_min_max: + day_timeslots = [] + for t in timeslots_by_room_and_day.get((r.pk, day), []): + day_timeslots.append({ + 'timeslot': t, + 'offset': timedelta_to_css_ems(t.time - day_min_time), + 'height': timedelta_to_css_ems(t.end_time() - t.time), + }) + + room_days.append({ + 'day': day, + 'timeslots': day_timeslots, + 'height': timedelta_to_css_ems(day_max_time - day_min_time), + }) + + if any(d['timeslots'] for d in room_days): + room_columns.append({ + 'room': r, + 'days': room_days, + }) # prepare sessions + for ts in timeslots_qs: + ts.session_assignments = [] + timeslots_by_pk = {ts.pk: ts for ts in timeslots_qs} + + def cubehelix(i, total, hue=1.2, start_angle=0.5): + # https://arxiv.org/pdf/1108.5083.pdf + rotations = total // 4 + x = float(i + 1) / (total + 1) + phi = 2 * math.pi * (start_angle / 3 + rotations * x) + a = hue * x * (1 - x) / 2.0 + + return ( + max(0, min(x + a * (-0.14861 * math.cos(phi) + 1.78277 * math.sin(phi)), 1)), + max(0, min(x + a * (-0.29227 * math.cos(phi) + -0.90649 * math.sin(phi)), 1)), + max(0, min(x + a * (1.97294 * math.cos(phi)), 1)), + ) + + session_parents = sorted(set( + s.group.parent for s in sessions + if s.group and s.group.parent and s.group.parent.type_id == 'area' or s.group.parent.acronym == 'irtf' + ), key=lambda p: p.acronym) + for i, p in enumerate(session_parents): + rgb_color = cubehelix(i, len(session_parents)) + p.scheduling_color = "#" + "".join(chr(int(round(x * 255))).encode('hex') for x in rgb_color) + unassigned_sessions = [] session_data = [] for s in sessions: @@ -573,7 +649,8 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None): d['comments'] = s.comments s.requested_duration_in_hours = s.requested_duration.seconds / 60.0 / 60.0 - s.scheduling_height = s.requested_duration_in_hours * css_ems_per_hour + s.layout_height = timedelta_to_css_ems(s.requested_duration) + s.parent_acronym = s.group.parent.acronym if s.group and s.group.parent else "" scheduled = False @@ -602,11 +679,11 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None): 'schedule': schedule, 'can_edit': can_edit, 'schedule_data': json.dumps(schedule_data, indent=2), + 'time_labels': time_labels, 'rooms': rooms, - 'timeslot_matrix': timeslot_matrix, + 'room_columns': room_columns, 'unassigned_sessions': unassigned_sessions, - 'timeslot_width': (100.0 - 10) / len(rooms), - #'areas': areas, + 'session_parents': session_parents, 'hide_menu': True, }) diff --git a/ietf/static/ietf/css/ietf.css b/ietf/static/ietf/css/ietf.css index 3f8ef79f6..9177d288a 100644 --- a/ietf/static/ietf/css/ietf.css +++ b/ietf/static/ietf/css/ietf.css @@ -990,57 +990,83 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container { padding-bottom: 10em; /* ensure there's room for the scheduling panel */ } -.edit-meeting-schedule .session { +.edit-meeting-schedule .edit-grid { + display: flex; +} + +.edit-meeting-schedule .schedule-column h5 { + text-align: center; + margin: 0; + white-space: nowrap; +} + +.edit-meeting-schedule .schedule-column .day { + position: relative; + margin-bottom: 3em; +} + +.edit-meeting-schedule .schedule-column .day > div { + position: absolute; +} + +.edit-meeting-schedule .time-labels-column > div { + min-width: 5em; + padding-right: 0.5em; +} + +.edit-meeting-schedule .time-labels-column .time-label { + width: 100%; +} + +.edit-meeting-schedule .time-labels-column .time-label.top-aligned { + border-top: 1px solid #ccc; +} + +.edit-meeting-schedule .time-labels-column .time-label.text-left span { background-color: #fff; - padding: 0 0.4em; - margin: 0.2em 0.4em; - border-radius: 0.3em; - text-align: center; + padding-right: 0.2em; } -.edit-meeting-schedule .session[draggable] { - cursor: grabbing; +.edit-meeting-schedule .time-labels-column .time-label.bottom-aligned { + border-bottom: 1px solid #ccc; } -.edit-meeting-schedule .session.dragging { - opacity: 0.3; - transition: opacity 0.4s; +.edit-meeting-schedule .room-column { + flex-grow: 1; } -.edit-meeting-schedule .session i.fa-comment-o { - width: 0; /* prevent icon from participating in text centering */ +.edit-meeting-schedule .room-column .day-label { + visibility: hidden; /* it's there to take up the space, but not shown */ } -.edit-meeting-schedule .edit-meeting-grid th { - text-align: center; -} - -.edit-meeting-schedule .edit-meeting-grid td.day { - padding-top: 1em; -} - -.edit-meeting-schedule .edit-meeting-grid td.timeslot { - border: 2px solid #fff; +.edit-meeting-schedule .timeslot { + display: flex; + flex-direction: column; background-color: #f6f6f6; - padding: 1px; + width: 100%; + border-right: 0.2em solid #fff; + border-left: 0.2em solid #fff; + overflow: hidden; } -.edit-meeting-schedule .edit-meeting-grid td.timeslot.disabled { - background-color: #fff; -} - -.edit-meeting-schedule .edit-meeting-grid td.timeslot.dropping { +.edit-meeting-schedule .timeslot.dropping { background-color: #f0f0f0; transition: background-color 0.2s; } +.edit-meeting-schedule .timeslot.overfull { + border-bottom: 2px dashed #ddd; +} + .edit-meeting-schedule .scheduling-panel { - position: fixed; + position: fixed; /* backwards compatibility */ + position: sticky; bottom: 0; left: 0; margin: 0; padding: 0 1em; width: 100%; + border-top: 0.2em solid #eee; background-color: #fff; opacity: 0.95; z-index: 1; @@ -1056,7 +1082,52 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container { transition: background-color 0.2s; } +.edit-meeting-schedule .session-parent-toggles { + margin-top: 1em; +} + +.edit-meeting-schedule .session-parent-toggles label { + font-weight: normal; + margin-right: 1em; + padding: 0 1em; + border: 0.1em solid #eee; + cursor: pointer; +} + +/* sessions */ +.edit-meeting-schedule .session { + background-color: #fff; + padding: 0 0.2em; + padding-left: 0.5em; + border: 0.2em solid #f6f6f6; /* this compensates for sessions being relatively smaller than they should */ + border-radius: 0.4em; + text-align: center; + overflow: hidden; +} + +.edit-meeting-schedule .session[draggable] { + cursor: grabbing; +} + +.edit-meeting-schedule .session.dragging { + opacity: 0.3; + transition: opacity 0.4s; +} + +.edit-meeting-schedule .session .color { + display: inline-block; + width: 1em; + height: 1em; + vertical-align: middle; +} + +.edit-meeting-schedule .session i.fa-comment-o { + width: 0; /* prevent icon from participating in text centering */ +} + .edit-meeting-schedule .unassigned-sessions .session { + vertical-align: top; display: inline-block; min-width: 6em; + margin-right: 0.4em; } diff --git a/ietf/static/ietf/js/edit-meeting-schedule.js b/ietf/static/ietf/js/edit-meeting-schedule.js index 27fc5ef42..730752dbb 100644 --- a/ietf/static/ietf/js/edit-meeting-schedule.js +++ b/ietf/static/ietf/js/edit-meeting-schedule.js @@ -9,6 +9,7 @@ jQuery(document).ready(function () { } var sessions = content.find(".session"); + var timeslots = content.find(".timeslot"); // dragging sessions.on("dragstart", function (event) { @@ -66,6 +67,7 @@ jQuery(document).ready(function () { function done() { dropElement.append(sessionElement); // move element + maintainTimeSlotHints(); } if (dropElement.hasClass("unassigned-sessions")) { @@ -90,5 +92,37 @@ jQuery(document).ready(function () { }).fail(failHandler).done(done); } }); + + + // hints + function maintainTimeSlotHints() { + timeslots.each(function () { + var total = 0; + jQuery(this).find(".session").each(function () { + total += +jQuery(this).data("duration"); + }); + + jQuery(this).toggleClass("overfull", total > +jQuery(this).data("duration")); + }); + } + + maintainTimeSlotHints(); + + // toggling of parents + var sessionParentInputs = content.find(".session-parent-toggles input"); + + function maintainSessionParentToggling() { + var checked = []; + sessionParentInputs.filter(":checked").each(function () { + checked.push(".parent-" + this.value); + }); + + sessions.filter(".toggleable").filter(checked.join(",")).show(); + sessions.filter(".toggleable").not(checked.join(",")).hide(); + } + + sessionParentInputs.on("click", maintainSessionParentToggling); + + maintainSessionParentToggling(); }); diff --git a/ietf/templates/meeting/edit_meeting_schedule.html b/ietf/templates/meeting/edit_meeting_schedule.html index 3fac4309b..14007b6b8 100644 --- a/ietf/templates/meeting/edit_meeting_schedule.html +++ b/ietf/templates/meeting/edit_meeting_schedule.html @@ -5,12 +5,9 @@ {% load ietf_filters %} {% block morecss %} - {# FIXME #} - {% for area in area_list %} - .group-colored-{{ area.upcase_acronym}} { - border-color: {{ area.fg_color}}; - color:{{ area.fg_color }}; - background-color: {{ area.bg_color }} + {% for parent in session_parents %} + .parent-{{ parent.acronym }} { + background: linear-gradient(to right, {{ parent.scheduling_color }} 0.4em, #fff 0.5em); } {% endfor %} {% endblock morecss %} @@ -60,50 +57,67 @@ {% endif %}

- - - - - {% for r in rooms %} - - {% endfor %} - - +
+ {# note: in order for all this to align properly, make sure there's the same markup in all columns #} -
- {% for start_time, end_time, hours, room_timeslots in timeslot_matrix %} - {% ifchanged %} - - - - {% endifchanged %} +
+
 
-
- + {% for d in time_labels %} +
+ {{ d.day|date:"D" }}
+ {{ d.day|date:"Y-m-d" }} +
- {% for r, timeslot in room_timeslots %} - +
+ {% for t, vertical_alignment, vertical_offset, horizontal_alignment in d.labels %} +
+ {{ t|date:"H:i" }} +
{% endfor %} -
+ {% endfor %} - -
{{ r.name }}{% if r.capacity %} - {{ r.capacity }} {% endif %}
- {{ start_time|date:"l, F j, Y" }} -
-
{{ start_time|date:"G:i" }}-{{ end_time|date:"G:i" }}
-
- {% for assignment, session in timeslot.session_assignments %} - {% include "meeting/edit_meeting_schedule_session.html" %} - {% endfor %} -
+ + + {% for r in room_columns %} +
+
{{ r.room.name }}{% if r.room.capacity %} ({{ r.room.capacity }} {% endif %})
+ + {% for d in r.days %} +
+ {{ d.day|date:"D" }}
+ {{ d.day|date:"Y-m-d" }} +
+ +
+ {% for t in d.timeslots %} +
+ {% for assignment, session in t.timeslot.session_assignments %} + {% include "meeting/edit_meeting_schedule_session.html" %} + {% endfor %} +
+ {% endfor %} +
+ {% endfor %} +
+ {% endfor %} +
-

Unscheduled

+
Not yet assigned
+
{% for session in unassigned_sessions %} {% include "meeting/edit_meeting_schedule_session.html" %} {% endfor %}
+ +
+ Show: + {% for p in session_parents %} + + {% endfor %} +
diff --git a/ietf/templates/meeting/edit_meeting_schedule_session.html b/ietf/templates/meeting/edit_meeting_schedule_session.html index 7d9828a49..c64616ba1 100644 --- a/ietf/templates/meeting/edit_meeting_schedule_session.html +++ b/ietf/templates/meeting/edit_meeting_schedule_session.html @@ -1,3 +1,3 @@ -
+
{{ session.scheduling_label }} {% if session.comments %}{% endif %}