From 3dfce7b850ea2366171d787d20d275fe35373212 Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Thu, 4 Nov 2021 17:01:32 +0000 Subject: [PATCH] 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 --- ...populate_groupfeatures_session_purposes.py | 14 +++- .../migrations/0049_session_on_agenda.py | 18 ++++ .../0050_populate_session_on_agenda.py | 37 +++++++++ ietf/meeting/models.py | 1 + ietf/meeting/tests_views.py | 2 +- ietf/meeting/urls.py | 1 + ietf/meeting/views.py | 82 +++++++++++++++---- .../migrations/0035_sessionpurposename.py | 1 + .../0036_populate_sessionpurposename.py | 27 +++--- ...037_depopulate_timeslottypename_private.py | 31 +++++++ .../0038_remove_timeslottypename_private.py | 17 ++++ ietf/name/models.py | 3 +- ietf/secr/sreq/forms.py | 2 +- ietf/secr/sreq/views.py | 43 +++++----- ietf/secr/static/secr/js/session_form.js | 28 +++++++ .../includes/sessions_request_view.html | 2 +- .../ietf/js/meeting/session_details_form.js | 24 ++++-- .../meeting/edit_meeting_schedule.html | 2 + .../edit_meeting_schedule_session.html | 5 +- ietf/templates/meeting/edit_session.html | 27 ++++++ .../meeting/session_details_form.html | 45 +++++----- 21 files changed, 323 insertions(+), 89 deletions(-) create mode 100644 ietf/meeting/migrations/0049_session_on_agenda.py create mode 100644 ietf/meeting/migrations/0050_populate_session_on_agenda.py create mode 100644 ietf/name/migrations/0037_depopulate_timeslottypename_private.py create mode 100644 ietf/name/migrations/0038_remove_timeslottypename_private.py create mode 100644 ietf/secr/static/secr/js/session_form.js create mode 100644 ietf/templates/meeting/edit_session.html diff --git a/ietf/group/migrations/0052_populate_groupfeatures_session_purposes.py b/ietf/group/migrations/0052_populate_groupfeatures_session_purposes.py index ba7a93ea43..86bee4a15e 100644 --- a/ietf/group/migrations/0052_populate_groupfeatures_session_purposes.py +++ b/ietf/group/migrations/0052_populate_groupfeatures_session_purposes.py @@ -7,17 +7,23 @@ 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'], diff --git a/ietf/meeting/migrations/0049_session_on_agenda.py b/ietf/meeting/migrations/0049_session_on_agenda.py new file mode 100644 index 0000000000..ee0376a9e1 --- /dev/null +++ b/ietf/meeting/migrations/0049_session_on_agenda.py @@ -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?'), + ), + ] diff --git a/ietf/meeting/migrations/0050_populate_session_on_agenda.py b/ietf/meeting/migrations/0050_populate_session_on_agenda.py new file mode 100644 index 0000000000..286035fa18 --- /dev/null +++ b/ietf/meeting/migrations/0050_populate_session_on_agenda.py @@ -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), + ] diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py index 964a02979c..20db83b69c 100644 --- a/ietf/meeting/models.py +++ b/ietf/meeting/models.py @@ -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) diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index 498b8298a1..fda407ada7 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -430,7 +430,7 @@ def test_agenda_personalize(self): 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)) diff --git a/ietf/meeting/urls.py b/ietf/meeting/urls.py index c9c388c0e6..f87870d3de 100644 --- a/ietf/meeting/urls.py +++ b/ietf/meeting/urls.py @@ -146,6 +146,7 @@ url(r'^upcoming\.ics/?$', views.upcoming_ical), url(r'^upcoming\.json/?$', views.upcoming_json), url(r'^session/(?P\d+)/agenda_materials$', views.session_materials), + url(r'^session/(?P\d+)/edit/?', views.edit_session), # Then patterns from more specific to less url(r'^(?Pinterim-[a-z0-9-]+)/', include(type_interim_patterns)), url(r'^(?P\d+)/requests.html$', RedirectView.as_view(url='/meeting/%(num)s/requests', permanent=True)), diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index 6e9fb0664e..65315ba460 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -58,7 +58,7 @@ 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 timeslot_locked(ts): "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 timeslot_locked(ts): 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 timeslot_locked(ts): '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') - min_duration = min(t.duration for t in timeslots_qs) - max_duration = max(t.duration for t in timeslots_qs) + 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 prepare_timeslots_for_display(timeslots, rooms): all_days = sorted(all_days) # changes set to a list # Note the maximum timeslot count for any room - max_timeslots = max(rd['timeslot_count'] for rd in room_data.values()) + 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 _json_response(success, status=None, **extra_data): 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) diff --git a/ietf/name/migrations/0035_sessionpurposename.py b/ietf/name/migrations/0035_sessionpurposename.py index 52d3ed82c6..ebb970d60b 100644 --- a/ietf/name/migrations/0035_sessionpurposename.py +++ b/ietf/name/migrations/0035_sessionpurposename.py @@ -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'], diff --git a/ietf/name/migrations/0036_populate_sessionpurposename.py b/ietf/name/migrations/0036_populate_sessionpurposename.py index a8304b86d4..75034ee172 100644 --- a/ietf/name/migrations/0036_populate_sessionpurposename.py +++ b/ietf/name/migrations/0036_populate_sessionpurposename.py @@ -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, ) diff --git a/ietf/name/migrations/0037_depopulate_timeslottypename_private.py b/ietf/name/migrations/0037_depopulate_timeslottypename_private.py new file mode 100644 index 0000000000..09407b2f43 --- /dev/null +++ b/ietf/name/migrations/0037_depopulate_timeslottypename_private.py @@ -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), + ] diff --git a/ietf/name/migrations/0038_remove_timeslottypename_private.py b/ietf/name/migrations/0038_remove_timeslottypename_private.py new file mode 100644 index 0000000000..0c2a9578df --- /dev/null +++ b/ietf/name/migrations/0038_remove_timeslottypename_private.py @@ -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', + ), + ] diff --git a/ietf/name/models.py b/ietf/name/models.py index a06b789430..e797c77822 100644 --- a/ietf/name/models.py +++ b/ietf/name/models.py @@ -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)") diff --git a/ietf/secr/sreq/forms.py b/ietf/secr/sreq/forms.py index 9bcf9ac8d4..ff67e8b69e 100644 --- a/ietf/secr/sreq/forms.py +++ b/ietf/secr/sreq/forms.py @@ -287,7 +287,7 @@ def clean(self): @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): diff --git a/ietf/secr/sreq/views.py b/ietf/secr/sreq/views.py index f3a806acd7..1b344a1ab1 100644 --- a/ietf/secr/sreq/views.py +++ b/ietf/secr/sreq/views.py @@ -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 - # 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() + 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) + 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,12 +450,14 @@ 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): - SchedulingEvent.objects.create( - session=new_session, - status_id=status_slug_for_new_session(new_session, n), - by=request.user.person, - ) + for n, subform in enumerate(form.session_forms): + session = subform.instance + if session in form.session_forms.created_instances: + SchedulingEvent.objects.create( + session=session, + status_id=status_slug_for_new_session(session, n), + by=request.user.person, + ) for sf in changed_session_forms: session_changed(sf.instance) @@ -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, diff --git a/ietf/secr/static/secr/js/session_form.js b/ietf/secr/static/secr/js/session_form.js new file mode 100644 index 0000000000..6f28f16db4 --- /dev/null +++ b/ietf/secr/static/secr/js/session_form.js @@ -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); +})(); \ No newline at end of file diff --git a/ietf/secr/templates/includes/sessions_request_view.html b/ietf/secr/templates/includes/sessions_request_view.html index 0f5bfca8f2..bb19f5e2a5 100644 --- a/ietf/secr/templates/includes/sessions_request_view.html +++ b/ietf/secr/templates/includes/sessions_request_view.html @@ -13,7 +13,7 @@
Purpose
{{ 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 %}
{% endif %} diff --git a/ietf/static/ietf/js/meeting/session_details_form.js b/ietf/static/ietf/js/meeting/session_details_form.js index 8bce860b09..5016a0139a 100644 --- a/ietf/static/ietf/js/meeting/session_details_form.js +++ b/ietf/static/ietf/js/meeting/session_details_form.js @@ -55,10 +55,12 @@ function update_name_field_visibility(name_elt, purpose) { const row = name_elt.closest('tr'); - if (purpose === 'regular') { - row.setAttribute('hidden', 'hidden'); - } else { - row.removeAttribute('hidden'); + if (row) { + if (purpose === 'regular') { + row.setAttribute('hidden', 'hidden'); + } else { + row.removeAttribute('hidden'); + } } } @@ -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( diff --git a/ietf/templates/meeting/edit_meeting_schedule.html b/ietf/templates/meeting/edit_meeting_schedule.html index 6f6fd6c0bb..a4b22b9dfe 100644 --- a/ietf/templates/meeting/edit_meeting_schedule.html +++ b/ietf/templates/meeting/edit_meeting_schedule.html @@ -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; ); } diff --git a/ietf/templates/meeting/edit_meeting_schedule_session.html b/ietf/templates/meeting/edit_meeting_schedule_session.html index 472138e26e..f2226fda64 100644 --- a/ietf/templates/meeting/edit_meeting_schedule_session.html +++ b/ietf/templates/meeting/edit_meeting_schedule_session.html @@ -1,5 +1,5 @@
{{ session.group.parent.acronym }} {% endif %} + {% if not session.on_agenda %}· off agenda{% endif %}
{% endif %} @@ -84,5 +85,7 @@ {% for s in session.other_sessions %}
Other session
{% endfor %} + + Edit session diff --git a/ietf/templates/meeting/edit_session.html b/ietf/templates/meeting/edit_session.html new file mode 100644 index 0000000000..35e8eba1d9 --- /dev/null +++ b/ietf/templates/meeting/edit_session.html @@ -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 %} +

Edit session "{{ session }}"

+
+ {% csrf_token %} + {% bootstrap_form form %} + {% buttons %} + + Cancel + {% endbuttons %} +
+{% endblock %} + +{% block js %} + {{ form.media.js }} +{% endblock %} \ No newline at end of file diff --git a/ietf/templates/meeting/session_details_form.html b/ietf/templates/meeting/session_details_form.html index cb69d88589..daafdac938 100644 --- a/ietf/templates/meeting/session_details_form.html +++ b/ietf/templates/meeting/session_details_form.html @@ -1,24 +1,25 @@ {# Copyright The IETF Trust 2007-2020, All Rights Reserved #} -{% if hidden %}{{ form.name.as_hidden }}{{ form.purpose.as_hidden }}{{ form.type.as_hidden }}{{ form.requested_duration.as_hidden }} -{% else %}
- {{ form.id.as_hidden }} - {{ form.DELETE.as_hidden }} - - - - - - - - - - - - - -
{{ form.name.label_tag }}{{ form.name }}{{ form.purpose.errors }}
{{ form.purpose.label_tag }} - {{ form.purpose }} {{ form.type }} - {{ form.purpose.errors }}{{ form.type.errors }} -
{{ form.requested_duration.label_tag }}{{ form.requested_duration }}{{ form.requested_duration.errors }}
+
+ {% if hidden %}{{ form.name.as_hidden }}{{ form.purpose.as_hidden }}{{ form.type.as_hidden }}{{ form.requested_duration.as_hidden }} + {% else %} + + + + + + + + + + + + + +
{{ form.name.label_tag }}{{ form.name }}{{ form.purpose.errors }}
{{ form.purpose.label_tag }} + {{ form.purpose }} {{ form.type }} + {{ form.purpose.errors }}{{ form.type.errors }} +
{{ form.requested_duration.label_tag }}{{ form.requested_duration }}{{ form.requested_duration.errors }}
+ {% 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 }}
-{% endif %} \ No newline at end of file