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
This commit is contained in:
Ole Laursen 2020-06-17 16:16:57 +00:00
parent 8bd9e5de6e
commit b60939a26c
5 changed files with 184 additions and 136 deletions

View file

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

View file

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

View file

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

View file

@ -58,10 +58,8 @@
&nbsp;
</div>
<div class="timeline"></div>
{% for room in labels %}
<div class="timeline timeslots">
<div class="timeslots">
<div class="room-name">
<strong>{{ room.name }}</strong><br>
{% if room.capacity %}{{ room.capacity }} <i class="fa fa-user-o"></i>{% endif %}
@ -74,26 +72,26 @@
<div class="day-flow">
{% for day in days %}
<div class="day" style="width: {{ day.width }}em;">
<div class="day">
<div class="day-label">
<strong>{{ day.day|date:"l" }}</strong><br>
{{ day.day|date:"N j, Y" }}
</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>
{% for room, timeslots in day.room_timeslots %}
<div class="timeline timeslots" data-roomcapacity="{{ room.capacity }}">
<div class="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 %}
<div id="timeslot{{ t.pk }}" class="timeslot {{ t.start_end_group }}" data-start="{{ t.time.isoformat }}" data-end="{{ t.end_time.isoformat }}" data-duration="{{ t.duration.total_seconds }}" style="width: {{ t.layout_width }}rem;">
<div class="time-label">
{{ t.time|date:"G:i" }} - {{ t.end_time|date:"G:i" }}
</div>
<div class="drop-target">
{% for assignment, session in t.session_assignments %}
{% include "meeting/edit_meeting_schedule_session.html" %}
{% endfor %}
</div>
</div>
{% endfor %}
</div>
@ -106,9 +104,11 @@
<div class="scheduling-panel">
<div class="unassigned-container">
<div class="unassigned-sessions">
{% for session in unassigned_sessions %}
{% include "meeting/edit_meeting_schedule_session.html" %}
{% endfor %}
<div class="drop-target">
{% for session in unassigned_sessions %}
{% include "meeting/edit_meeting_schedule_session.html" %}
{% endfor %}
</div>
</div>
<div class="preferences">
@ -128,11 +128,44 @@
<label class="parent-{{ p.acronym }}"><input type="checkbox" checked value="{{ p.acronym }}"> {{ p.acronym }}</label>
{% endfor %}
</span>
<span class="timeslot-group-toggles">
<button class="btn btn-default" data-toggle="modal" data-target="#timeslot-group-toggles-modal"><input type="checkbox" checked="checked" disabled> Timeslots</button>
</span>
</div>
</div>
<div class="session-info-container"></div>
</div>
<div id="timeslot-group-toggles-modal" class="modal" role="dialog" aria-labelledby="timeslot-group-toggles-modal-title">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">
<span aria-hidden="true">&times;</span>
<span class="sr-only">Close</span>
</button>
<h4 class="modal-title" id="timeslot-group-toggles-modal-title">Displayed timeslots</h4>
</div>
<div class="modal-body">
{% for day, t_groups in timeslot_groups %}
<div>
<div><strong>{{ day|date:"M. d" }}</strong></div>
{% for start, end, key in t_groups %}
<label><input type="checkbox" name="timeslot-group" value="{{ key }}" checked="checked"> {{ start|date:"H:i" }} - {{ end|date:"H:i" }}</label>
{% endfor %}
</div>
{% endfor %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -4,8 +4,10 @@
</div>
<div>
<span class="requested-duration">{{ session.requested_duration_in_hours|floatformat }}h</span>
{% if session.attendees != None %}
<span class="attendees">{{ session.attendees }}</span>
<span class="attendees">&middot; {{ session.attendees }}</span>
{% endif %}
{% if session.comments %}
@ -23,12 +25,15 @@
{# the JS uses this to display session information in the bottom panel #}
<div class="session-info">
<label>
{{ session.scheduling_label }}
&middot; {{ session.requested_duration_in_hours }} h
{% if session.group %}&middot; {% if session.group.is_bof %}BoF{% else %}{{ session.group.type.name }}{% endif %}{% endif %}
{% if session.attendees != None %}&middot; {{ session.attendees }} <i class="fa fa-user-o"></i>{% endif %}
</label>
<div>
<strong>
<span class="time pull-right"></span>
{{ session.scheduling_label }}
&middot; {{ session.requested_duration_in_hours }}h
{% if session.group %}&middot; {% if session.group.is_bof %}BoF{% else %}{{ session.group.type.name }}{% endif %}{% endif %}
{% if session.attendees != None %}&middot; {{ session.attendees }} <i class="fa fa-user-o"></i>{% endif %}
</strong>
</div>
{% if session.group %}
<div>
@ -42,7 +47,7 @@
{% if session.requested_by_person %}
<div>
<i class="fa fa-user-circle-o"></i> {{ session.requested_by_person.plain_name }} {% if session.requested_time %}({{ session.requested_time|date:"Y-m-d" }}){% endif %}
<i title="Requested by" class="fa fa-user-circle-o"></i> {{ session.requested_by_person.plain_name }} {% if session.requested_time %}({{ session.requested_time|date:"Y-m-d" }}){% endif %}
</div>
{% endif %}