From 5faccf5379e55329f32e2999ca2fdaf82a511012 Mon Sep 17 00:00:00 2001
From: Ole Laursen
Date: Mon, 9 Mar 2020 19:40:31 +0000
Subject: [PATCH] Put the time slots in the new meeting schedule editor on a
time scale per day. This opens the possibility of having the slots not match
across the different rooms.
Add colors to session in areas/IRTF. Add support for toggling display
of areas/IRTF.
Show graphical hint when a time slot is overfull.
- Legacy-Id: 17415
---
ietf/meeting/tests_views.py | 7 +-
ietf/meeting/views.py | 129 ++++++++++++++----
ietf/static/ietf/css/ietf.css | 129 ++++++++++++++----
ietf/static/ietf/js/edit-meeting-schedule.js | 34 +++++
.../meeting/edit_meeting_schedule.html | 90 ++++++------
.../edit_meeting_schedule_session.html | 2 +-
6 files changed, 293 insertions(+), 98 deletions(-)
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 %}
- {{ r.name }}{% if r.capacity %} - {{ r.capacity }} {% endif %}
- {% 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 %}
-
-
- {{ start_time|date:"l, F j, Y" }}
-
-
- {% endifchanged %}
+
+
-
-
- {{ start_time|date:"G:i" }}-{{ end_time|date:"G:i" }}
-
+ {% for d in time_labels %}
+
+ {{ d.day|date:"D" }}
+ {{ d.day|date:"Y-m-d" }}
+
- {% for r, timeslot in room_timeslots %}
-
- {% for assignment, session in timeslot.session_assignments %}
- {% include "meeting/edit_meeting_schedule_session.html" %}
- {% endfor %}
-
+
+ {% for t, vertical_alignment, vertical_offset, horizontal_alignment in d.labels %}
+
+ {{ t|date:"H:i" }}
+
{% endfor %}
-
+
{% 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 %}
+ {{ p.acronym }}
+ {% 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 %}