Update purpose/types after discussions, add on_agenda Session field, prevent session requests for groups with no allowed purpose, handle addition fields in session request, fix editing session requests, add session edit form/access from schedule editor, eliminate TimeSlotTypeName "private" field, add server-side timeslot type filtering to schedule editor

- Legacy-Id: 19549
This commit is contained in:
Jennifer Richards 2021-11-04 17:01:32 +00:00
parent 5cbe402036
commit 3dfce7b850
21 changed files with 323 additions and 89 deletions

View file

@ -7,17 +7,23 @@ from django.db import migrations
default_purposes = dict(
adhoc=['presentation'],
adm=['closed_meeting', 'officehours'],
adm=['closed_meeting', 'office_hours'],
ag=['regular'],
area=['regular'],
dir=['presentation', 'social', 'tutorial', 'regular'],
dir=['open_meeting', 'presentation', 'regular', 'social', 'tutorial'],
iab=['closed_meeting', 'regular'],
iabasg=['open_meeting', 'closed_meeting'],
iabasg=['closed_meeting', 'open_meeting'],
iana=['office_hours'],
iesg=['closed_meeting', 'open_meeting'],
ietf=['admin', 'plenary', 'presentation', 'social'],
nomcom=['closed_meeting', 'officehours'],
irtf=[],
ise=['office_hours'],
isoc=['office_hours', 'open_meeting', 'presentation'],
nomcom=['closed_meeting', 'office_hours'],
program=['regular', 'tutorial'],
rag=['regular'],
review=['open_meeting', 'social'],
rfcedtyp=['office_hours'],
rg=['regular'],
team=['coding', 'presentation', 'social', 'tutorial'],
wg=['regular'],

View file

@ -0,0 +1,18 @@
# Generated by Django 2.2.24 on 2021-10-22 06:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('meeting', '0048_session_purpose'),
]
operations = [
migrations.AddField(
model_name='session',
name='on_agenda',
field=models.BooleanField(default=True, help_text='Is this session visible on the meeting agenda?'),
),
]

View file

@ -0,0 +1,37 @@
# Generated by Django 2.2.24 on 2021-10-22 06:58
from django.db import migrations, models
def forward(apps, schema_editor):
Session = apps.get_model('meeting', 'Session')
SchedTimeSessAssignment = apps.get_model('meeting', 'SchedTimeSessAssignment')
# find official assignments that are to private timeslots and fill in session.on_agenda
private_assignments = SchedTimeSessAssignment.objects.filter(
models.Q(
schedule=models.F('session__meeting__schedule')
) | models.Q(
schedule=models.F('session__meeting__schedule__base')
),
timeslot__type__private=True,
)
Session.objects.filter(timeslotassignments__in=private_assignments).update(on_agenda=False)
# Also update any sessions to match their purpose's default setting (this intentionally
# overrides the timeslot settings above, but that is unlikely to matter because the
# purposes will roll out at the same time as the on_agenda field)
Session.objects.filter(purpose__on_agenda=False).update(on_agenda=False)
Session.objects.filter(purpose__on_agenda=True).update(on_agenda=True)
def reverse(apps, schema_editor):
Session = apps.get_model('meeting', 'Session')
Session.objects.update(on_agenda=True) # restore all to default on_agenda=True state
class Migration(migrations.Migration):
dependencies = [
('meeting', '0049_session_on_agenda'),
]
operations = [
migrations.RunPython(forward, reverse),
]

View file

@ -1173,6 +1173,7 @@ class Session(models.Model):
scheduled = models.DateTimeField(null=True, blank=True)
modified = models.DateTimeField(auto_now=True)
remote_instructions = models.CharField(blank=True,max_length=1024)
on_agenda = models.BooleanField(default=True, help_text='Is this session visible on the meeting agenda?')
tombstone_for = models.ForeignKey('Session', blank=True, null=True, help_text="This session is the tombstone for a session that was rescheduled", on_delete=models.CASCADE)

View file

@ -430,7 +430,7 @@ class MeetingTests(BaseMeetingTestCase):
q = PyQuery(r.content)
for assignment in SchedTimeSessAssignment.objects.filter(
schedule__in=[meeting.schedule, meeting.schedule.base],
timeslot__type__private=False,
session__on_agenda=True,
):
row = q('#row-{}'.format(assignment.slug()))
self.assertIsNotNone(row, 'No row for assignment {}'.format(assignment))

View file

@ -146,6 +146,7 @@ urlpatterns = [
url(r'^upcoming\.ics/?$', views.upcoming_ical),
url(r'^upcoming\.json/?$', views.upcoming_json),
url(r'^session/(?P<session_id>\d+)/agenda_materials$', views.session_materials),
url(r'^session/(?P<session_id>\d+)/edit/?', views.edit_session),
# Then patterns from more specific to less
url(r'^(?P<num>interim-[a-z0-9-]+)/', include(type_interim_patterns)),
url(r'^(?P<num>\d+)/requests.html$', RedirectView.as_view(url='/meeting/%(num)s/requests', permanent=True)),

View file

@ -58,7 +58,7 @@ from ietf.mailtrigger.utils import gather_address_lists
from ietf.meeting.models import Meeting, Session, Schedule, FloorPlan, SessionPresentation, TimeSlot, SlideSubmission
from ietf.meeting.models import SessionStatusName, SchedulingEvent, SchedTimeSessAssignment, Room, TimeSlotTypeName
from ietf.meeting.forms import ( CustomDurationField, SwapDaysForm, SwapTimeslotsForm,
TimeSlotCreateForm, TimeSlotEditForm )
TimeSlotCreateForm, TimeSlotEditForm, SessionEditForm )
from ietf.meeting.helpers import get_areas, get_person_by_email, get_schedule_by_name
from ietf.meeting.helpers import build_all_agenda_slices, get_wg_name_list
from ietf.meeting.helpers import get_all_assignments_from_schedule
@ -500,6 +500,16 @@ def new_meeting_schedule(request, num, owner=None, name=None):
@ensure_csrf_cookie
def edit_meeting_schedule(request, num=None, owner=None, name=None):
"""Schedule editor
In addition to the URL parameters, accepts a query string parameter 'type'.
If present, only sessions/timeslots with a TimeSlotTypeName with that slug
will be included in the editor. More than one type can be enabled by passing
multiple type parameters.
?type=regular - shows only regular sessions/timeslots (i.e., old editor behavior)
?type=regular&type=other - shows both regular and other sessions/timeslots
"""
# 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')
@ -532,11 +542,19 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
"hide_menu": True
}, status=403, content_type="text/html")
# See if we were given one or more 'type' query string parameters. If so, filter to that timeslot type.
if 'type' in request.GET:
include_timeslot_types = request.GET.getlist('type')
else:
include_timeslot_types = None # disables filtering by type (other than IGNORE_TIMESLOT_TYPES)
assignments = SchedTimeSessAssignment.objects.filter(
schedule__in=[schedule, schedule.base],
timeslot__location__isnull=False,
# session__type='regular',
).order_by('timeslot__time','timeslot__name')
)
if include_timeslot_types is not None:
assignments = assignments.filter(session__type__in=include_timeslot_types)
assignments = assignments.order_by('timeslot__time','timeslot__name')
assignments_by_session = defaultdict(list)
for a in assignments:
@ -544,10 +562,11 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
tombstone_states = ['canceled', 'canceledpa', 'resched']
sessions = Session.objects.filter(meeting=meeting)
if include_timeslot_types is not None:
sessions = sessions.filter(type__in=include_timeslot_types)
sessions = add_event_info_to_session_qs(
Session.objects.filter(
meeting=meeting,
).exclude(
sessions.exclude(
type__in=IGNORE_TIMESLOT_TYPES,
).order_by('pk'),
requested_time=True,
@ -559,14 +578,19 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
'resources', 'group', 'group__parent', 'group__type', 'joint_with_groups', 'purpose',
)
timeslots_qs = TimeSlot.objects.filter(
meeting=meeting,
).exclude(
timeslots_qs = TimeSlot.objects.filter(meeting=meeting)
if include_timeslot_types is not None:
timeslots_qs = timeslots_qs.filter(type__in=include_timeslot_types)
timeslots_qs = timeslots_qs.exclude(
type__in=IGNORE_TIMESLOT_TYPES,
).prefetch_related('type').order_by('location', 'time', 'name')
if timeslots_qs.count() > 0:
min_duration = min(t.duration for t in timeslots_qs)
max_duration = max(t.duration for t in timeslots_qs)
else:
min_duration = 1
max_duration = 2
def timedelta_to_css_ems(timedelta):
# we scale the session and slots a bit according to their
@ -707,7 +731,10 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
all_days = sorted(all_days) # changes set to a list
# Note the maximum timeslot count for any room
if len(room_data) > 0:
max_timeslots = max(rd['timeslot_count'] for rd in room_data.values())
else:
max_timeslots = 0
# Partition rooms into groups with identical timeslot arrangements.
# Start by discarding any roos that have no timeslots.
@ -920,7 +947,10 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
return _json_response(False, error="Invalid parameters")
# Show only rooms that have regular sessions
rooms = meeting.room_set.filter(session_types__slug='regular')
if include_timeslot_types is None:
rooms = meeting.room_set.all()
else:
rooms = meeting.room_set.filter(session_types__slug__in=include_timeslot_types)
# Construct timeslot data for the template to render
days = prepare_timeslots_for_display(timeslots_qs, rooms)
@ -1583,7 +1613,7 @@ def get_assignments_for_agenda(schedule):
"""Get queryset containing assignments to show on the agenda"""
return SchedTimeSessAssignment.objects.filter(
schedule__in=[schedule, schedule.base],
timeslot__type__private=False,
session__on_agenda=True,
)
@ -1938,7 +1968,7 @@ def week_view(request, num=None, name=None, owner=None):
filtered_assignments = SchedTimeSessAssignment.objects.filter(
schedule__in=[schedule, schedule.base],
timeslot__type__private=False,
session__on_agenda=True,
)
filtered_assignments = preprocess_assignments_for_agenda(filtered_assignments, meeting)
AgendaKeywordTagger(assignments=filtered_assignments).apply()
@ -2121,7 +2151,7 @@ def agenda_ical(request, num=None, name=None, acronym=None, session_id=None):
assignments = SchedTimeSessAssignment.objects.filter(
schedule__in=[schedule, schedule.base],
timeslot__type__private=False,
session__on_agenda=True,
)
assignments = preprocess_assignments_for_agenda(assignments, meeting)
AgendaKeywordTagger(assignments=assignments).apply()
@ -2159,7 +2189,7 @@ def agenda_json(request, num=None):
parent_acronyms = set()
assignments = SchedTimeSessAssignment.objects.filter(
schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None],
timeslot__type__private=False,
session__on_agenda=True,
).exclude(
session__type__in=['break', 'reg']
)
@ -4098,6 +4128,24 @@ def create_timeslot(request, num):
)
@role_required('Secretariat')
def edit_session(request, session_id):
session = get_object_or_404(Session, pk=session_id)
if request.method == 'POST':
form = SessionEditForm(instance=session, data=request.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect(
reverse('ietf.meeting.views.edit_meeting_schedule',
kwargs={'num': form.instance.meeting.number}))
else:
form = SessionEditForm(instance=session)
return render(
request,
'meeting/edit_session.html',
{'session': session, 'form': form},
)
@role_required('Secretariat')
def request_minutes(request, num=None):
meeting = get_ietf_meeting(num)

View file

@ -23,6 +23,7 @@ class Migration(migrations.Migration):
('used', models.BooleanField(default=True)),
('order', models.IntegerField(default=0)),
('timeslot_types', jsonfield.fields.JSONField(default=[], help_text='Allowed TimeSlotTypeNames', max_length=256, validators=[ietf.name.models.JSONForeignKeyListValidator('name.TimeSlotTypeName')])),
('on_agenda', models.BooleanField(default=True, help_text='Are sessions of this purpose visible on the agenda by default?')),
],
options={
'ordering': ['order', 'name'],

View file

@ -9,19 +9,19 @@ def forward(apps, schema_editor):
SessionPurposeName = apps.get_model('name', 'SessionPurposeName')
TimeSlotTypeName = apps.get_model('name', 'TimeSlotTypeName')
for order, (slug, name, desc, tstypes) in enumerate((
('regular', 'Regular', 'Regular group session', ['regular']),
('tutorial', 'Tutorial', 'Tutorial or training session', ['other']),
('officehours', 'Office hours', 'Office hours session', ['other']),
('coding', 'Coding', 'Coding session', ['other']),
('admin', 'Administrative', 'Meeting administration', ['other', 'reg']),
('social', 'Social', 'Social event or activity', ['break', 'other']),
('plenary', 'Plenary', 'Plenary session', ['plenary']),
('presentation', 'Presentation', 'Presentation session', ['other', 'regular']),
('open_meeting', 'Open meeting', 'Open meeting', ['other']),
('closed_meeting', 'Closed meeting', 'Closed meeting', ['other', 'regular']),
for order, (slug, name, desc, tstypes, on_agenda) in enumerate((
('regular', 'Regular', 'Regular group session', ['regular'], True),
('tutorial', 'Tutorial', 'Tutorial or training session', ['other'], True),
('office_hours', 'Office hours', 'Office hours session', ['other'], True),
('coding', 'Coding', 'Coding session', ['other'], True),
('admin', 'Administrative', 'Meeting administration', ['other', 'reg'], True),
('social', 'Social', 'Social event or activity', ['break', 'other'], True),
('plenary', 'Plenary', 'Plenary session', ['plenary'], True),
('presentation', 'Presentation', 'Presentation session', ['other', 'regular'], True),
('open_meeting', 'Open meeting', 'Open meeting', ['other'], True),
('closed_meeting', 'Closed meeting', 'Closed meeting', ['other', 'regular'], False),
)):
# verify that we're not about to use an invalid purpose
# verify that we're not about to use an invalid type
for ts_type in tstypes:
TimeSlotTypeName.objects.get(pk=ts_type) # throws an exception unless exists
@ -31,7 +31,8 @@ def forward(apps, schema_editor):
desc=desc,
used=True,
order=order,
timeslot_types = tstypes
timeslot_types = tstypes,
on_agenda=on_agenda,
)

View file

@ -0,0 +1,31 @@
# Generated by Django 2.2.24 on 2021-10-25 16:58
from django.db import migrations
PRIVATE_TIMESLOT_SLUGS = {'lead', 'offagenda'} # from DB 2021 Oct
def forward(apps, schema_editor):
TimeSlotTypeName = apps.get_model('name', 'TimeSlotTypeName')
slugs = TimeSlotTypeName.objects.filter(private=True).values_list('slug', flat=True)
if set(slugs) != PRIVATE_TIMESLOT_SLUGS:
# the reverse migration will not restore the database, refuse to migrate
raise ValueError('Disagreement between migration data and database')
def reverse(apps, schema_editor):
TimeSlotTypeName = apps.get_model('name', 'TimeSlotTypeName')
TimeSlotTypeName.objects.filter(slug__in=PRIVATE_TIMESLOT_SLUGS).update(private=True)
class Migration(migrations.Migration):
dependencies = [
('name', '0036_populate_sessionpurposename'),
('meeting', '0050_populate_session_on_agenda'),
]
operations = [
migrations.RunPython(forward, reverse),
]

View file

@ -0,0 +1,17 @@
# Generated by Django 2.2.24 on 2021-10-25 17:23
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('name', '0037_depopulate_timeslottypename_private'),
]
operations = [
migrations.RemoveField(
model_name='timeslottypename',
name='private',
),
]

View file

@ -78,9 +78,10 @@ class SessionPurposeName(NameModel):
help_text='Allowed TimeSlotTypeNames',
validators=[JSONForeignKeyListValidator('name.TimeSlotTypeName')],
)
on_agenda = models.BooleanField(default=True, help_text='Are sessions of this purpose visible on the agenda by default?')
class TimeSlotTypeName(NameModel):
"""Session, Break, Registration, Other, Reserved, unavail"""
private = models.BooleanField(default=False, help_text="Whether sessions of this type should be kept off the public agenda")
class ConstraintName(NameModel):
"""conflict, conflic2, conflic3, bethere, timerange, time_relation, wg_adjacent"""
penalty = models.IntegerField(default=0, help_text="The penalty for violating this kind of constraint; for instance 10 (small penalty) or 10000 (large penalty)")

View file

@ -287,7 +287,7 @@ class SessionForm(forms.Form):
@property
def media(self):
# get media for our formset
return super().media + self.session_forms.media
return super().media + self.session_forms.media + forms.Media(js=('secr/js/session_form.js',))
class VirtualSessionForm(SessionForm):

View file

@ -60,8 +60,13 @@ def get_initial_session(sessions, prune_conflicts=False):
constraints = group.constraint_source_set.filter(meeting=meeting) # all constraints with this group as source
conflicts = constraints.filter(name__is_group_conflict=True) # only the group conflict constraints
if group.features.acts_like_wg:
# even if there are three sessions requested, the old form has 2 in this field
initial['num_session'] = min(sessions.count(), 2) if group.features.acts_like_wg else sessions.count()
initial['num_session'] = min(sessions.count(), 2)
initial['third_session'] = sessions.count() > 2
else:
initial['num_session'] = sessions.count()
initial['third_session'] = False
initial['attendees'] = sessions[0].attendees
def valid_conflict(conflict):
@ -274,6 +279,8 @@ def confirm(request, acronym):
'''
# FIXME: this should be using form.is_valid/form.cleaned_data - invalid input will make it crash
group = get_object_or_404(Group,acronym=acronym)
if len(group.features.session_purposes) == 0:
raise Http404(f'Cannot request sessions for group "{acronym}"')
meeting = get_meeting(days=14)
FormClass = get_session_form_class()
@ -316,18 +323,9 @@ def confirm(request, acronym):
add_event_info_to_session_qs(Session.objects.filter(group=group, meeting=meeting)).filter(current_status__in=['canceled', 'notmeet']).delete()
num_sessions = int(form.cleaned_data['num_session']) + (1 if form.cleaned_data['third_session'] else 0)
# Create new session records
# Should really use sess_form.save(), but needs data from the main form as well. Need to sort that out properly.
form.session_forms.save()
for count, sess_form in enumerate(form.session_forms[:num_sessions]):
new_session = Session.objects.create(
meeting=meeting,
group=group,
attendees=form.cleaned_data['attendees'],
requested_duration=sess_form.cleaned_data['requested_duration'],
name=sess_form.cleaned_data['name'],
comments=form.cleaned_data['comments'],
purpose=sess_form.cleaned_data['purpose'],
type=sess_form.cleaned_data['type'],
)
new_session = sess_form.instance
SchedulingEvent.objects.create(
session=new_session,
status=SessionStatusName.objects.get(slug=status_slug_for_new_session(new_session, count)),
@ -342,6 +340,7 @@ def confirm(request, acronym):
groups_split = form.cleaned_data.get('joint_with_groups').replace(',',' ').split()
joint = Group.objects.filter(acronym__in=groups_split)
new_session.joint_with_groups.set(joint)
new_session.save()
session_changed(new_session)
# write constraint records
@ -413,6 +412,8 @@ def edit(request, acronym, num=None):
'''
meeting = get_meeting(num,days=14)
group = get_object_or_404(Group, acronym=acronym)
if len(group.features.session_purposes) == 0:
raise Http404(f'Cannot request sessions for group "{acronym}"')
sessions = add_event_info_to_session_qs(
Session.objects.filter(group=group, meeting=meeting)
).filter(
@ -449,10 +450,12 @@ def edit(request, acronym, num=None):
if form.has_changed():
changed_session_forms = [sf for sf in form.session_forms.forms_to_keep if sf.has_changed()]
form.session_forms.save()
for n, new_session in enumerate(form.session_forms.created_instances):
for n, subform in enumerate(form.session_forms):
session = subform.instance
if session in form.session_forms.created_instances:
SchedulingEvent.objects.create(
session=new_session,
status_id=status_slug_for_new_session(new_session, n),
session=session,
status_id=status_slug_for_new_session(session, n),
by=request.user.person,
)
for sf in changed_session_forms:
@ -638,6 +641,8 @@ def new(request, acronym):
to create the request.
'''
group = get_object_or_404(Group, acronym=acronym)
if len(group.features.session_purposes) == 0:
raise Http404(f'Cannot request sessions for group "{acronym}"')
meeting = get_meeting(days=14)
session_conflicts = dict(inbound=inbound_session_conflicts_as_string(group, meeting))
is_virtual = meeting.number in settings.SECR_VIRTUAL_MEETINGS,

View file

@ -0,0 +1,28 @@
/* Copyright The IETF Trust 2021, All Rights Reserved
*
* JS support for the SessionForm
* */
(function() {
'use strict';
function track_common_input(input, name_suffix) {
const handler = function() {
const hidden_inputs = document.querySelectorAll(
'.session-details-form input[name$="-' + name_suffix + '"]'
);
for (let hi of hidden_inputs) {
hi.value = input.value;
}
};
input.addEventListener('change', handler);
handler();
}
function initialize() {
// Keep all the hidden inputs in sync with the main form
track_common_input(document.getElementById('id_attendees'), 'attendees');
track_common_input(document.getElementById('id_comments'), 'comments');
}
window.addEventListener('load', initialize);
})();

View file

@ -13,7 +13,7 @@
<dt>Purpose</dt>
<dd>
{{ sess_form.cleaned_data.purpose }}
{% if sess_form.cleaned_data.purpose.timeslot_types|length > 1 %}({{ sess_form.cleaned_data.type }}{% endif %}
{% if sess_form.cleaned_data.purpose.timeslot_types|length > 1 %}({{ sess_form.cleaned_data.type }}){% endif %}
</dd>
{% endif %}
</dl>

View file

@ -55,12 +55,14 @@
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');
}
}
}
/* Factory for event handler with a closure */
function purpose_change_handler(name_elt, type_elt, type_options, allowed_types) {
@ -72,12 +74,16 @@
}
function add_purpose_change_handler(form) {
const id_prefix = 'id_' + form.dataset.prefix;
const name_elt = document.getElementById(id_prefix + '-name');
const purpose_elt = document.getElementById(id_prefix + '-purpose');
const type_elt = document.getElementById(id_prefix + '-type');
const id_prefix = 'id_' + (form.dataset.prefix ? (form.dataset.prefix + '-') : '');
const purpose_elt = document.getElementById(id_prefix + 'purpose');
if (purpose_elt.type === 'hidden') {
return; // element is hidden, so nothing to do
}
const name_elt = document.getElementById(id_prefix + 'name');
const type_elt = document.getElementById(id_prefix + 'type');
const type_options = type_elt.getElementsByTagName('option');
const allowed_types = JSON.parse(type_elt.dataset.allowedOptions);
const allowed_types = (type_elt.dataset.allowedOptions) ?
JSON.parse(type_elt.dataset.allowedOptions) : [];
// update on future changes
purpose_elt.addEventListener(

View file

@ -16,6 +16,8 @@
.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; }
{# 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; ); }

View file

@ -1,5 +1,5 @@
<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 %} purpose-{% firstof session.purpose.slug 'regular' %} {% if session.readonly %}readonly{% endif %}"
class="session {% if not session.group.parent.scheduling_color %}untoggleable-by-parent{% endif %} {% if session.parent_acronym %}parent-{{ session.parent_acronym }}{% endif %} purpose-{% firstof session.purpose.slug 'regular' %} {% if session.readonly %}readonly{% endif %} {% if not session.on_agenda %}off-agenda{% endif %}"
style="width:{{ session.layout_width }}em;"
data-duration="{{ session.requested_duration.total_seconds }}" {% if session.attendees != None %}
data-attendees="{{ session.attendees }}"{% endif %}
@ -47,6 +47,7 @@
{% if session.group.parent %}
&middot; <span class="session-parent">{{ session.group.parent.acronym }}</span>
{% endif %}
{% if not session.on_agenda %}&middot; <i>off agenda</i>{% endif %}
</div>
{% endif %}
@ -84,5 +85,7 @@
{% for s in session.other_sessions %}
<div class="other-session" data-othersessionid="{{ s.pk }}"><i class="fa fa-calendar"></i> Other session <span class="time" data-scheduled="scheduled: {time}" data-notscheduled="not yet scheduled"></span></div>
{% endfor %}
<a href="{% url 'ietf.meeting.views.edit_session' session_id=session.pk %}">Edit session</a>
</div>
</div>

View file

@ -0,0 +1,27 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2021, All Rights Reserved #}
{% load origin %}
{% load bootstrap3 %}
{% block pagehead %}
{{ form.media.css }}
{% endblock %}
{% block title %}Edit session "{{ session }}"{% endblock %}
{% block content %}
{% origin %}
<h1>Edit session "{{ session }}"</h1>
<form class="session-details-form" method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button type="submit" class="btn btn-primary">Save</button>
<a class="btn btn-default" href="{% url 'ietf.meeting.views.edit_meeting_schedule' num=session.meeting.number %}">Cancel</a>
{% endbuttons %}
</form>
{% endblock %}
{% block js %}
{{ form.media.js }}
{% endblock %}

View file

@ -1,8 +1,7 @@
{# Copyright The IETF Trust 2007-2020, All Rights Reserved #}
<div class="session-details-form" data-prefix="{{ form.prefix }}">
{% if hidden %}{{ form.name.as_hidden }}{{ form.purpose.as_hidden }}{{ form.type.as_hidden }}{{ form.requested_duration.as_hidden }}
{% else %}<div class="session-details-form" data-prefix="{{ form.prefix }}">
{{ form.id.as_hidden }}
{{ form.DELETE.as_hidden }}
{% else %}
<table>
<tr>
<th>{{ form.name.label_tag }}</th>
@ -14,11 +13,13 @@
{{ form.purpose }} {{ form.type }}
{{ form.purpose.errors }}{{ form.type.errors }}
</td>
</tr>
<tr>
<th>{{ form.requested_duration.label_tag }}</th>
<td>{{ form.requested_duration }}{{ form.requested_duration.errors }}</td>
</tr>
</tr>
</table>
</div>
{% endif %}
{# hidden fields shown whether or not the whole form is hidden #}
{{ form.attendees.as_hidden }}{{ form.comments.as_hidden }}{{ form.id.as_hidden }}{{ form.on_agenda.as_hidden }}{{ form.DELETE.as_hidden }}
</div>