Hide timeslots type is disabled plus other schedule editor debugging/improvements. Fixes #3510. Fixes #3430. Commit ready for merge.

- Legacy-Id: 19878
This commit is contained in:
Jennifer Richards 2022-01-27 00:41:27 +00:00
parent c4bf508cd8
commit 795d861d96
8 changed files with 360 additions and 153 deletions

View file

@ -662,11 +662,15 @@ class SessionDetailsForm(forms.ModelForm):
def __init__(self, group, *args, **kwargs):
session_purposes = group.features.session_purposes
kwargs.setdefault('initial', {})
kwargs['initial'].setdefault(
'purpose',
session_purposes[0] if len(session_purposes) > 0 else None,
)
# Default to the first allowed session_purposes. Do not do this if we have an instance,
# though, because ModelForm will override instance data with initial data if it gets both.
# When we have an instance we want to keep its value.
if 'instance' not in kwargs:
kwargs.setdefault('initial', {})
kwargs['initial'].setdefault(
'purpose',
session_purposes[0] if len(session_purposes) > 0 else None,
)
super().__init__(*args, **kwargs)
self.fields['type'].widget.attrs.update({
@ -678,11 +682,11 @@ class SessionDetailsForm(forms.ModelForm):
self.fields['purpose'].queryset = SessionPurposeName.objects.filter(pk__in=session_purposes)
if not group.features.acts_like_wg:
self.fields['requested_duration'].durations = [datetime.timedelta(minutes=m) for m in range(30, 241, 30)]
class Meta:
model = Session
fields = (
'name', 'short', 'purpose', 'type', 'requested_duration',
'purpose', 'name', 'short', 'type', 'requested_duration',
'on_agenda', 'remote_instructions', 'attendees', 'comments',
)
labels = {'requested_duration': 'Length'}

View file

@ -541,21 +541,21 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
past_swap_ts_buttons = self.driver.find_elements(By.CSS_SELECTOR,
','.join(
'.swap-timeslot-col[data-start="{}"]'.format(ts.utc_start_time().isoformat()) for ts in past_timeslots
'*[data-start="{}"] .swap-timeslot-col'.format(ts.utc_start_time().isoformat()) for ts in past_timeslots
)
)
self.assertEqual(len(past_swap_ts_buttons), len(past_timeslots), 'Missing past swap timeslot col buttons')
future_swap_ts_buttons = self.driver.find_elements(By.CSS_SELECTOR,
','.join(
'.swap-timeslot-col[data-start="{}"]'.format(ts.utc_start_time().isoformat()) for ts in future_timeslots
'*[data-start="{}"] .swap-timeslot-col'.format(ts.utc_start_time().isoformat()) for ts in future_timeslots
)
)
self.assertEqual(len(future_swap_ts_buttons), len(future_timeslots), 'Missing future swap timeslot col buttons')
now_swap_ts_buttons = self.driver.find_elements(By.CSS_SELECTOR,
','.join(
'.swap-timeslot-col[data-start="{}"]'.format(ts.utc_start_time().isoformat()) for ts in now_timeslots
'[data-start="{}"] .swap-timeslot-col'.format(ts.utc_start_time().isoformat()) for ts in now_timeslots
)
)
self.assertEqual(len(now_swap_ts_buttons), len(now_timeslots), 'Missing "now" swap timeslot col buttons')

View file

@ -536,15 +536,17 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
for s in sessions:
s.requested_by_person = requested_by_lookup.get(s.requested_by)
s.scheduling_label = "???"
s.purpose_label = None
if (s.purpose.slug in ('none', 'regular')) and s.group:
s.scheduling_label = s.group.acronym
s.purpose_label = 'BoF' if s.group.is_bof() else s.group.type.name
if s.group:
if (s.purpose.slug in ('none', 'regular')):
s.scheduling_label = s.group.acronym
s.purpose_label = 'BoF' if s.group.is_bof() else s.group.type.name
else:
s.scheduling_label = s.name if s.name else f'??? [{s.group.acronym}]'
s.purpose_label = s.purpose.name
else:
s.purpose_label = s.purpose.name
if s.name:
s.scheduling_label = s.name
s.scheduling_label = s.name if s.name else '???'
s.purpose_label = s.purpose.name
s.requested_duration_in_hours = round(s.requested_duration.seconds / 60.0 / 60.0, 1)

View file

@ -190,7 +190,7 @@ class MiscSessionForm(TimeSlotForm):
Plenary = IETF''',
required=False)
location = forms.ModelChoiceField(queryset=Room.objects, required=False)
remote_instructions = forms.CharField(max_length=255)
remote_instructions = forms.CharField(max_length=255, required=False)
show_location = forms.BooleanField(required=False)
def __init__(self,*args,**kwargs):

View file

@ -1,5 +1,7 @@
/* globals alert, jQuery, moment */
jQuery(document).ready(function () {
'use strict';
let content = jQuery(".edit-meeting-schedule");
/* Drag data stored via the drag event dataTransfer interface is only accessible on
* dragstart and dragend events. Other drag events can see only the MIME types that have
@ -12,8 +14,9 @@ jQuery(document).ready(function () {
function reportServerError(xhr, textStatus, error) {
let errorText = error || textStatus;
if (xhr && xhr.responseText)
errorText += "\n\n" + xhr.responseText;
if (xhr && xhr.responseText) {
errorText += '\n\n' + xhr.responseText;
}
alert("Error: " + errorText);
}
@ -33,9 +36,14 @@ jQuery(document).ready(function () {
let swapTimeslotButtons = content.find('.swap-timeslot-col');
let days = content.find(".day-flow .day");
let officialSchedule = content.hasClass('official-schedule');
let timeSlotTypeInputs = content.find('.timeslot-type-toggles input');
let sessionPurposeInputs = content.find('.session-purpose-toggles input');
let timeSlotGroupInputs = content.find("#timeslot-group-toggles-modal .modal-body .individual-timeslots input");
let sessionParentInputs = content.find(".session-parent-toggles input");
const classes_to_hide = '.hidden-timeslot-group,.hidden-timeslot-type';
// 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") {
if (content.find(".scheduling-panel").css("position") !== "sticky") {
content.find(".scheduling-panel").css("position", "fixed");
content.css("padding-bottom", "14em");
}
@ -59,7 +67,7 @@ jQuery(document).ready(function () {
let res = [];
timeslots.each(function () {
var timeslot = jQuery(this);
const timeslot = jQuery(this);
let start = startMoment(timeslot);
let end = endMoment(timeslot);
@ -97,10 +105,11 @@ jQuery(document).ready(function () {
let timeElement = jQuery(this).find(".time");
otherSessionElement.addClass("other-session-selected");
if (scheduledAt)
timeElement.text(timeElement.data("scheduled").replace("{time}", scheduledAt));
else
timeElement.text(timeElement.data("notscheduled"));
if (scheduledAt) {
timeElement.text(timeElement.data('scheduled').replace('{time}', scheduledAt));
} else {
timeElement.text(timeElement.data('notscheduled'));
}
});
}
else {
@ -256,10 +265,10 @@ jQuery(document).ready(function () {
// hide swap day/timeslot column buttons
if (officialSchedule) {
swapDaysButtons.filter(
(_, elt) => parseISOTimestamp(elt.dataset.start).isSameOrBefore(now, 'day')
(_, elt) => parseISOTimestamp(elt.closest('*[data-start]').dataset.start).isSameOrBefore(now, 'day')
).hide();
swapTimeslotButtons.filter(
(_, elt) => parseISOTimestamp(elt.dataset.start).isSameOrBefore(now, 'minute')
(_, elt) => parseISOTimestamp(elt.closest('*[data-start]').dataset.start).isSameOrBefore(now, 'minute')
).hide();
}
}
@ -278,9 +287,12 @@ jQuery(document).ready(function () {
}
content.on("click", function (event) {
if (jQuery(event.target).is(".session-info-container") || jQuery(event.target).closest(".session-info-container").length > 0)
return;
selectSessionElement(null);
if (!(
jQuery(event.target).is('.session-info-container') ||
jQuery(event.target).closest('.session-info-container').length > 0
)) {
selectSessionElement(null);
}
});
sessions.on("click", function (event) {
@ -418,12 +430,14 @@ jQuery(document).ready(function () {
}
dropElement.append(sessionElement); // move element
if (response.tombstone)
if (response.tombstone) {
dragParent.append(response.tombstone);
}
updateCurrentSchedulingHints();
if (dropParent.hasClass("unassigned-sessions"))
if (dropParent.hasClass("unassigned-sessions")) {
sortUnassigned();
}
}
if (dropParent.hasClass("unassigned-sessions")) {
@ -436,8 +450,7 @@ jQuery(document).ready(function () {
session: sessionElement.id.slice("session".length)
}
}).fail(failHandler).done(done);
}
else {
} else {
jQuery.ajax({
url: window.location.href,
method: "post",
@ -476,7 +489,7 @@ jQuery(document).ready(function () {
// disable any that have passed
const now=effectiveNow();
const past_radios = radios.filter(
(_, radio) => parseISOTimestamp(radio.dataset.start).isSameOrBefore(now, datePrecision)
(_, radio) => parseISOTimestamp(radio.closest('*[data-start]').dataset.start).isSameOrBefore(now, datePrecision)
);
past_radios.parent().addClass('text-muted');
past_radios.prop('disabled', true);
@ -489,7 +502,7 @@ jQuery(document).ready(function () {
let swapDaysLabels = swapDaysModal.find(".modal-body label");
let swapDaysRadios = swapDaysLabels.find('input[name=target_day]');
let updateSwapDaysSubmitButton = function () {
updateSwapSubmitButton(swapDaysModal, 'target_day')
updateSwapSubmitButton(swapDaysModal, 'target_day');
};
// handler to prep and open the modal
content.find(".swap-days").on("click", function () {
@ -505,7 +518,7 @@ jQuery(document).ready(function () {
updateSwapDaysSubmitButton();
swapDaysModal.modal('show'); // show via JS so it won't open until it is initialized
});
swapDaysRadios.on("change", function () {updateSwapDaysSubmitButton()});
swapDaysRadios.on("change", function () {updateSwapDaysSubmitButton();});
// swap timeslot columns
let swapTimeslotsModal = content.find('#swap-timeslot-col-modal');
@ -534,7 +547,7 @@ jQuery(document).ready(function () {
updateSwapTimeslotsSubmitButton();
swapTimeslotsModal.modal('show');
});
swapTimeslotsRadios.on("change", function () {updateSwapTimeslotsSubmitButton()});
swapTimeslotsRadios.on("change", function () {updateSwapTimeslotsSubmitButton();});
}
// hints for the current schedule
@ -557,22 +570,44 @@ jQuery(document).ready(function () {
});
scheduledSessions.sort(function (a, b) {
if (a.start < b.start)
if (a.start < b.start) {
return -1;
if (a.start > b.start)
}
if (a.start > b.start) {
return 1;
}
return 0;
});
let currentlyOpen = {};
let openedIndex = 0;
let markSessionConstraintViolations = function (sess, currentlyOpen) {
sess.element.find(".constraints > span").each(function() {
let sessionIds = this.dataset.sessions;
let violated = sessionIds && sessionIds.split(",").filter(function (v) {
return (
v !== sess.id &&
v in currentlyOpen &&
// ignore errors within the same timeslot
// under the assumption that the sessions
// in the timeslot happen sequentially
sess.timeslot !== currentlyOpen[v].timeslot
);
}).length > 0;
jQuery(this).toggleClass("violated-hint", violated);
});
};
for (let i = 0; i < scheduledSessions.length; ++i) {
let s = scheduledSessions[i];
// prune
for (let sessionIdStr in currentlyOpen) {
if (currentlyOpen[sessionIdStr].end <= s.start)
if (currentlyOpen[sessionIdStr].end <= s.start) {
delete currentlyOpen[sessionIdStr];
}
}
// expand
@ -583,20 +618,7 @@ jQuery(document).ready(function () {
}
// check for violated constraints
s.element.find(".constraints > span").each(function () {
let sessionIds = this.dataset.sessions;
let violated = sessionIds && sessionIds.split(",").filter(function (v) {
return (v != s.id
&& v in currentlyOpen
// ignore errors within the same timeslot
// under the assumption that the sessions
// in the timeslot happen sequentially
&& s.timeslot != currentlyOpen[v].timeslot);
}).length > 0;
jQuery(this).toggleClass("violated-hint", violated);
});
markSessionConstraintViolations(s, currentlyOpen);
}
}
@ -614,8 +636,9 @@ jQuery(document).ready(function () {
function updateAttendeesViolations() {
sessions.each(function () {
let roomCapacity = jQuery(this).closest(".timeslots").data("roomcapacity");
if (roomCapacity && this.dataset.attendees)
if (roomCapacity && this.dataset.attendees) {
jQuery(this).toggleClass("too-many-attendees", +this.dataset.attendees > +roomCapacity);
}
});
}
@ -634,10 +657,12 @@ jQuery(document).ready(function () {
let ai = a[i];
let bi = b[i];
if (ai > bi)
if (ai > bi) {
return 1;
else if (ai < bi)
}
else if (ai < bi) {
return -1;
}
}
return 0;
@ -645,8 +670,9 @@ jQuery(document).ready(function () {
let arrayWithSortKeys = array.map(function (a) {
let res = [a];
for (let i = 0; i < keyFunctions.length; ++i)
for (let i = 0; i < keyFunctions.length; ++i) {
res.push(keyFunctions[i](a));
}
return res;
});
@ -693,8 +719,9 @@ jQuery(document).ready(function () {
let unassignedSessionsContainer = content.find(".unassigned-sessions .drop-target");
let sortedSessions = sortArrayWithKeyFunctions(unassignedSessionsContainer.children(".session").toArray(), keyFunctions);
for (let i = 0; i < sortedSessions.length; ++i)
for (let i = 0; i < sortedSessions.length; ++i) {
unassignedSessionsContainer.append(sortedSessions[i]);
}
}
content.find("select[name=sort_unassigned]").on("change click", function () {
@ -704,7 +731,6 @@ jQuery(document).ready(function () {
sortUnassigned();
// toggling visible sessions by session parents
let sessionParentInputs = content.find(".session-parent-toggles input");
function setSessionHiddenParent(sess, hide) {
sess.toggleClass('hidden-parent', hide);
@ -725,7 +751,6 @@ jQuery(document).ready(function () {
updateSessionParentToggling();
// Toggling timeslot types
let timeSlotTypeInputs = content.find('.timeslot-type-toggles input');
function updateTimeSlotTypeToggling() {
let checked = [];
timeSlotTypeInputs.filter(":checked").each(function () {
@ -736,27 +761,12 @@ jQuery(document).ready(function () {
sessions.not(checked.join(",")).addClass('hidden-timeslot-type');
timeslots.filter(checked.join(",")).removeClass('hidden-timeslot-type');
timeslots.not(checked.join(",")).addClass('hidden-timeslot-type');
}
if (timeSlotTypeInputs.length > 0) {
timeSlotTypeInputs.on("change", updateTimeSlotTypeToggling);
updateTimeSlotTypeToggling();
content.find('#timeslot-group-toggles-modal .timeslot-type-toggles .select-all').get(0).addEventListener(
'click',
function() {
timeSlotTypeInputs.prop('checked', true);
updateTimeSlotTypeToggling();
});
content.find('#timeslot-group-toggles-modal .timeslot-type-toggles .clear-all').get(0).addEventListener(
'click',
function() {
timeSlotTypeInputs.prop('checked', false);
updateTimeSlotTypeToggling();
});
updateGridVisibility();
}
// Toggling session purposes
let sessionPurposeInputs = content.find('.session-purpose-toggles input');
function updateSessionPurposeToggling(evt) {
function updateSessionPurposeToggling() {
let checked = [];
sessionPurposeInputs.filter(":checked").each(function () {
checked.push(".purpose-" + this.value);
@ -765,50 +775,193 @@ jQuery(document).ready(function () {
sessions.filter(checked.join(",")).removeClass('hidden-purpose');
sessions.not(checked.join(",")).addClass('hidden-purpose');
}
if (timeSlotTypeInputs.length > 0) {
timeSlotTypeInputs.on("change", updateTimeSlotTypeToggling);
updateTimeSlotTypeToggling();
content.find('#timeslot-type-toggles-modal .timeslot-type-toggles .select-all').get(0).addEventListener(
'click',
function() {
timeSlotTypeInputs.prop('checked', true);
updateTimeSlotTypeToggling();
});
content.find('#timeslot-type-toggles-modal .timeslot-type-toggles .clear-all').get(0).addEventListener(
'click',
function() {
timeSlotTypeInputs.prop('checked', false);
updateTimeSlotTypeToggling();
});
}
if (sessionPurposeInputs.length > 0) {
sessionPurposeInputs.on("change", updateSessionPurposeToggling);
updateSessionPurposeToggling();
content.find('#session-toggles-modal .select-all').get(0).addEventListener(
'click',
function() {
sessionPurposeInputs.prop('checked', true);
sessionPurposeInputs.not(':disabled').prop('checked', true);
updateSessionPurposeToggling();
});
content.find('#session-toggles-modal .clear-all').get(0).addEventListener(
'click',
function() {
sessionPurposeInputs.prop('checked', false);
sessionPurposeInputs.not(':disabled').prop('checked', false);
updateSessionPurposeToggling();
});
}
// toggling visible timeslots
let timeSlotGroupInputs = content.find("#timeslot-group-toggles-modal .modal-body .individual-timeslots 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");
timeslots.filter(checked.join(",")).removeClass("hidden-timeslot-group");
timeslots.not(checked.join(",")).addClass("hidden-timeslot-group");
updateGridVisibility();
}
days.each(function () {
jQuery(this).toggle(jQuery(this).find(".timeslot:not(.hidden)").length > 0);
function updateSessionPurposeOptions() {
sessionPurposeInputs.each((_, purpose_input) => {
if (sessions
.filter('.purpose-' + purpose_input.value)
.not('.hidden')
.length === 0) {
purpose_input.setAttribute('disabled', 'disabled');
purpose_input.closest('.session-purpose-toggle').classList.add('text-muted');
} else {
purpose_input.removeAttribute('disabled');
purpose_input.closest('.session-purpose-toggle').classList.remove('text-muted');
}
});
}
/**
* Hide timeslot toggles for hidden timeslots
*/
function updateTimeSlotOptions() {
timeSlotGroupInputs.each((_, timeslot_input) => {
if (timeslots
.filter('.' + timeslot_input.value)
.not('.hidden-timeslot-type')
.length === 0) {
timeslot_input.setAttribute('disabled', 'disabled');
} else {
timeslot_input.removeAttribute('disabled');
}
});
}
/**
* Make timeslots visible/invisible/hidden
*
* Responsible for final determination of whether a timeslot is visible, invisible, or hidden.
*/
function updateTimeSlotVisibility() {
timeslots.not(classes_to_hide).removeClass('hidden');
timeslots.filter(classes_to_hide).addClass('hidden');
}
/**
* Make sessions visible/invisible/hidden
*
* Responsible for final determination of whether a session is visible or hidden.
*/
function updateSessionVisibility() {
sessions.not(classes_to_hide).removeClass('hidden');
sessions.filter(classes_to_hide).addClass('hidden');
}
/**
* Make day / time headers visible / hidden to match visible grid contents
*/
function updateHeaderVisibility() {
days.each(function () {
jQuery(this).toggle(jQuery(this).find(".timeslot").not(".hidden").length > 0);
});
const rgs = content.find('.day-flow .room-group');
rgs.each(function (index, roomGroup) {
const headerLabels = jQuery(roomGroup).find('.time-header .time-label');
const rgTimeslots = jQuery(roomGroup).find('.timeslot');
headerLabels.each(function(index, label) {
jQuery(label).toggle(
rgTimeslots
.filter('[data-start="' + label.dataset.start + '"][data-end="' + label.dataset.end + '"]')
.not('.hidden')
.length > 0
);
});
});
}
/**
* Update visibility of room rows
*/
function updateRoomVisibility() {
const tsContainers = { toShow: [], toHide: [] };
const roomGroups = { toShow: [], toHide: [] };
// roomsWithVisibleSlots is an array of room IDs that have at least one visible timeslot
let roomsWithVisibleSlots = content.find('.day-flow .timeslots')
.has('.timeslot:not(.hidden)')
.map((_, e) => e.dataset.roomId).get();
roomsWithVisibleSlots = [...new Set(roomsWithVisibleSlots)]; // unique-ify by converting to Set and back
/* The "timeslots" class identifies elements (now and probably always <div>s) that are containers (i.e.,
* parents) of timeslots (elements with the "timeslot" class). Sort these containers based on whether
* their room has at least one timeslot visible - if so, we will show it, if not it will be hidden.
* This will hide containers both in the day-flow and room label sections, so it will hide the room
* labels for rooms with no visible timeslots. */
content.find('.timeslots').each((_, e) => {
if (roomsWithVisibleSlots.indexOf(e.dataset.roomId) === -1) {
tsContainers.toHide.push(e);
} else {
tsContainers.toShow.push(e);
}
});
/* Now check whether each room group has any rooms not being hidden. If not, entirely hide the
* room group so that all its headers, etc, do not take up space. */
content.find('.room-group').each((_, e) => {
if (jQuery(e).has(tsContainers.toShow).length > 0) {
roomGroups.toShow.push(e);
} else {
roomGroups.toHide.push(e);
}
});
jQuery(roomGroups.toShow).show();
jQuery(roomGroups.toHide).hide();
jQuery(tsContainers.toShow).show();
jQuery(tsContainers.toHide).hide();
}
/**
* Update visibility of UI elements
*
* Call this after changing 'hidden-*' classes on timeslots
*/
function updateGridVisibility() {
updateTimeSlotVisibility();
updateSessionVisibility();
updateHeaderVisibility();
updateRoomVisibility();
updateTimeSlotOptions();
updateSessionPurposeOptions();
content.find('div.edit-grid').removeClass('hidden');
}
timeSlotGroupInputs.on("click change", updateTimeSlotGroupToggling);
content.find('#timeslot-group-toggles-modal .timeslot-group-buttons .select-all').get(0).addEventListener(
'click',
function() {
timeSlotGroupInputs.prop('checked', true);
timeSlotGroupInputs.not(':disabled').prop('checked', true);
updateTimeSlotGroupToggling();
});
content.find('#timeslot-group-toggles-modal .timeslot-group-buttons .clear-all').get(0).addEventListener(
'click',
function() {
timeSlotGroupInputs.prop('checked', false);
timeSlotGroupInputs.not(':disabled').prop('checked', false);
updateTimeSlotGroupToggling();
});
@ -817,9 +970,9 @@ jQuery(document).ready(function () {
setInterval(updatePastTimeslots, 10 * 1000 /* ms */);
// session info
content.find(".session-info-container").on("mouseover", ".other-session", function (event) {
content.find(".session-info-container").on("mouseover", ".other-session", function () {
sessions.filter("#session" + this.dataset.othersessionid).addClass("highlight");
}).on("mouseleave", ".other-session", function (event) {
}).on("mouseleave", ".other-session", function () {
sessions.filter("#session" + this.dataset.othersessionid).removeClass("highlight");
});
});

View file

@ -1,4 +1,4 @@
/* Copyright The IETF Trust 2021, All Rights Reserved
/* Copyright The IETF Trust 2021-2022, All Rights Reserved
*
* JS support for the SessionDetailsForm
* */
@ -37,30 +37,27 @@
}
/* Update visibility of 'type' select so it is only shown when multiple options are available */
function update_widget_visibility(elt, purpose, allowed_types) {
function update_type_field_visibility(elt, purpose, allowed_types) {
const valid_types = allowed_types[purpose] || [];
if (valid_types.length > 1) {
elt.removeAttribute('hidden'); // make visible
elt.classList.remove('hidden'); // make visible
} else {
elt.setAttribute('hidden', 'hidden'); // make invisible
elt.classList.add('hidden'); // make invisible
}
}
/* Update the 'type' select to reflect a change in the selected purpose */
function update_type_element(type_elt, purpose, type_options, allowed_types) {
update_widget_visibility(type_elt, purpose, allowed_types);
update_type_field_visibility(type_elt.closest('.form-group'), purpose, allowed_types);
update_type_option_visibility(type_options, purpose, allowed_types);
set_valid_type(type_elt, purpose, allowed_types);
}
function update_name_field_visibility(name_elt, purpose) {
const row = name_elt.closest('tr');
if (row) {
if (purpose === 'regular') {
row.setAttribute('hidden', 'hidden');
} else {
row.removeAttribute('hidden');
}
function update_name_field_visibility(name_elts, purpose) {
if (!purpose || purpose === 'regular') {
name_elts.forEach(e => e.closest('.form-group').classList.add('hidden'));
} else {
name_elts.forEach(e => e.closest('.form-group').classList.remove('hidden'));
}
}
@ -79,7 +76,10 @@
if (purpose_elt.type === 'hidden') {
return; // element is hidden, so nothing to do
}
const name_elt = document.getElementById(id_prefix + 'name');
const name_elts = [
document.getElementById(id_prefix + 'name'),
document.getElementById(id_prefix + 'short'),
];
const type_elt = document.getElementById(id_prefix + 'type');
const type_options = type_elt.getElementsByTagName('option');
const allowed_types = (type_elt.dataset.allowedOptions) ?
@ -88,17 +88,17 @@
// update on future changes
purpose_elt.addEventListener(
'change',
purpose_change_handler(name_elt, type_elt, type_options, allowed_types)
purpose_change_handler(name_elts, type_elt, type_options, allowed_types)
);
// update immediately
update_type_element(type_elt, purpose_elt.value, type_options, allowed_types);
update_name_field_visibility(name_elt, purpose_elt.value);
update_name_field_visibility(name_elts, purpose_elt.value);
// hide the purpose selector if only one option
const purpose_options = purpose_elt.querySelectorAll('option:not([value=""])');
if (purpose_options.length < 2) {
purpose_elt.closest('tr').setAttribute('hidden', 'hidden');
purpose_elt.closest('.form-group').classList.add('hidden');
}
}

View file

@ -19,12 +19,9 @@
{# style off-agenda sessions to indicate this #}
.edit-meeting-schedule .session.off-agenda { filter: brightness(0.9); }
{# type and purpose styling #}
.edit-meeting-schedule .edit-grid .timeslot.wrong-timeslot-type,
.edit-meeting-schedule .edit-grid .timeslot.hidden-timeslot-type { background-color: transparent; ); }
.edit-meeting-schedule .edit-grid .timeslot.wrong-timeslot-type .time-label,
.edit-meeting-schedule .edit-grid .timeslot.hidden-timeslot-type .time-label { color: transparent; ); }
.edit-meeting-schedule .session.hidden-purpose,
.edit-meeting-schedule .session.hidden-timeslot-type { filter: blur(3px); }
.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 %}
@ -45,14 +42,17 @@
<p class="pull-right">
{% if can_edit_properties %}
<a href="{% url "ietf.meeting.views.edit_schedule_properties" schedule.meeting.number schedule.owner_email schedule.name %}">Edit properties</a>
&middot;
{% endif %}{% if user|has_role:"Secretariat" %}
<a href="{% url "ietf.meeting.views.edit_timeslots" num=meeting.number %}">
Edit timeslots
</a>
&middot;
{% endif %}
<a href="{% url "ietf.meeting.views.new_meeting_schedule" num=meeting.number owner=schedule.owner_email name=schedule.name %}">Copy agenda</a>
&middot;
<a href="{% url "ietf.meeting.views.list_schedules" num=meeting.number %}">Other Agendas</a>
<a href="{% url "ietf.meeting.views.list_schedules" num=meeting.number %}">Other agendas</a>
</p>
<p>
@ -85,7 +85,7 @@
</a>
</p>
{% else %}
<div class="edit-grid {% if not can_edit %}read-only{% endif %}">
<div class="edit-grid hidden {% if not can_edit %}read-only{% endif %}">
{# using the same markup in both room labels and the actual days ensures they are aligned #}
<div class="room-label-column">
{% for day_data in days.values %}
@ -99,7 +99,7 @@
<div class="room-group">
<div class="time-header"><div class="time-label"></div></div>
{% for room_data in rgroup %}{% with room_data.room as room %}
<div class="timeslots">
<div class="timeslots" data-room-id="{{ room.pk }}">
<div class="room-name">
<strong>{{ room.name }}</strong><br>
{% if room.capacity %}{{ room.capacity }} <i class="fa fa-user-o"></i>{% endif %}
@ -131,19 +131,23 @@
<div class="time-header">
{# All rooms in a group have same timeslots; grab the first for the labels #}
{% for t in rgroup.0.timeslots %}
<div class="time-label" style="width: {{ t.layout_width }}rem">
<div class="time-label"
style="width: {{ t.layout_width }}rem"
data-start="{{ t.utc_start_time.isoformat }}"
data-end="{{ t.utc_end_time.isoformat }}">
<span>
{{ t.time|date:"G:i" }} - {{ t.end_time|date:"G:i" }}
<i class="fa fa-exchange swap-timeslot-col"
data-origin-label="{{ day|date:"l, N j" }}, {{ t.time|date:"G:i" }}-{{ t.end_time|date:"G:i" }}"
data-start="{{ t.utc_start_time.isoformat }}"
data-timeslot-pk="{{ t.pk }}"></i>
</span>
</div>
{% endfor %}
</div>
{% for room_data in rgroup %}{% with room_data.room as room %}
<div class="timeslots" data-roomcapacity="{{ room.capacity }}">
<div class="timeslots"
data-room-id="{{ room.pk }}"
data-roomcapacity="{{ room.capacity }}">
{% for t in room_data.timeslots %}
<div id="timeslot{{ t.pk }}"
class="timeslot {{ t.start_end_group }}"
@ -204,10 +208,26 @@
{% endfor %}
</span>
{% if session_purposes|length > 1 %}
<button id="session-toggle-modal-open" class="btn btn-default" data-toggle="modal" data-target="#session-toggles-modal"><input type="checkbox" checked="checked" disabled> Sessions</button>
{% endif %}
<button id="timeslot-toggle-modal-open" class="btn btn-default" data-toggle="modal" data-target="#timeslot-group-toggles-modal"><input type="checkbox" checked="checked" disabled> Timeslots</button>
<button id="session-toggle-modal-open"
class="btn btn-default"
data-toggle="modal"
data-target="#session-toggles-modal">
<span class="fa fa-check-square"></span> Session Purposes
</button>
<button id="timeslot-toggle-modal-open"
class="btn btn-default"
data-toggle="modal"
data-target="#timeslot-group-toggles-modal">
<span class="fa fa-check-square"></span> Timeslots
</button>
<button id="timeslot-type-modal-open"
class="btn btn-default"
data-toggle="modal"
data-target="#timeslot-type-toggles-modal">
<span class="fa fa-check-square"></span> Timeslot Types
</button>
</div>
</div>
@ -225,39 +245,65 @@
<h4 class="modal-title" id="timeslot-group-toggles-modal-title">Displayed timeslots</h4>
</div>
<div class="modal-body">
<div class="timeslot-group-buttons">
<button type="button" class="btn btn-default select-all">Select all times</button>
<button type="button" class="btn btn-default clear-all">Clear times</button>
</div>
<div class="individual-timeslots">
<div class="modal-body">
<div class="individual-timeslots">
{% 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 %}
{% 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="timeslot-group-buttons">
<button type="button" class="btn btn-default select-all">Select all</button>
<button type="button" class="btn btn-default clear-all">Clear all</button>
</div>
{% endfor %}
</div>
<div class="timeslots-by-type timeslot-type-toggles">
Type:
{% for type in timeslot_types %}
<label class="timeslot-type-{{ type.slug }}"><input type="checkbox" checked value="{{ type.slug }}"> {{ type }}</label>
{% endfor %}
<button type="button" class="btn btn-default select-all">Select all types</button>
<button type="button" class="btn btn-default clear-all">Clear types</button>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal">Close</button>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div id="timeslot-type-toggles-modal" class="modal" role="dialog" aria-labelledby="timeslot-type-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-type-toggles-modal-title">Displayed types</h4>
</div>
<div class="modal-body">
<div class="timeslots-by-type timeslot-type-toggles">
{% for type in timeslot_types %}
<div class="timeslot-type-toggle">
<label class="timeslot-type-{{ type.slug }}">
<input type="checkbox"
value="{{ type.slug }}" {% if type.slug == 'regular' %}checked{% endif %}>
{{ type }}
</label>
</div>
{% endfor %}
<button type="button" class="btn btn-default select-all">Select all</button>
<button type="button" class="btn btn-default clear-all">Clear all</button>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div id="session-toggles-modal" class="modal" role="dialog" aria-labelledby="session-toggles-modal-title">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
@ -272,7 +318,7 @@
<div class="modal-body">
<div class="session-purpose-toggles">
{% for purpose in session_purposes %}
<div>
<div class="session-purpose-toggle">
<label class="purpose-{{ purpose.slug }}"><input type="checkbox" checked value="{% firstof purpose.slug 'none' %}"> {{ purpose }}</label>
</div>
{% endfor %}

View file

@ -41,6 +41,8 @@
{% else %} {# secr app room view requires a schedule - show something for now (should try to avoid this possibility) #}
<span title="Must create meeting schedule to edit rooms">Edit rooms</span>
{% endif %}
&middot;
<a href="{% url "ietf.meeting.views.list_schedules" num=meeting.number %}">Agenda list</a>
</p>
<p> IETF {{ meeting.number }} Meeting Agenda: Timeslots and Room Availability</p>