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(
dir=['presentation', 'social', 'tutorial'],
ietf=['admin', 'presentation', 'social'],
nomcom=['closed', 'officehours'],
rg=['session'],
team=['coding', 'presentation', 'social', 'tutorial'],
wg=['session'],

View file

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

View file

@ -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,9 +596,13 @@ 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.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(
sessions = Session.objects.requests().filter(
meeting__number=meeting.number,
# type__slug='regular',
group__parent__isnull=False
),
requested_by=True,
).exclude(
).with_current_status().with_requested_by().exclude(
requested_by=0
).order_by(
"group__parent__acronym", "current_status", "group__acronym"

View file

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

View file

@ -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() {

View file

@ -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;">
<div class="time-label">
<div class="past-flag">&nbsp;{# blank div keeps time centered vertically #}</div>
@ -184,6 +189,18 @@
{% endfor %}
</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">
<button class="btn btn-default" data-toggle="modal" data-target="#timeslot-group-toggles-modal"><input type="checkbox" checked="checked" disabled> Timeslots</button>
</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 %}">
{{ session.scheduling_label }}
{% if session.group and session.group.is_bof %}<span class="bof-tag">BOF</span>{% endif %}
@ -30,14 +35,9 @@
<div class="title">
<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 %}
{{ session.scheduling_label }} &middot; {{ session.requested_duration_in_hours }}h
{% if session.purpose_label %} &middot; {{ session.purpose_label }} {% endif %}
{% if session.attendees != None %} &middot; {{ session.attendees }} <i class="fa fa-user-o"></i> {% endif %}
</strong>
</div>