Add 'closed' session purpose, assign purposes for nomcom groups, and update schedule editor to enforce timeslot type and allow blurring sessions by purpose

- Legacy-Id: 19427
This commit is contained in:
Jennifer Richards 2021-10-15 19:33:33 +00:00
parent 5318081608
commit 173e438aee
7 changed files with 131 additions and 51 deletions

View file

@ -8,6 +8,7 @@ from django.db import migrations
default_purposes = dict( default_purposes = dict(
dir=['presentation', 'social', 'tutorial'], dir=['presentation', 'social', 'tutorial'],
ietf=['admin', 'presentation', 'social'], ietf=['admin', 'presentation', 'social'],
nomcom=['closed', 'officehours'],
rg=['session'], rg=['session'],
team=['coding', 'presentation', 'social', 'tutorial'], team=['coding', 'presentation', 'social', 'tutorial'],
wg=['session'], wg=['session'],

View file

@ -1146,6 +1146,11 @@ class SessionQuerySet(models.QuerySet):
type__slug='regular' 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): class Session(models.Model):
"""Session records that a group should have a session on the """Session records that a group should have a session on the

View file

@ -500,6 +500,9 @@ def new_meeting_schedule(request, num, owner=None, name=None):
@ensure_csrf_cookie @ensure_csrf_cookie
def edit_meeting_schedule(request, num=None, owner=None, name=None): 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) meeting = get_meeting(num)
if name is None: if name is None:
schedule = meeting.schedule 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( sessions = add_event_info_to_session_qs(
Session.objects.filter( Session.objects.filter(
meeting=meeting, meeting=meeting,
# type='regular', ).exclude(
type__in=IGNORE_TIMESLOT_TYPES,
).order_by('pk'), ).order_by('pk'),
requested_time=True, requested_time=True,
requested_by=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=['appr', 'schedw', 'scheda', 'sched'])
| Q(current_status__in=tombstone_states, pk__in={a.session_id for a in assignments}) | Q(current_status__in=tombstone_states, pk__in={a.session_id for a in assignments})
).prefetch_related( ).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( timeslots_qs = TimeSlot.objects.filter(
meeting=meeting, meeting=meeting,
# type='regular', ).exclude(
type__in=IGNORE_TIMESLOT_TYPES,
).prefetch_related('type').order_by('location', 'time', 'name') ).prefetch_related('type').order_by('location', 'time', 'name')
min_duration = min(t.duration for t in timeslots_qs) 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.requested_by_person = requested_by_lookup.get(s.requested_by)
s.scheduling_label = "???" 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 s.scheduling_label = s.group.acronym
elif s.name: s.purpose_label = 'BoF' if s.group.is_bof() else s.group.type.name
s.scheduling_label = s.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) 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.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)) 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", { return render(request, "meeting/edit_meeting_schedule.html", {
'meeting': meeting, 'meeting': meeting,
'schedule': schedule, '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()), 'timeslot_groups': sorted((d, list(sorted(t_groups))) for d, t_groups in timeslot_groups.items()),
'unassigned_sessions': unassigned_sessions, 'unassigned_sessions': unassigned_sessions,
'session_parents': session_parents, 'session_parents': session_parents,
'session_purposes': session_purposes,
'hide_menu': True, 'hide_menu': True,
'lock_time': lock_time, 'lock_time': lock_time,
}) })
@ -2261,14 +2273,10 @@ def agenda_json(request, num=None):
def meeting_requests(request, num=None): def meeting_requests(request, num=None):
meeting = get_meeting(num) meeting = get_meeting(num)
sessions = add_event_info_to_session_qs( sessions = Session.objects.requests().filter(
Session.objects.filter( meeting__number=meeting.number,
meeting__number=meeting.number, group__parent__isnull=False
# type__slug='regular', ).with_current_status().with_requested_by().exclude(
group__parent__isnull=False
),
requested_by=True,
).exclude(
requested_by=0 requested_by=0
).order_by( ).order_by(
"group__parent__acronym", "current_status", "group__acronym" "group__parent__acronym", "current_status", "group__acronym"

View file

@ -16,7 +16,8 @@ def forward(apps, schema_editor):
('coding', 'Coding', 'Coding session', ['other']), ('coding', 'Coding', 'Coding session', ['other']),
('admin', 'Administrative', 'Meeting administration', ['other', 'reg']), ('admin', 'Administrative', 'Meeting administration', ['other', 'reg']),
('social', 'Social', 'Social event or activity', ['other']), ('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 # verify that we're not about to use an invalid purpose
for ts_type in tstypes: for ts_type in tstypes:

View file

@ -82,6 +82,7 @@ jQuery(document).ready(function () {
jQuery(element).addClass("selected"); jQuery(element).addClass("selected");
showConstraintHints(element); showConstraintHints(element);
showTimeSlotTypeIndicators(element.dataset.type);
let sessionInfoContainer = content.find(".scheduling-panel .session-info-container"); let sessionInfoContainer = content.find(".scheduling-panel .session-info-container");
sessionInfoContainer.html(jQuery(element).find(".session-info").html()); sessionInfoContainer.html(jQuery(element).find(".session-info").html());
@ -105,6 +106,7 @@ jQuery(document).ready(function () {
else { else {
sessions.removeClass("selected"); sessions.removeClass("selected");
showConstraintHints(); showConstraintHints();
resetTimeSlotTypeIndicators();
content.find(".scheduling-panel .session-info-container").html(""); 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? * 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)); 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? * Can a session be dropped in this element?
* *
* Drop is allowed in drop-zones that are in unassigned-session or timeslot containers * Drop is allowed in drop-zones that are in unassigned-session or timeslot containers
* not marked as 'past'. * not marked as 'past'.
*/ */
function sessionDropAllowed(elt) { function sessionDropAllowed(dropElement, sessionElement) {
if (!officialSchedule) { const relevant_parent = dropElement.closest('.timeslot, .unassigned-sessions');
return true; if (!relevant_parent || !sessionElement) {
return false;
} }
const relevant_parent = elt.closest('.timeslot, .unassigned-sessions'); if (officialSchedule && relevant_parent.classList.contains('past')) {
return relevant_parent && !(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")) { if (!content.find(".edit-grid").hasClass("read-only")) {
@ -314,7 +356,7 @@ jQuery(document).ready(function () {
// dropping // dropping
let dropElements = content.find(".timeslot .drop-target,.unassigned-sessions .drop-target"); let dropElements = content.find(".timeslot .drop-target,.unassigned-sessions .drop-target");
dropElements.on('dragenter', function (event) { 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 event.preventDefault(); // default action is signalling that this is not a valid target
jQuery(this).parent().addClass("dropping"); 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 // we don't actually need this event, except we need to signal
// that this is a valid drop target, by cancelling the default // that this is a valid drop target, by cancelling the default
// action // action
if (sessionDropAllowed(this)) { if (sessionDropAllowed(this, getDraggedSession(event))) {
event.preventDefault(); event.preventDefault();
} }
}); });
@ -332,7 +374,7 @@ jQuery(document).ready(function () {
dropElements.on('dragleave', function (event) { dropElements.on('dragleave', function (event) {
// skip dragleave events if they are to children // skip dragleave events if they are to children
const leaving_child = event.originalEvent.currentTarget.contains(event.originalEvent.relatedTarget); 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'); jQuery(this).parent().removeClass('dropping');
} }
}); });
@ -340,30 +382,21 @@ jQuery(document).ready(function () {
dropElements.on('drop', function (event) { dropElements.on('drop', function (event) {
let dropElement = jQuery(this); let dropElement = jQuery(this);
if (!isSessionDragEvent(event)) { const sessionElement = getDraggedSession(event);
// event is result of something other than a session drag if (!sessionElement) {
// not drag event or not from a session we recognize
dropElement.parent().removeClass("dropping"); dropElement.parent().removeClass("dropping");
return; return;
} }
const sessionId = event.originalEvent.dataTransfer.getData(dnd_mime_type); if (!sessionDropAllowed(this, sessionElement)) {
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)) {
dropElement.parent().removeClass("dropping"); // just in case dropElement.parent().removeClass("dropping"); // just in case
return; // drop not allowed return; // drop not allowed
} }
event.preventDefault(); // prevent opening as link event.preventDefault(); // prevent opening as link
let dragParent = sessionElement.parent(); let dragParent = jQuery(sessionElement).parent();
if (dragParent.is(this)) { if (dragParent.is(this)) {
dropElement.parent().removeClass("dropping"); dropElement.parent().removeClass("dropping");
return; return;
@ -400,7 +433,7 @@ jQuery(document).ready(function () {
timeout: 5 * 1000, timeout: 5 * 1000,
data: { data: {
action: "unassign", action: "unassign",
session: sessionId.slice("session".length) session: sessionElement.id.slice("session".length)
} }
}).fail(failHandler).done(done); }).fail(failHandler).done(done);
} }
@ -410,7 +443,7 @@ jQuery(document).ready(function () {
method: "post", method: "post",
data: { data: {
action: "assign", action: "assign",
session: sessionId.slice("session".length), session: sessionElement.id.slice("session".length),
timeslot: dropParent.attr("id").slice("timeslot".length) timeslot: dropParent.attr("id").slice("timeslot".length)
}, },
timeout: 5 * 1000 timeout: 5 * 1000
@ -673,7 +706,7 @@ jQuery(document).ready(function () {
// toggling visible sessions by session parents // toggling visible sessions by session parents
let sessionParentInputs = content.find(".session-parent-toggles input"); let sessionParentInputs = content.find(".session-parent-toggles input");
function setSessionHidden(sess, hide) { function setSessionHiddenParent(sess, hide) {
sess.toggleClass('hidden-parent', hide); sess.toggleClass('hidden-parent', hide);
sess.prop('draggable', !hide); sess.prop('draggable', !hide);
} }
@ -684,13 +717,28 @@ jQuery(document).ready(function () {
checked.push(".parent-" + this.value); checked.push(".parent-" + this.value);
}); });
setSessionHidden(sessions.not(".untoggleable").filter(checked.join(",")), false); setSessionHiddenParent(sessions.not(".untoggleable-by-parent").filter(checked.join(",")), false);
setSessionHidden(sessions.not(".untoggleable").not(checked.join(",")), true); setSessionHiddenParent(sessions.not(".untoggleable-by-parent").not(checked.join(",")), true);
} }
sessionParentInputs.on("click", updateSessionParentToggling); sessionParentInputs.on("click", updateSessionParentToggling);
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 // toggling visible timeslots
let timeslotGroupInputs = content.find("#timeslot-group-toggles-modal .modal-body input"); let timeslotGroupInputs = content.find("#timeslot-group-toggles-modal .modal-body input");
function updateTimeslotGroupToggling() { function updateTimeslotGroupToggling() {

View file

@ -16,6 +16,10 @@
.edit-meeting-schedule .edit-grid .timeslot.past-hint { filter: brightness(0.9); } .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 .past-flag { visibility: hidden; font-size: smaller; }
.edit-meeting-schedule .edit-grid .timeslot.past .past-flag { visibility: visible; color: #aaaaaa; } .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 %} {% endblock morecss %}
{% block title %}{{ schedule.name }}: IETF {{ meeting.number }} meeting agenda{% endblock %} {% block title %}{{ schedule.name }}: IETF {{ meeting.number }} meeting agenda{% endblock %}
@ -133,6 +137,7 @@
data-end="{{ t.utc_end_time.isoformat }}" data-end="{{ t.utc_end_time.isoformat }}"
data-duration="{{ t.duration.total_seconds }}" data-duration="{{ t.duration.total_seconds }}"
data-scheduledatlabel="{{ t.time|date:"l G:i" }}-{{ t.end_time|date:"G:i" }}" 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;"> style="width: {{ t.layout_width }}rem;">
<div class="time-label"> <div class="time-label">
<div class="past-flag">&nbsp;{# blank div keeps time centered vertically #}</div> <div class="past-flag">&nbsp;{# blank div keeps time centered vertically #}</div>
@ -184,6 +189,18 @@
{% endfor %} {% endfor %}
</span> </span>
<span class="session-purpose-toggles">
{% for purpose in session_purposes %}
<label class="purpose-{{ purpose.slug }}"><input type="checkbox" checked value="{{ purpose.slug }}"> {{ purpose }}</label>
{% endfor %}
</span>
<span class="timeslot-type-toggles">
{% for purpose in session_purposes %}
<label class="purpose-{{ purpose.slug }}"><input type="checkbox" checked value="{{ purpose.slug }}"> {{ purpose }}</label>
{% endfor %}
</span>
<span class="timeslot-group-toggles"> <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> <button class="btn btn-default" data-toggle="modal" data-target="#timeslot-group-toggles-modal"><input type="checkbox" checked="checked" disabled> Timeslots</button>
</span> </span>

View file

@ -1,4 +1,9 @@
<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 %} {% if session.readonly %}readonly{% endif %}" style="width:{{ session.layout_width }}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-by-parent{% endif %} {% if session.parent_acronym %}parent-{{ session.parent_acronym }} {% endif %}{% if session.purpose %}purpose-{{ session.purpose.slug }} {% else %} purpose-session {% endif %}{% if session.readonly %}readonly {% endif %}"
style="width:{{ session.layout_width }}em;"
data-duration="{{ session.requested_duration.total_seconds }}" {% if session.attendees != None %}
data-attendees="{{ session.attendees }}"{% endif %}
data-type="{{ session.type.slug }}">
<div class="session-label {% if session.group and session.group.is_bof %}bof-session{% endif %}"> <div class="session-label {% if session.group and session.group.is_bof %}bof-session{% endif %}">
{{ session.scheduling_label }} {{ session.scheduling_label }}
{% if session.group and session.group.is_bof %}<span class="bof-tag">BOF</span>{% endif %} {% if session.group and session.group.is_bof %}<span class="bof-tag">BOF</span>{% endif %}
@ -30,14 +35,9 @@
<div class="title"> <div class="title">
<strong> <strong>
<span class="time pull-right"></span> <span class="time pull-right"></span>
{{ session.scheduling_label }} {{ session.scheduling_label }} &middot; {{ session.requested_duration_in_hours }}h
&middot; {{ session.requested_duration_in_hours }}h {% if session.purpose_label %} &middot; {{ session.purpose_label }} {% endif %}
{% if session.group %} {% if session.attendees != None %} &middot; {{ session.attendees }} <i class="fa fa-user-o"></i> {% endif %}
&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> </strong>
</div> </div>