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:
parent
393ee64bec
commit
5faccf5379
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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> </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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue