diff --git a/ietf/group/migrations/0052_populate_groupfeatures_session_purposes.py b/ietf/group/migrations/0052_populate_groupfeatures_session_purposes.py index 5ad6bdbd9..5e2353ed7 100644 --- a/ietf/group/migrations/0052_populate_groupfeatures_session_purposes.py +++ b/ietf/group/migrations/0052_populate_groupfeatures_session_purposes.py @@ -8,6 +8,7 @@ from django.db import migrations default_purposes = dict( dir=['presentation', 'social', 'tutorial'], ietf=['admin', 'presentation', 'social'], + nomcom=['closed', 'officehours'], rg=['session'], team=['coding', 'presentation', 'social', 'tutorial'], wg=['session'], diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py index 445ea49e2..964a02979 100644 --- a/ietf/meeting/models.py +++ b/ietf/meeting/models.py @@ -1146,6 +1146,11 @@ class SessionQuerySet(models.QuerySet): type__slug='regular' ) + def requests(self): + """Queryset containing sessions that may be handled as requests""" + return self.exclude( + type__in=('offagenda', 'reserved', 'unavail') + ) class Session(models.Model): """Session records that a group should have a session on the diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index 5d06084cd..b2fb786a0 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -500,6 +500,9 @@ def new_meeting_schedule(request, num, owner=None, name=None): @ensure_csrf_cookie def edit_meeting_schedule(request, num=None, owner=None, name=None): + # Need to coordinate this list with types of session requests + # that can be created (see, e.g., SessionQuerySet.requests()) + IGNORE_TIMESLOT_TYPES = ('offagenda', 'reserved', 'unavail') meeting = get_meeting(num) if name is None: schedule = meeting.schedule @@ -544,7 +547,8 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None): sessions = add_event_info_to_session_qs( Session.objects.filter( meeting=meeting, - # type='regular', + ).exclude( + type__in=IGNORE_TIMESLOT_TYPES, ).order_by('pk'), requested_time=True, requested_by=True, @@ -552,12 +556,13 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None): Q(current_status__in=['appr', 'schedw', 'scheda', 'sched']) | Q(current_status__in=tombstone_states, pk__in={a.session_id for a in assignments}) ).prefetch_related( - 'resources', 'group', 'group__parent', 'group__type', 'joint_with_groups', + 'resources', 'group', 'group__parent', 'group__type', 'joint_with_groups', 'purpose', ) timeslots_qs = TimeSlot.objects.filter( meeting=meeting, - # type='regular', + ).exclude( + type__in=IGNORE_TIMESLOT_TYPES, ).prefetch_related('type').order_by('location', 'time', 'name') min_duration = min(t.duration for t in timeslots_qs) @@ -591,10 +596,14 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None): s.requested_by_person = requested_by_lookup.get(s.requested_by) s.scheduling_label = "???" - if (s.purpose is None or s.purpose.slug == 'regular') and s.group: + s.purpose_label = None + if (s.purpose is None or s.purpose.slug == 'session') and s.group: s.scheduling_label = s.group.acronym - elif s.name: - s.scheduling_label = s.name + s.purpose_label = 'BoF' if s.group.is_bof() else s.group.type.name + else: + s.purpose_label = s.purpose.name + if s.name: + s.scheduling_label = s.name s.requested_duration_in_hours = round(s.requested_duration.seconds / 60.0 / 60.0, 1) @@ -981,6 +990,8 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None): p.scheduling_color = "rgb({}, {}, {})".format(*tuple(int(round(x * 255)) for x in rgb_color)) p.light_scheduling_color = "rgb({}, {}, {})".format(*tuple(int(round((0.9 + 0.1 * x) * 255)) for x in rgb_color)) + session_purposes = sorted(set(s.purpose for s in sessions if s.purpose), key=lambda p: p.name) + return render(request, "meeting/edit_meeting_schedule.html", { 'meeting': meeting, 'schedule': schedule, @@ -991,6 +1002,7 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None): 'timeslot_groups': sorted((d, list(sorted(t_groups))) for d, t_groups in timeslot_groups.items()), 'unassigned_sessions': unassigned_sessions, 'session_parents': session_parents, + 'session_purposes': session_purposes, 'hide_menu': True, 'lock_time': lock_time, }) @@ -2261,14 +2273,10 @@ def agenda_json(request, num=None): def meeting_requests(request, num=None): meeting = get_meeting(num) - sessions = add_event_info_to_session_qs( - Session.objects.filter( - meeting__number=meeting.number, - # type__slug='regular', - group__parent__isnull=False - ), - requested_by=True, - ).exclude( + sessions = Session.objects.requests().filter( + meeting__number=meeting.number, + group__parent__isnull=False + ).with_current_status().with_requested_by().exclude( requested_by=0 ).order_by( "group__parent__acronym", "current_status", "group__acronym" diff --git a/ietf/name/migrations/0036_populate_sessionpurposename.py b/ietf/name/migrations/0036_populate_sessionpurposename.py index 50231ce63..b36036059 100644 --- a/ietf/name/migrations/0036_populate_sessionpurposename.py +++ b/ietf/name/migrations/0036_populate_sessionpurposename.py @@ -16,7 +16,8 @@ def forward(apps, schema_editor): ('coding', 'Coding', 'Coding session', ['other']), ('admin', 'Administrative', 'Meeting administration', ['other', 'reg']), ('social', 'Social', 'Social event or activity', ['other']), - ('presentation', 'Presentation', 'Presentation session', ['other', 'regular']) + ('presentation', 'Presentation', 'Presentation session', ['other', 'regular']), + ('closed', 'Closed meeting', 'Closed meeting', ['other',]), )): # verify that we're not about to use an invalid purpose for ts_type in tstypes: diff --git a/ietf/static/ietf/js/edit-meeting-schedule.js b/ietf/static/ietf/js/edit-meeting-schedule.js index b4aa3e365..dc8b35565 100644 --- a/ietf/static/ietf/js/edit-meeting-schedule.js +++ b/ietf/static/ietf/js/edit-meeting-schedule.js @@ -82,6 +82,7 @@ jQuery(document).ready(function () { jQuery(element).addClass("selected"); showConstraintHints(element); + showTimeSlotTypeIndicators(element.dataset.type); let sessionInfoContainer = content.find(".scheduling-panel .session-info-container"); sessionInfoContainer.html(jQuery(element).find(".session-info").html()); @@ -105,6 +106,7 @@ jQuery(document).ready(function () { else { sessions.removeClass("selected"); showConstraintHints(); + resetTimeSlotTypeIndicators(); content.find(".scheduling-panel .session-info-container").html(""); } } @@ -203,6 +205,23 @@ jQuery(document).ready(function () { } } + /** + * Remove timeslot classes indicating timeslot type disagreement + */ + function resetTimeSlotTypeIndicators() { + timeslots.removeClass('wrong-timeslot-type'); + } + + /** + * Add timeslot classes indicating timeslot type disagreement + * + * @param timeslot_type + */ + function showTimeSlotTypeIndicators(timeslot_type) { + timeslots.removeClass('wrong-timeslot-type'); + timeslots.filter('[data-type!="' + timeslot_type + '"]').addClass('wrong-timeslot-type'); + } + /** * Should this timeslot be treated as a future timeslot? * @@ -277,19 +296,42 @@ jQuery(document).ready(function () { return Boolean(event.originalEvent.dataTransfer.getData(dnd_mime_type)); } + /** + * Get the session element being dragged + * + * @param event drag-related event + */ + function getDraggedSession(event) { + if (!isSessionDragEvent(event)) { + return null; + } + const sessionId = event.originalEvent.dataTransfer.getData(dnd_mime_type); + const sessionElements = sessions.filter("#" + sessionId); + if (sessionElements.length > 0) { + return sessionElements[0]; + } + return null; + } + /** * Can a session be dropped in this element? * * Drop is allowed in drop-zones that are in unassigned-session or timeslot containers * not marked as 'past'. */ - function sessionDropAllowed(elt) { - if (!officialSchedule) { - return true; + function sessionDropAllowed(dropElement, sessionElement) { + const relevant_parent = dropElement.closest('.timeslot, .unassigned-sessions'); + if (!relevant_parent || !sessionElement) { + return false; } - const relevant_parent = elt.closest('.timeslot, .unassigned-sessions'); - return relevant_parent && !(relevant_parent.classList.contains('past')); + if (officialSchedule && relevant_parent.classList.contains('past')) { + return false; + } + + return !relevant_parent.dataset.type || ( + relevant_parent.dataset.type === sessionElement.dataset.type + ); } if (!content.find(".edit-grid").hasClass("read-only")) { @@ -314,7 +356,7 @@ jQuery(document).ready(function () { // dropping let dropElements = content.find(".timeslot .drop-target,.unassigned-sessions .drop-target"); dropElements.on('dragenter', function (event) { - if (sessionDropAllowed(this)) { + if (sessionDropAllowed(this, getDraggedSession(event))) { event.preventDefault(); // default action is signalling that this is not a valid target jQuery(this).parent().addClass("dropping"); } @@ -324,7 +366,7 @@ jQuery(document).ready(function () { // we don't actually need this event, except we need to signal // that this is a valid drop target, by cancelling the default // action - if (sessionDropAllowed(this)) { + if (sessionDropAllowed(this, getDraggedSession(event))) { event.preventDefault(); } }); @@ -332,7 +374,7 @@ jQuery(document).ready(function () { dropElements.on('dragleave', function (event) { // skip dragleave events if they are to children const leaving_child = event.originalEvent.currentTarget.contains(event.originalEvent.relatedTarget); - if (!leaving_child && sessionDropAllowed(this)) { + if (!leaving_child && sessionDropAllowed(this, getDraggedSession(event))) { jQuery(this).parent().removeClass('dropping'); } }); @@ -340,30 +382,21 @@ jQuery(document).ready(function () { dropElements.on('drop', function (event) { let dropElement = jQuery(this); - if (!isSessionDragEvent(event)) { - // event is result of something other than a session drag + const sessionElement = getDraggedSession(event); + if (!sessionElement) { + // not drag event or not from a session we recognize dropElement.parent().removeClass("dropping"); return; } - const sessionId = event.originalEvent.dataTransfer.getData(dnd_mime_type); - let sessionElement = sessions.filter("#" + sessionId); - if (sessionElement.length === 0) { - // drag event is not from a session we recognize - dropElement.parent().removeClass("dropping"); - return; - } - - // We now know this is a drop of a recognized session - - if (!sessionDropAllowed(this)) { + if (!sessionDropAllowed(this, sessionElement)) { dropElement.parent().removeClass("dropping"); // just in case return; // drop not allowed } event.preventDefault(); // prevent opening as link - let dragParent = sessionElement.parent(); + let dragParent = jQuery(sessionElement).parent(); if (dragParent.is(this)) { dropElement.parent().removeClass("dropping"); return; @@ -400,7 +433,7 @@ jQuery(document).ready(function () { timeout: 5 * 1000, data: { action: "unassign", - session: sessionId.slice("session".length) + session: sessionElement.id.slice("session".length) } }).fail(failHandler).done(done); } @@ -410,7 +443,7 @@ jQuery(document).ready(function () { method: "post", data: { action: "assign", - session: sessionId.slice("session".length), + session: sessionElement.id.slice("session".length), timeslot: dropParent.attr("id").slice("timeslot".length) }, timeout: 5 * 1000 @@ -673,7 +706,7 @@ jQuery(document).ready(function () { // toggling visible sessions by session parents let sessionParentInputs = content.find(".session-parent-toggles input"); - function setSessionHidden(sess, hide) { + function setSessionHiddenParent(sess, hide) { sess.toggleClass('hidden-parent', hide); sess.prop('draggable', !hide); } @@ -684,13 +717,28 @@ jQuery(document).ready(function () { checked.push(".parent-" + this.value); }); - setSessionHidden(sessions.not(".untoggleable").filter(checked.join(",")), false); - setSessionHidden(sessions.not(".untoggleable").not(checked.join(",")), true); + setSessionHiddenParent(sessions.not(".untoggleable-by-parent").filter(checked.join(",")), false); + setSessionHiddenParent(sessions.not(".untoggleable-by-parent").not(checked.join(",")), true); } sessionParentInputs.on("click", updateSessionParentToggling); updateSessionParentToggling(); + // Toggling session purposes + let sessionPurposeInputs = content.find('.session-purpose-toggles input'); + function updateSessionPurposeToggling() { + let checked = []; + sessionPurposeInputs.filter(":checked").each(function () { + checked.push(".purpose-" + this.value); + }); + + sessions.filter(checked.join(",")).removeClass('hidden-purpose'); + sessions.not(checked.join(",")).addClass('hidden-purpose'); + } + + sessionPurposeInputs.on("click", updateSessionPurposeToggling); + updateSessionPurposeToggling(); + // toggling visible timeslots let timeslotGroupInputs = content.find("#timeslot-group-toggles-modal .modal-body input"); function updateTimeslotGroupToggling() { diff --git a/ietf/templates/meeting/edit_meeting_schedule.html b/ietf/templates/meeting/edit_meeting_schedule.html index 6718fd673..584836aa9 100644 --- a/ietf/templates/meeting/edit_meeting_schedule.html +++ b/ietf/templates/meeting/edit_meeting_schedule.html @@ -16,6 +16,10 @@ .edit-meeting-schedule .edit-grid .timeslot.past-hint { filter: brightness(0.9); } .edit-meeting-schedule .past-flag { visibility: hidden; font-size: smaller; } .edit-meeting-schedule .edit-grid .timeslot.past .past-flag { visibility: visible; color: #aaaaaa; } + {# type and purpose styling #} + .edit-meeting-schedule .edit-grid .timeslot.wrong-timeslot-type { background-color: transparent; ); } + .edit-meeting-schedule .edit-grid .timeslot.wrong-timeslot-type .time-label { color: transparent; ); } + .edit-meeting-schedule .session.hidden-purpose { filter: blur(3px); } {% endblock morecss %} {% block title %}{{ schedule.name }}: IETF {{ meeting.number }} meeting agenda{% endblock %} @@ -133,6 +137,7 @@ data-end="{{ t.utc_end_time.isoformat }}" data-duration="{{ t.duration.total_seconds }}" data-scheduledatlabel="{{ t.time|date:"l G:i" }}-{{ t.end_time|date:"G:i" }}" + data-type="{{ t.type.slug }}" style="width: {{ t.layout_width }}rem;">
 {# blank div keeps time centered vertically #}
@@ -184,6 +189,18 @@ {% endfor %} + + {% for purpose in session_purposes %} + + {% endfor %} + + + + {% for purpose in session_purposes %} + + {% endfor %} + + diff --git a/ietf/templates/meeting/edit_meeting_schedule_session.html b/ietf/templates/meeting/edit_meeting_schedule_session.html index 064279d2f..29cdd18df 100644 --- a/ietf/templates/meeting/edit_meeting_schedule_session.html +++ b/ietf/templates/meeting/edit_meeting_schedule_session.html @@ -1,4 +1,9 @@ -
+
{{ session.scheduling_label }} {% if session.group and session.group.is_bof %}BOF{% endif %} @@ -30,14 +35,9 @@
- {{ session.scheduling_label }} - · {{ session.requested_duration_in_hours }}h - {% if session.group %} - · {% if session.group.is_bof %}BOF{% else %}{{ session.group.type.name }}{% endif %} - {% endif %} - {% if session.attendees != None %} - · {{ session.attendees }} - {% endif %} + {{ session.scheduling_label }} · {{ session.requested_duration_in_hours }}h + {% if session.purpose_label %} · {{ session.purpose_label }} {% endif %} + {% if session.attendees != None %} · {{ session.attendees }} {% endif %}