Swap the axes in the meeting schedule editor and rework it to allow

flowing the days.

Add JS workaround for missing position sticky support, instead of the
CSS workaround which added an annoying padding for everyone.
 - Legacy-Id: 17616
This commit is contained in:
Ole Laursen 2020-04-09 18:16:56 +00:00
parent b8b1b67e6d
commit 6c48575042
5 changed files with 234 additions and 213 deletions

View file

@ -508,17 +508,14 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
for a in assignments: for a in assignments:
assignments_by_session[a.session_id].append(a) assignments_by_session[a.session_id].append(a)
# Prepare timeslot layout. We arrange time slots in columns per # Prepare timeslot layout, making a timeline per day scaled in
# room where everything inside is grouped by day. Things inside # browser em units to ensure that everything lines up even if the
# the days are then layouted proportionally to the actual time of # timeslots are not the same in the different rooms
# day, to ensure that everything lines up, even if the time slots
# are not the same in the different rooms.
def timedelta_to_css_ems(timedelta): def timedelta_to_css_ems(timedelta):
css_ems_per_hour = 1.8 css_ems_per_hour = 5
return timedelta.seconds / 60.0 / 60.0 * css_ems_per_hour return timedelta.seconds / 60.0 / 60.0 * css_ems_per_hour
# time labels column
timeslots_by_day = defaultdict(list) timeslots_by_day = defaultdict(list)
for t in timeslots_qs: for t in timeslots_qs:
timeslots_by_day[t.time.date()].append(t) timeslots_by_day[t.time.date()].append(t)
@ -526,10 +523,19 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
day_min_max = [] day_min_max = []
for day, timeslots in sorted(timeslots_by_day.iteritems()): 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))) day_min_max.append((day, min(t.time for t in timeslots), max(t.end_time() for t in timeslots)))
time_labels = [] timeslots_by_room_and_day = defaultdict(list)
room_has_timeslots = set()
for t in timeslots_qs:
room_has_timeslots.add(t.location_id)
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: for day, day_min_time, day_max_time in day_min_max:
day_labels = [] day_labels = []
day_width = timedelta_to_css_ems(day_max_time - day_min_time)
label_width = 4 # em
hourly_delta = 2 hourly_delta = 2
@ -540,47 +546,42 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
end = day_max_time.replace(hour=last_hour, minute=0, second=0, microsecond=0) end = day_max_time.replace(hour=last_hour, minute=0, second=0, microsecond=0)
while t <= end: while t <= end:
day_labels.append((t, 'top', timedelta_to_css_ems(t - day_min_time), 'left')) 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) t += datetime.timedelta(seconds=hourly_delta * 60 * 60)
if not day_labels: if not day_labels:
day_labels.append((day_min_time, 'top', 0, 'left')) day_labels.append((day_min_time, 'left', 0))
time_labels.append({ room_timeslots = []
'day': day, for r in rooms:
'height': timedelta_to_css_ems(day_max_time - day_min_time), if r.pk not in room_has_timeslots:
'labels': day_labels, continue
})
# room columns timeslots = []
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), []): for t in timeslots_by_room_and_day.get((r.pk, day), []):
day_timeslots.append({ timeslots.append({
'timeslot': t, 'timeslot': t,
'offset': timedelta_to_css_ems(t.time - day_min_time), 'offset': timedelta_to_css_ems(t.time - day_min_time),
'height': timedelta_to_css_ems(t.end_time() - t.time), 'width': timedelta_to_css_ems(t.end_time() - t.time),
}) })
room_days.append({ room_timeslots.append((r, timeslots))
'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): days.append({
room_columns.append({ 'day': day,
'room': r, 'width': day_width,
'days': room_days, '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))]
# prepare sessions # prepare sessions
for ts in timeslots_qs: for ts in timeslots_qs:
@ -701,7 +702,7 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
s.requested_duration_in_hours = s.requested_duration.seconds / 60.0 / 60.0 s.requested_duration_in_hours = s.requested_duration.seconds / 60.0 / 60.0
session_layout_margin = 0.2 session_layout_margin = 0.2
s.layout_height = timedelta_to_css_ems(s.requested_duration) - 2 * session_layout_margin s.layout_width = timedelta_to_css_ems(s.requested_duration) - 2 * session_layout_margin
s.parent_acronym = s.group.parent.acronym if s.group and s.group.parent else "" s.parent_acronym = s.group.parent.acronym if s.group and s.group.parent else ""
s.historic_group_ad_name = ad_names.get(s.group_id) s.historic_group_ad_name = ad_names.get(s.group_id)
@ -744,9 +745,8 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
'schedule': schedule, 'schedule': schedule,
'can_edit': can_edit, 'can_edit': can_edit,
'js_data': json.dumps(js_data, indent=2), 'js_data': json.dumps(js_data, indent=2),
'time_labels': time_labels, 'days': days,
'rooms': rooms, 'room_labels': room_labels,
'room_columns': room_columns,
'unassigned_sessions': unassigned_sessions, 'unassigned_sessions': unassigned_sessions,
'session_parents': session_parents, 'session_parents': session_parents,
'hide_menu': True, 'hide_menu': True,

View file

@ -974,7 +974,7 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
.fc-button { .fc-button {
/* same as button-primary */ /* same as button-primary */
background-image: linear-gradient(rgb(107, 91, 173) 0px, rgb(80, 68, 135) 100%) background-image: linear-gradient(rgb(107, 91, 173) 0px, rgb(80, 68, 135) 100%);
} }
/* === Edit Milestones============================================= */ /* === Edit Milestones============================================= */
@ -987,62 +987,77 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
/* === Edit Meeting Schedule ====================================== */ /* === Edit Meeting Schedule ====================================== */
.edit-meeting-schedule .edit-grid { .edit-meeting-schedule .edit-grid {
position: relative;
display: flex; display: flex;
} }
.edit-meeting-schedule .schedule-column .room-name { .edit-meeting-schedule .edit-grid .room-label-column {
height: 2em; /* make sure we cut this column off - the time slots will determine
font-weight: bold; how much of it is shown */
text-align: center; position: absolute;
margin: 0; top: 0;
white-space: nowrap; bottom: 0;
left: 0;
overflow: hidden;
width: 8em;
} }
.edit-meeting-schedule .schedule-column .day-label { .edit-meeting-schedule .edit-grid .day {
height: 2.5em; margin-right: 2.5em;
max-width: 5em; /* let it stick out and overlap the other columns */
white-space: nowrap;
font-style: italic;
margin-top: 1em;
}
.edit-meeting-schedule .schedule-column > .day {
position: relative;
margin-bottom: 2em; margin-bottom: 2em;
} }
.edit-meeting-schedule .schedule-column > .day > div { .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;
}
.edit-meeting-schedule .edit-grid .day-flow .day-label {
border-bottom: 2px solid #eee;
}
.edit-meeting-schedule .edit-grid .timeline {
position: relative;
height: 1.6em;
}
.edit-meeting-schedule .edit-grid .timeline > div {
position: absolute; position: absolute;
} }
.edit-meeting-schedule .time-labels-column > div { .edit-meeting-schedule .edit-grid .timeline.timeslots {
min-width: 5em; height: 3.3em;
padding-right: 0.5em;
} }
.edit-meeting-schedule .time-labels-column .time-label.top-aligned { .edit-meeting-schedule .edit-grid .timeline .time-label {
border-top: 1px solid #ccc; font-size: smaller;
border-left: 2px solid #eee;
border-right: 2px solid #eee;
padding: 0 0.2em;
height: 1.3em;
} }
.edit-meeting-schedule .time-labels-column .time-label.text-left span { .edit-meeting-schedule .edit-grid .timeline .time-label.text-left {
background-color: #fff; border-right: none;
padding-right: 0.2em;
} }
.edit-meeting-schedule .time-labels-column .time-label.bottom-aligned { .edit-meeting-schedule .edit-grid .timeline .time-label.text-right {
border-bottom: 1px solid #ccc; border-left: none;
}
.edit-meeting-schedule .room-column {
flex-grow: 1;
} }
.edit-meeting-schedule .timeslot { .edit-meeting-schedule .timeslot {
display: flex; display: flex;
flex-direction: column; flex-direction: row;
background-color: #eee; background-color: #eee;
width: 100%; height: 100%;
border-left: 0.15em solid #fff; border-bottom: 0.15em solid #fff;
overflow: hidden; overflow: hidden;
} }
@ -1052,99 +1067,23 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
} }
.edit-meeting-schedule .timeslot.overfull { .edit-meeting-schedule .timeslot.overfull {
border-bottom: 2px dashed #fff; /* cut-off illusion */ border-right: 2px dashed #fff; /* cut-off illusion */
}
.edit-meeting-schedule {
/* this is backwards-compatible measure - if the browser doesn't
support position: sticky but only position: fixed, we ensure there's room for the scheduling
panel */
padding-bottom: 5em;
}
.edit-meeting-schedule .scheduling-panel {
position: fixed; /* backwards compatibility */
z-index: 1;
position: sticky;
display: flex;
bottom: 0;
left: 0;
width: 100%;
border-top: 0.2em solid #ccc;
background-color: #fff;
opacity: 0.95;
}
.edit-meeting-schedule .scheduling-panel .unassigned-container {
flex-grow: 1;
}
.edit-meeting-schedule .unassigned-sessions {
min-height: 4em;
max-height: 13em;
overflow-y: auto;
display: flex;
flex-wrap: wrap;
background-color: #eee;
margin-top: 0.5em;
}
.edit-meeting-schedule .unassigned-sessions.dropping {
background-color: #e5e5e5;
transition: background-color 0.2s;
}
.edit-meeting-schedule .scheduling-panel .preferences {
margin: 0.5em 0;
}
.edit-meeting-schedule .scheduling-panel .preferences > span {
margin-right: 1em;
}
.edit-meeting-schedule .sort-unassigned select {
width: auto;
display: inline-block;
}
.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;
}
.edit-meeting-schedule .scheduling-panel .session-info-container {
padding-left: 0.5em;
flex: 0 0 20em;
max-height: 15em;
overflow-y: auto;
}
.edit-meeting-schedule .scheduling-panel .session-info-container .comments {
font-style: italic;
} }
/* sessions */ /* sessions */
.edit-meeting-schedule .session { .edit-meeting-schedule .session {
display: flex;
background-color: #fff; background-color: #fff;
padding: 0 0.2em;
padding-left: 0.5em;
margin: 0.2em; margin: 0.2em;
padding-right: 0.2em;
padding-left: 0.5em;
line-height: 1.3em;
border-radius: 0.4em; border-radius: 0.4em;
overflow: hidden; overflow: hidden;
cursor: pointer; cursor: pointer;
} }
.edit-meeting-schedule .session.selected { .edit-meeting-schedule .session.selected {
border: 1px solid #bbb; background-color: #fcfcfc;
} }
.edit-meeting-schedule .session.dragging { .edit-meeting-schedule .session.dragging {
@ -1153,13 +1092,8 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
} }
.edit-meeting-schedule .timeslot.overfull .session { .edit-meeting-schedule .timeslot.overfull .session {
border-radius: 0.4em 0.4em 0 0; /* remove bottom rounding to illude to being cut off */ border-radius: 0.4em 0 0 0.4em; /* remove bottom rounding to illude to being cut off */
margin-bottom: 0; margin-right: 0;
}
.edit-meeting-schedule .unassigned-sessions .session {
min-width: 6em;
margin-right: 0.3em;
} }
.edit-meeting-schedule .session .session-label { .edit-meeting-schedule .session .session-label {
@ -1208,12 +1142,75 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
display: none; display: none;
} }
.edit-meeting-schedule .session .comments {
font-size: smaller;
margin-right: 0.1em;
}
.edit-meeting-schedule .session .session-info { .edit-meeting-schedule .session .session-info {
display: none; display: none;
} }
/* scheduling panel */
.edit-meeting-schedule .scheduling-panel {
position: sticky;
display: flex;
bottom: 0;
left: 0;
width: 100%;
border-top: 0.2em solid #ccc;
background-color: #fff;
opacity: 0.95;
}
.edit-meeting-schedule .scheduling-panel .unassigned-container {
flex-grow: 1;
}
.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;
}
.edit-meeting-schedule .unassigned-sessions.dropping {
background-color: #e5e5e5;
transition: background-color 0.2s;
}
.edit-meeting-schedule .scheduling-panel .preferences {
margin: 0.5em 0;
}
.edit-meeting-schedule .scheduling-panel .preferences > span {
margin-right: 1em;
}
.edit-meeting-schedule .sort-unassigned select {
width: auto;
display: inline-block;
}
.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;
}
.edit-meeting-schedule .scheduling-panel .session-info-container {
padding-left: 0.5em;
flex: 0 0 20em;
max-height: 15em;
overflow-y: auto;
}
.edit-meeting-schedule .scheduling-panel .session-info-container .comments {
font-style: italic;
}

View file

@ -11,6 +11,12 @@ jQuery(document).ready(function () {
let sessions = content.find(".session"); let sessions = content.find(".session");
let timeslots = content.find(".timeslot"); let timeslots = content.find(".timeslot");
// 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") {
content.find(".scheduling-panel").css("position", "fixed");
content.css("padding-bottom", "14em");
}
// selecting // selecting
function selectSessionElement(element) { function selectSessionElement(element) {
if (element) { if (element) {
@ -220,7 +226,7 @@ jQuery(document).ready(function () {
function updateAttendeesViolations() { function updateAttendeesViolations() {
sessions.each(function () { sessions.each(function () {
let roomCapacity = jQuery(this).closest(".room-column").data("roomcapacity"); let roomCapacity = jQuery(this).closest(".timeline").data("roomcapacity");
if (roomCapacity && this.dataset.attendees) if (roomCapacity && this.dataset.attendees)
jQuery(this).toggleClass("too-many-attendees", +this.dataset.attendees > +roomCapacity); jQuery(this).toggleClass("too-many-attendees", +this.dataset.attendees > +roomCapacity);
}); });

View file

@ -48,43 +48,59 @@
</p> </p>
<div class="edit-grid"> <div class="edit-grid">
{# in order for all this to align properly vertically, we have the same structure in all columns #}
<div class="time-labels-column schedule-column"> {# using the same markup in both room labels and the actual days ensures they are aligned #}
<div class="room-name"></div> <div class="room-label-column">
{% for labels in room_labels %}
<div class="day">
<div class="day-label">
<strong>&nbsp;</strong><br>
&nbsp;
</div>
{% for d in time_labels %} <div class="timeline"></div>
<div class="day-label">{{ d.day|date:"l, F j, Y" }}</div>
<div class="day" style="height: {{ d.height }}em;"> {% for room in labels %}
{% for t, vertical_alignment, vertical_offset, horizontal_alignment in d.labels %} <div class="timeline timeslots">
<div class="time-label {{ vertical_alignment }}-aligned text-{{ horizontal_alignment }}" style="{{ vertical_alignment }}: {{ vertical_offset }}em;"> <div class="room-name">
<span>{{ t|date:"H:i" }}</span> <strong>{{ room.name }}</strong><br>
{% if room.capacity %}{{ room.capacity }} <i class="fa fa-user-o"></i>{% endif %}
</div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% for r in room_columns %} <div class="day-flow">
<div class="room-column schedule-column" data-roomcapacity="{{ r.room.capacity }}"> {% for day in days %}
<div class="room-name">{{ r.room.name }}{% if r.room.capacity %} ({{ r.room.capacity }} <i class="fa fa-user-o"></i>{% endif %})</div> <div class="day" style="width: {{ day.width }}em;">
<div class="day-label">
<strong>{{ day.day|date:"l" }}</strong><br>
{{ day.day|date:"N j, Y" }}
</div>
{% for d in r.days %} <div class="timeline">
<div class="day-label"></div> {# for spacing purposes #} {% for t, left_or_right, offset in day.time_labels %}
<div class="time-label text-{{ left_or_right }}" style="{{ left_or_right }}: {{ offset }}em;">{{ t|date:"H:i" }}</div>
<div class="day" style="height: {{ d.height }}em;">
{% for t in d.timeslots %}
<div id="timeslot{{ t.timeslot.pk }}" class="timeslot" data-start="{{ t.timeslot.time.isoformat }}" data-end="{{ t.timeslot.end_time.isoformat }}" 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 %} {% endfor %}
</div> </div>
{% endfor %}
</div> {% for room, timeslots in day.room_timeslots %}
{% endfor %} <div class="timeline timeslots" data-roomcapacity="{{ room.capacity }}">
{% for t in timeslots %}
<div id="timeslot{{ t.timeslot.pk }}" class="timeslot" data-start="{{ t.timeslot.time.isoformat }}" data-end="{{ t.timeslot.end_time.isoformat }}" data-duration="{{ t.timeslot.duration.total_seconds }}" style="left: {{ t.offset }}em; width: {{ t.width }}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> </div>
<div class="scheduling-panel"> <div class="scheduling-panel">

View file

@ -1,25 +1,27 @@
<div id="session{{ session.pk }}" class="session {% if not session.group.parent.scheduling_color %}untoggleable{% endif %} {% if session.parent_acronym %}parent-{{ session.parent_acronym }}{% endif %}" style="height:{{ session.layout_height }}em;" data-duration="{{ session.requested_duration.total_seconds }}" {% if session.attendees != None %}data-attendees="{{ session.attendees }}"{% endif %}> <div id="session{{ session.pk }}" class="session {% if not session.group.parent.scheduling_color %}untoggleable{% endif %} {% if session.parent_acronym %}parent-{{ session.parent_acronym }}{% endif %}" style="width:{{ session.layout_width }}em;" data-duration="{{ session.requested_duration.total_seconds }}" {% if session.attendees != None %}data-attendees="{{ session.attendees }}"{% endif %}>
<div class="session-label"> <div class="session-label">
{{ session.scheduling_label }} {{ session.scheduling_label }}
</div> </div>
{% if session.constrained_sessions %} <div>
<div class="constraints"> {% if session.attendees != None %}
{% for explanation, sessions in session.constrained_sessions %} <span class="attendees">{{ session.attendees }}</span>
<span data-sessions="{{ sessions|join:"," }}">{{ explanation }}</span> {% endif %}
{% endfor %}
</div>
{% endif %}
{% if session.comments %} {% if session.comments %}
<div class="comments"><i class="fa fa-comment-o"></i></div> <span class="comments"><i class="fa fa-comment-o"></i></span>
{% endif %} {% endif %}
{% if session.attendees != None %}
<div class="attendees">{{ session.attendees }}</div>
{% endif %}
{% if session.constrained_sessions %}
<span class="constraints">
{% for explanation, sessions in session.constrained_sessions %}
<span data-sessions="{{ sessions|join:"," }}">{{ explanation }}</span>
{% endfor %}
</span>
{% endif %}
</div>
{# this is shown elsewhere on the page with JS - we just include it here for convenience #}
<div class="session-info"> <div class="session-info">
<label> <label>
{{ session.scheduling_label }} {{ session.scheduling_label }}