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
This commit is contained in:
Ole Laursen 2020-03-09 19:40:31 +00:00
parent 393ee64bec
commit 5faccf5379
6 changed files with 293 additions and 98 deletions

View file

@ -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:

View file

@ -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,
})

View file

@ -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;
}

View file

@ -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();
});

View file

@ -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 %}
</p>
<table class="table edit-meeting-grid">
<thead>
<tr>
<th></th>
{% for r in rooms %}
<th>{{ r.name }}{% if r.capacity %} - {{ r.capacity }} <i class="fa fa-user-o"></i>{% endif %}</th>
{% endfor %}
</tr>
</thead>
<div class="edit-grid">
{# note: in order for all this to align properly, make sure there's the same markup in all columns #}
<tbody>
{% for start_time, end_time, hours, room_timeslots in timeslot_matrix %}
{% ifchanged %}
<tr>
<td class="day" colspan="1000">
<strong>{{ start_time|date:"l, F j, Y" }}</strong>
</td>
</tr>
{% endifchanged %}
<div class="time-labels-column schedule-column">
<h5>&nbsp;</h5>
<tr>
<td>
<div class="period" style="min-height:{{ hours }}em;">{{ start_time|date:"G:i" }}-{{ end_time|date:"G:i" }}</div>
</td>
{% for d in time_labels %}
<div class="day-label">
<strong>{{ d.day|date:"D" }}</strong><br>
<i>{{ d.day|date:"Y-m-d" }}</i>
</div>
{% for r, timeslot in room_timeslots %}
<td class="timeslot {% if not timeslot %}disabled{% endif %}" {% if timeslot %}data-timeslot="{{ timeslot.pk }}"{% endif %} style="width:{{ timeslot_width }}%">
{% for assignment, session in timeslot.session_assignments %}
{% include "meeting/edit_meeting_schedule_session.html" %}
{% endfor %}
</td>
<div class="day" style="height: {{ d.height }}em;">
{% for t, vertical_alignment, vertical_offset, horizontal_alignment in d.labels %}
<div class="time-label {{ vertical_alignment }}-aligned text-{{ horizontal_alignment }}" style="{{ vertical_alignment }}: {{ vertical_offset }}em;">
<span>{{ t|date:"H:i" }}</span>
</div>
{% endfor %}
</tr>
</div>
{% endfor %}
</tbody>
</table>
</div>
{% for r in room_columns %}
<div class="room-column schedule-column">
<h5>{{ r.room.name }}{% if r.room.capacity %} ({{ r.room.capacity }} <i class="fa fa-user-o"></i>{% endif %})</h5>
{% for d in r.days %}
<div class="day-label">
<strong>{{ d.day|date:"D" }}</strong><br>
<i>{{ d.day|date:"Y-m-d" }}</i>
</div>
<div class="day" style="height: {{ d.height }}em;">
{% for t in d.timeslots %}
<div class="timeslot" data-timeslot="{{ t.timeslot.pk }}" data-duration="{{ t.timeslot.duration.total_seconds }}" style="top: {{ t.offset }}em; height: {{ t.height }}em;">
{% for assignment, session in t.timeslot.session_assignments %}
{% include "meeting/edit_meeting_schedule_session.html" %}
{% endfor %}
</div>
{% endfor %}
</div>
{% endfor %}
</div>
{% endfor %}
</div>
<div class="scheduling-panel">
<h4>Unscheduled</h4>
<h5>Not yet assigned</h5>
<div class="unassigned-sessions">
{% for session in unassigned_sessions %}
{% include "meeting/edit_meeting_schedule_session.html" %}
{% endfor %}
</div>
<div class="session-parent-toggles">
Show:
{% for p in session_parents %}
<label class="parent-{{ p.acronym }}"><input type="checkbox" checked value="{{ p.acronym }}"> {{ p.acronym }}</label>
{% endfor %}
</div>
</div>
</div>

View file

@ -1,3 +1,3 @@
<div id="session{{ session.pk }}" class="session" style="min-height:{{ session.scheduling_height }}em;">
<div id="session{{ session.pk }}" class="session {% if session.group.parent.scheduling_color %}toggleable {% if session.parent_acronym %}parent-{{ session.parent_acronym }}{% endif %}{% endif %}" style="height:{{ session.layout_height }}em;" data-duration="{{ session.requested_duration.total_seconds }}">
{{ session.scheduling_label }} {% if session.comments %}<i class="fa fa-comment-o"></i>{% endif %}
</div>