From b60939a26c44ea0d09e0eeb2924c43cc78e319c5 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Wed, 17 Jun 2020 16:16:57 +0000 Subject: [PATCH] Change new meeting schedule editor to not layout the time slots on a linear scale, instead lay them out horizontally per day in chronological order with a small amount of size hinting by interpolating the width depending on the duration of the timeslot/session. Solve the problem of labeling time slots that don't necessarily align by simply labeling each slot separately. Add scheduled time slot information to the session info in the bottom right corner. Add selector for hiding timeslots to make it possible to hide special morning sessions. Add requested duration to the sessions in the grid. Use a smaller font size for the grid and switch to a non-serif, more condensed font. Tweak the margins. The grid is now slightly smaller than the old editor. Fix a couple of bugs. - Legacy-Id: 18012 --- ietf/meeting/views.py | 69 ++++------ ietf/static/ietf/css/ietf.css | 121 ++++++++++-------- ietf/static/ietf/js/edit-meeting-schedule.js | 44 +++++-- .../meeting/edit_meeting_schedule.html | 65 +++++++--- .../edit_meeting_schedule_session.html | 21 +-- 5 files changed, 184 insertions(+), 136 deletions(-) diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index 8f2d28cda..b095619e9 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -508,20 +508,22 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None): assignments_by_session[a.session_id].append(a) # Prepare timeslot layout, making a timeline per day scaled in - # browser em units to ensure that everything lines up even if the + # browser rem units to ensure that everything lines up even if the # timeslots are not the same in the different rooms + min_duration = min(t.duration for t in timeslots_qs) + max_duration = max(t.duration for t in timeslots_qs) + def timedelta_to_css_ems(timedelta): - css_ems_per_hour = 5 - return timedelta.seconds / 60.0 / 60.0 * css_ems_per_hour + capped_min_d = max(min_duration, datetime.timedelta(minutes=30)) + capped_max_d = min(max_duration, datetime.timedelta(hours=4)) + capped_timedelta = min(max(capped_min_d, timedelta), capped_max_d) - 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.items()): - day_min_max.append((day, min(t.time for t in timeslots), max(t.end_time() for t in timeslots))) + min_d_css_rems = 8 + max_d_css_rems = 10 + # interpolate + scale = (capped_timedelta - capped_min_d) / (capped_max_d - capped_min_d) if capped_min_d != capped_max_d else 1 + return min_d_css_rems + (max_d_css_rems - min_d_css_rems) * scale timeslots_by_room_and_day = defaultdict(list) room_has_timeslots = set() @@ -530,34 +532,7 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None): timeslots_by_room_and_day[(t.location_id, t.time.date())].append(t) days = [] - for day, day_min_time, day_max_time in day_min_max: - day_labels = [] - day_width = timedelta_to_css_ems(day_max_time - day_min_time) - - label_width = 4 # em - - 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: - left_offset = timedelta_to_css_ems(t - day_min_time) - right_offset = day_width - left_offset - if right_offset > label_width: - # there's room for the label - day_labels.append((t, 'left', left_offset)) - else: - day_labels.append((t, 'right', right_offset)) - - t += datetime.timedelta(seconds=hourly_delta * 60 * 60) - - if not day_labels: - day_labels.append((day_min_time, 'left', 0)) - + for day in sorted(set(t.time.date() for t in timeslots_qs)): room_timeslots = [] for r in rooms: if r.pk not in room_has_timeslots: @@ -565,23 +540,24 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None): timeslots = [] for t in timeslots_by_room_and_day.get((r.pk, day), []): - timeslots.append({ - 'timeslot': t, - 'offset': timedelta_to_css_ems(t.time - day_min_time), - 'width': timedelta_to_css_ems(t.end_time() - t.time), - }) + t.layout_width = timedelta_to_css_ems(t.end_time() - t.time) + timeslots.append(t) room_timeslots.append((r, timeslots)) days.append({ 'day': day, - 'width': day_width, - 'time_labels': day_labels, 'room_timeslots': room_timeslots, }) room_labels = [[r for r in rooms if r.pk in room_has_timeslots] for i in range(len(days))] + # possible timeslot start/ends + timeslot_groups = defaultdict(set) + for ts in timeslots_qs: + ts.start_end_group = "ts-group-{}-{}".format(ts.time.strftime("%Y%m%d-%H%M"), int(ts.duration.total_seconds() / 60)) + timeslot_groups[ts.time.date()].add((ts.time, ts.end_time(), ts.start_end_group)) + # prepare sessions for ts in timeslots_qs: ts.session_assignments = [] @@ -637,7 +613,7 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None): elif s.name: s.scheduling_label = s.name - s.requested_duration_in_hours = s.requested_duration.seconds / 60.0 / 60.0 + s.requested_duration_in_hours = round(s.requested_duration.seconds / 60.0 / 60.0, 1) session_layout_margin = 0.2 s.layout_width = timedelta_to_css_ems(s.requested_duration) - 2 * session_layout_margin @@ -688,6 +664,7 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None): 'js_data': json.dumps(js_data, indent=2), 'days': days, 'room_labels': room_labels, + 'timeslot_groups': sorted((d, list(sorted(t_groups))) for d, t_groups in timeslot_groups.items()), 'unassigned_sessions': unassigned_sessions, 'session_parents': session_parents, 'hide_menu': True, diff --git a/ietf/static/ietf/css/ietf.css b/ietf/static/ietf/css/ietf.css index 12158431d..774cdb8ee 100644 --- a/ietf/static/ietf/css/ietf.css +++ b/ietf/static/ietf/css/ietf.css @@ -1022,81 +1022,66 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container { } .edit-meeting-schedule .edit-grid .day { - margin-right: 2.5em; + margin-left: 1em; margin-bottom: 2em; } +.edit-meeting-schedule .edit-grid .room-label-column .day { + margin-left: 0; +} + .edit-meeting-schedule .edit-grid .day-label { height: 3em; - border-bottom: 2px solid transparent; } .edit-meeting-schedule .edit-grid .day-flow { margin-left: 8em; display: flex; flex-wrap: wrap; - justify-content: space-between; + justify-content: flex-start; } -@media only screen and (max-width: 120em) { - /* if there's only room for two days, it looks a bit odd with space-between */ - .edit-meeting-schedule .edit-grid .day-flow { - justify-content: flex-start; - } -} - -.edit-meeting-schedule .edit-grid .day-flow .day-label { - border-bottom: 2px solid #eee; -} - -.edit-meeting-schedule .edit-grid .timeline { +.edit-meeting-schedule .edit-grid .timeslots { position: relative; - height: 1.6em; + height: 4.5em; + padding-bottom: 0.15em; } -.edit-meeting-schedule .edit-grid .timeline > div { - position: absolute; -} - -.edit-meeting-schedule .edit-grid .timeline.timeslots { - height: 3.3em; -} - -.edit-meeting-schedule .edit-grid .timeline .time-label { - font-size: smaller; - border-left: 2px solid #eee; - border-right: 2px solid #eee; - padding: 0 0.2em; - height: 1.3em; -} - -.edit-meeting-schedule .edit-grid .timeline .time-label.text-left { - border-right: none; -} - -.edit-meeting-schedule .edit-grid .timeline .time-label.text-right { - border-left: none; -} - -.edit-meeting-schedule .timeslot { - display: flex; - flex-direction: row; - background-color: #eee; +.edit-meeting-schedule .edit-grid .timeslot { + position: relative; + display: inline-block; + background-color: #f4f4f4; height: 100%; - border-bottom: 0.15em solid #fff; overflow: hidden; } -.edit-meeting-schedule .timeslot.dropping { +.edit-meeting-schedule .edit-grid .timeslot .time-label { + display: flex; + position: absolute; + height: 100%; + width: 100%; + align-items: center; + justify-content: center; + color: #999; +} + +.edit-meeting-schedule .edit-grid .timeslot .drop-target { + position: relative; /* this is merely to make sure we are positioned above the time labels */ + display: flex; + flex-direction: row; + height: 100%; +} + +.edit-meeting-schedule .edit-grid .timeslot.dropping { background-color: #ccc; transition: background-color 0.2s; } -.edit-meeting-schedule .timeslot.overfull { +.edit-meeting-schedule .edit-grid .timeslot.overfull { border-right: 2px dashed #fff; /* cut-off illusion */ } -.edit-meeting-schedule .timeslot.would-violate-hint { +.edit-meeting-schedule .edit-grid .timeslot.would-violate-hint { background-color: #ffe0e0; } @@ -1111,7 +1096,6 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container { .edit-meeting-schedule .formatted-constraints .encircled { font-size: smaller; - cursor: help; } /* sessions */ @@ -1139,7 +1123,7 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container { } .edit-meeting-schedule .session.dragging { - opacity: 0.3; + opacity: 0.1; transition: opacity 0.4s; } @@ -1148,6 +1132,11 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container { margin-right: 0; } +.edit-meeting-schedule .edit-grid, .edit-meeting-schedule .session { + font-family: arial, helvetica, sans-serif; + font-size: 11px; +} + .edit-meeting-schedule .session .session-label { flex-grow: 1; margin-left: 0.1em; @@ -1208,6 +1197,7 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container { left: 0; width: 100%; border-top: 0.2em solid #ccc; + margin-bottom: 2em; background-color: #fff; opacity: 0.95; } @@ -1217,14 +1207,11 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container { } .edit-meeting-schedule .unassigned-sessions { - display: flex; - flex-wrap: wrap; - align-items: flex-start; margin-top: 0.5em; min-height: 4em; max-height: 13em; overflow-y: auto; - background-color: #eee; + background-color: #f4f4f4; } .edit-meeting-schedule .unassigned-sessions.dropping { @@ -1232,6 +1219,12 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container { transition: background-color 0.2s; } +.edit-meeting-schedule .unassigned-sessions .drop-target { + display: flex; + flex-wrap: wrap; + align-items: flex-start; +} + .edit-meeting-schedule .scheduling-panel .preferences { margin: 0.5em 0; } @@ -1245,6 +1238,21 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container { display: inline-block; } +.edit-meeting-schedule #timeslot-group-toggles-modal .modal-body { + /*column-count: 3;*/ + display: flex; + flex-flow: row wrap; +} + +.edit-meeting-schedule #timeslot-group-toggles-modal .modal-body > * { + margin-right: 1.5em; +} + +.edit-meeting-schedule #timeslot-group-toggles-modal .modal-body label { + display: block; + font-weight: normal; +} + .edit-meeting-schedule .session-parent-toggles { margin-top: 1em; } @@ -1259,8 +1267,9 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container { .edit-meeting-schedule .scheduling-panel .session-info-container { padding-left: 0.5em; - flex: 0 0 20em; - max-height: 15em; + flex: 0 0 25em; + max-height: 25em; + font-size: 14px; overflow-y: auto; } diff --git a/ietf/static/ietf/js/edit-meeting-schedule.js b/ietf/static/ietf/js/edit-meeting-schedule.js index 8a8803ccd..de378cf06 100644 --- a/ietf/static/ietf/js/edit-meeting-schedule.js +++ b/ietf/static/ietf/js/edit-meeting-schedule.js @@ -10,6 +10,7 @@ jQuery(document).ready(function () { let sessions = content.find(".session"); let timeslots = content.find(".timeslot"); + let days = content.find(".day-flow .day"); // hack to work around lack of position sticky support in old browsers, see https://caniuse.com/#feat=css-sticky if (content.find(".scheduling-panel").css("position") != "sticky") { @@ -43,7 +44,11 @@ jQuery(document).ready(function () { sessions.not(element).removeClass("selected"); jQuery(element).addClass("selected"); showConstraintHints(element.id.slice("session".length)); - content.find(".scheduling-panel .session-info-container").html(jQuery(element).find(".session-info").html()).find('[data-original-title]').tooltip(); + let sessionInfoContainer = content.find(".scheduling-panel .session-info-container"); + sessionInfoContainer.html(jQuery(element).find(".session-info").html()); + sessionInfoContainer.find('[data-original-title]').tooltip(); + let time = jQuery(element).closest(".timeslot").find(".time-label").text() || ""; + sessionInfoContainer.find('.time').text(time.replace(new RegExp(" ", "g"), "")); } else { sessions.removeClass("selected"); @@ -107,13 +112,13 @@ jQuery(document).ready(function () { sessions.prop('draggable', true); // dropping - let dropElements = content.find(".timeslot,.unassigned-sessions"); + let dropElements = content.find(".timeslot .drop-target,.unassigned-sessions .drop-target"); dropElements.on('dragenter', function (event) { if ((event.originalEvent.dataTransfer.getData("text/plain") || "").slice(0, "session".length) != "session") return; event.preventDefault(); // default action is signalling that this is not a valid target - jQuery(this).addClass("dropping"); + jQuery(this).parent().addClass("dropping"); }); dropElements.on('dragover', function (event) { @@ -128,11 +133,11 @@ jQuery(document).ready(function () { if (event.originalEvent.currentTarget.contains(event.originalEvent.relatedTarget)) return; - jQuery(this).removeClass("dropping"); + jQuery(this).parent().removeClass("dropping"); }); dropElements.on('drop', function (event) { - jQuery(this).removeClass("dropping"); + jQuery(this).parent().removeClass("dropping"); let sessionId = event.originalEvent.dataTransfer.getData("text/plain"); if ((event.originalEvent.dataTransfer.getData("text/plain") || "").slice(0, "session".length) != "session") @@ -148,6 +153,7 @@ jQuery(document).ready(function () { return; let dropElement = jQuery(this); + let dropParent = dropElement.parent(); function done(response) { if (response != "OK") { @@ -157,11 +163,11 @@ jQuery(document).ready(function () { dropElement.append(sessionElement); // move element updateCurrentSchedulingHints(); - if (dropElement.hasClass("unassigned-sessions")) + if (dropParent.hasClass("unassigned-sessions")) sortUnassigned(); } - if (dropElement.hasClass("unassigned-sessions")) { + if (dropParent.hasClass("unassigned-sessions")) { jQuery.ajax({ url: ietfData.urls.assign, method: "post", @@ -179,7 +185,7 @@ jQuery(document).ready(function () { data: { action: "assign", session: sessionId.slice("session".length), - timeslot: dropElement.attr("id").slice("timeslot".length) + timeslot: dropParent.attr("id").slice("timeslot".length) }, timeout: 5 * 1000 }).fail(failHandler).done(done); @@ -348,7 +354,7 @@ jQuery(document).ready(function () { sortUnassigned(); - // toggling of sessions + // toggling visible sessions by session parents let sessionParentInputs = content.find(".session-parent-toggles input"); function updateSessionParentToggling() { @@ -362,7 +368,25 @@ jQuery(document).ready(function () { } sessionParentInputs.on("click", updateSessionParentToggling); - updateSessionParentToggling(); + + // toggling visible timeslots + let timeslotGroupInputs = content.find("#timeslot-group-toggles-modal .modal-body input"); + function updateTimeslotGroupToggling() { + let checked = []; + timeslotGroupInputs.filter(":checked").each(function () { + checked.push("." + this.value); + }); + + timeslots.filter(checked.join(",")).removeClass("hidden"); + timeslots.not(checked.join(",")).addClass("hidden"); + + days.each(function () { + jQuery(this).toggle(jQuery(this).find(".timeslot:not(.hidden)").length > 0); + }); + } + + timeslotGroupInputs.on("click change", updateTimeslotGroupToggling); + updateTimeslotGroupToggling(); }); diff --git a/ietf/templates/meeting/edit_meeting_schedule.html b/ietf/templates/meeting/edit_meeting_schedule.html index c17180651..4deb554d8 100644 --- a/ietf/templates/meeting/edit_meeting_schedule.html +++ b/ietf/templates/meeting/edit_meeting_schedule.html @@ -58,10 +58,8 @@   -
- {% for room in labels %} -
+
{{ room.name }}
{% if room.capacity %}{{ room.capacity }} {% endif %} @@ -74,26 +72,26 @@
{% for day in days %} -
+
{{ day.day|date:"l" }}
{{ day.day|date:"N j, Y" }}
-
- {% for t, left_or_right, offset in day.time_labels %} -
{{ t|date:"H:i" }}
- {% endfor %} -
- {% for room, timeslots in day.room_timeslots %} -
+
{% for t in timeslots %} -
- {% for assignment, session in t.timeslot.session_assignments %} +
+
+ {{ t.time|date:"G:i" }} - {{ t.end_time|date:"G:i" }} +
+ +
+ {% for assignment, session in t.session_assignments %} {% include "meeting/edit_meeting_schedule_session.html" %} {% endfor %} +
{% endfor %}
@@ -106,9 +104,11 @@
- {% for session in unassigned_sessions %} - {% include "meeting/edit_meeting_schedule_session.html" %} - {% endfor %} +
+ {% for session in unassigned_sessions %} + {% include "meeting/edit_meeting_schedule_session.html" %} + {% endfor %} +
@@ -128,11 +128,44 @@ {% endfor %} + + + +
+ +
{% endblock %} diff --git a/ietf/templates/meeting/edit_meeting_schedule_session.html b/ietf/templates/meeting/edit_meeting_schedule_session.html index 3d8f055b7..07be70c81 100644 --- a/ietf/templates/meeting/edit_meeting_schedule_session.html +++ b/ietf/templates/meeting/edit_meeting_schedule_session.html @@ -4,8 +4,10 @@
+ {{ session.requested_duration_in_hours|floatformat }}h + {% if session.attendees != None %} - {{ session.attendees }} + · {{ session.attendees }} {% endif %} {% if session.comments %} @@ -23,12 +25,15 @@ {# the JS uses this to display session information in the bottom panel #}
- +
+ + + {{ session.scheduling_label }} + · {{ session.requested_duration_in_hours }}h + {% if session.group %}· {% if session.group.is_bof %}BoF{% else %}{{ session.group.type.name }}{% endif %}{% endif %} + {% if session.attendees != None %}· {{ session.attendees }} {% endif %} + +
{% if session.group %}
@@ -42,7 +47,7 @@ {% if session.requested_by_person %}
- {{ session.requested_by_person.plain_name }} {% if session.requested_time %}({{ session.requested_time|date:"Y-m-d" }}){% endif %} + {{ session.requested_by_person.plain_name }} {% if session.requested_time %}({{ session.requested_time|date:"Y-m-d" }}){% endif %}
{% endif %}