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:
assignments_by_session[a.session_id].append(a)
# 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.
# Prepare timeslot layout, making a timeline per day scaled in
# browser em units to ensure that everything lines up even if the
# timeslots are not the same in the different rooms
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
# time labels column
timeslots_by_day = defaultdict(list)
for t in timeslots_qs:
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 = []
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 = []
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:
day_labels = []
day_width = timedelta_to_css_ems(day_max_time - day_min_time)
label_width = 4 # em
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)
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)
if not day_labels:
day_labels.append((day_min_time, 'top', 0, 'left'))
day_labels.append((day_min_time, 'left', 0))
time_labels.append({
'day': day,
'height': timedelta_to_css_ems(day_max_time - day_min_time),
'labels': day_labels,
})
room_timeslots = []
for r in rooms:
if r.pk not in room_has_timeslots:
continue
# 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 = []
timeslots = []
for t in timeslots_by_room_and_day.get((r.pk, day), []):
day_timeslots.append({
timeslots.append({
'timeslot': t,
'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({
'day': day,
'timeslots': day_timeslots,
'height': timedelta_to_css_ems(day_max_time - day_min_time),
})
room_timeslots.append((r, timeslots))
if any(d['timeslots'] for d in room_days):
room_columns.append({
'room': r,
'days': room_days,
})
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))]
# prepare sessions
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
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.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,
'can_edit': can_edit,
'js_data': json.dumps(js_data, indent=2),
'time_labels': time_labels,
'rooms': rooms,
'room_columns': room_columns,
'days': days,
'room_labels': room_labels,
'unassigned_sessions': unassigned_sessions,
'session_parents': session_parents,
'hide_menu': True,

View file

@ -974,7 +974,7 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
.fc-button {
/* 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============================================= */
@ -987,62 +987,77 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
/* === Edit Meeting Schedule ====================================== */
.edit-meeting-schedule .edit-grid {
position: relative;
display: flex;
}
.edit-meeting-schedule .schedule-column .room-name {
height: 2em;
font-weight: bold;
text-align: center;
margin: 0;
white-space: nowrap;
.edit-meeting-schedule .edit-grid .room-label-column {
/* make sure we cut this column off - the time slots will determine
how much of it is shown */
position: absolute;
top: 0;
bottom: 0;
left: 0;
overflow: hidden;
width: 8em;
}
.edit-meeting-schedule .schedule-column .day-label {
height: 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;
.edit-meeting-schedule .edit-grid .day {
margin-right: 2.5em;
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;
}
.edit-meeting-schedule .time-labels-column > div {
min-width: 5em;
padding-right: 0.5em;
.edit-meeting-schedule .edit-grid .timeline.timeslots {
height: 3.3em;
}
.edit-meeting-schedule .time-labels-column .time-label.top-aligned {
border-top: 1px solid #ccc;
.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 .time-labels-column .time-label.text-left span {
background-color: #fff;
padding-right: 0.2em;
.edit-meeting-schedule .edit-grid .timeline .time-label.text-left {
border-right: none;
}
.edit-meeting-schedule .time-labels-column .time-label.bottom-aligned {
border-bottom: 1px solid #ccc;
}
.edit-meeting-schedule .room-column {
flex-grow: 1;
.edit-meeting-schedule .edit-grid .timeline .time-label.text-right {
border-left: none;
}
.edit-meeting-schedule .timeslot {
display: flex;
flex-direction: column;
flex-direction: row;
background-color: #eee;
width: 100%;
border-left: 0.15em solid #fff;
height: 100%;
border-bottom: 0.15em solid #fff;
overflow: hidden;
}
@ -1052,99 +1067,23 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
}
.edit-meeting-schedule .timeslot.overfull {
border-bottom: 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;
border-right: 2px dashed #fff; /* cut-off illusion */
}
/* sessions */
.edit-meeting-schedule .session {
display: flex;
background-color: #fff;
padding: 0 0.2em;
padding-left: 0.5em;
margin: 0.2em;
padding-right: 0.2em;
padding-left: 0.5em;
line-height: 1.3em;
border-radius: 0.4em;
overflow: hidden;
cursor: pointer;
}
.edit-meeting-schedule .session.selected {
border: 1px solid #bbb;
background-color: #fcfcfc;
}
.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 {
border-radius: 0.4em 0.4em 0 0; /* remove bottom rounding to illude to being cut off */
margin-bottom: 0;
}
.edit-meeting-schedule .unassigned-sessions .session {
min-width: 6em;
margin-right: 0.3em;
border-radius: 0.4em 0 0 0.4em; /* remove bottom rounding to illude to being cut off */
margin-right: 0;
}
.edit-meeting-schedule .session .session-label {
@ -1208,12 +1142,75 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
display: none;
}
.edit-meeting-schedule .session .comments {
font-size: smaller;
margin-right: 0.1em;
}
.edit-meeting-schedule .session .session-info {
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 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
function selectSessionElement(element) {
if (element) {
@ -220,7 +226,7 @@ jQuery(document).ready(function () {
function updateAttendeesViolations() {
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)
jQuery(this).toggleClass("too-many-attendees", +this.dataset.attendees > +roomCapacity);
});

View file

@ -48,43 +48,59 @@
</p>
<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">
<div class="room-name"></div>
{# using the same markup in both room labels and the actual days ensures they are aligned #}
<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="day-label">{{ d.day|date:"l, F j, Y" }}</div>
<div class="timeline"></div>
<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>
{% for room in labels %}
<div class="timeline timeslots">
<div class="room-name">
<strong>{{ room.name }}</strong><br>
{% if room.capacity %}{{ room.capacity }} <i class="fa fa-user-o"></i>{% endif %}
</div>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
{% for r in room_columns %}
<div class="room-column schedule-column" data-roomcapacity="{{ r.room.capacity }}">
<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-flow">
{% for day in days %}
<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="day-label"></div> {# for spacing purposes #}
<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>
<div class="timeline">
{% 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>
{% endfor %}
</div>
{% endfor %}
</div>
{% endfor %}
{% for room, timeslots in day.room_timeslots %}
<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 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">
{{ session.scheduling_label }}
</div>
{% if session.constrained_sessions %}
<div class="constraints">
{% for explanation, sessions in session.constrained_sessions %}
<span data-sessions="{{ sessions|join:"," }}">{{ explanation }}</span>
{% endfor %}
</div>
{% endif %}
<div>
{% if session.attendees != None %}
<span class="attendees">{{ session.attendees }}</span>
{% endif %}
{% if session.comments %}
<div class="comments"><i class="fa fa-comment-o"></i></div>
{% endif %}
{% if session.attendees != None %}
<div class="attendees">{{ session.attendees }}</div>
{% endif %}
{% if session.comments %}
<span class="comments"><i class="fa fa-comment-o"></i></span>
{% 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">
<label>
{{ session.scheduling_label }}