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( default_purposes = dict(
adhoc=['presentation'], adhoc=['presentation'],
adm=['closed_meeting', 'officehours'], adm=['closed_meeting', 'office_hours'],
ag=['regular'], ag=['regular'],
area=['regular'], area=['regular'],
dir=['presentation', 'social', 'tutorial', 'regular'], dir=['open_meeting', 'presentation', 'regular', 'social', 'tutorial'],
iab=['closed_meeting', 'regular'], 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'], 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'], program=['regular', 'tutorial'],
rag=['regular'], rag=['regular'],
review=['open_meeting', 'social'], review=['open_meeting', 'social'],
rfcedtyp=['office_hours'],
rg=['regular'], rg=['regular'],
team=['coding', 'presentation', 'social', 'tutorial'], team=['coding', 'presentation', 'social', 'tutorial'],
wg=['regular'], 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) scheduled = models.DateTimeField(null=True, blank=True)
modified = models.DateTimeField(auto_now=True) modified = models.DateTimeField(auto_now=True)
remote_instructions = models.CharField(blank=True,max_length=1024) 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) 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) q = PyQuery(r.content)
for assignment in SchedTimeSessAssignment.objects.filter( for assignment in SchedTimeSessAssignment.objects.filter(
schedule__in=[meeting.schedule, meeting.schedule.base], schedule__in=[meeting.schedule, meeting.schedule.base],
timeslot__type__private=False, session__on_agenda=True,
): ):
row = q('#row-{}'.format(assignment.slug())) row = q('#row-{}'.format(assignment.slug()))
self.assertIsNotNone(row, 'No row for assignment {}'.format(assignment)) 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\.ics/?$', views.upcoming_ical),
url(r'^upcoming\.json/?$', views.upcoming_json), 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+)/agenda_materials$', views.session_materials),
url(r'^session/(?P<session_id>\d+)/edit/?', views.edit_session),
# Then patterns from more specific to less # Then patterns from more specific to less
url(r'^(?P<num>interim-[a-z0-9-]+)/', include(type_interim_patterns)), 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)), 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 Meeting, Session, Schedule, FloorPlan, SessionPresentation, TimeSlot, SlideSubmission
from ietf.meeting.models import SessionStatusName, SchedulingEvent, SchedTimeSessAssignment, Room, TimeSlotTypeName from ietf.meeting.models import SessionStatusName, SchedulingEvent, SchedTimeSessAssignment, Room, TimeSlotTypeName
from ietf.meeting.forms import ( CustomDurationField, SwapDaysForm, SwapTimeslotsForm, 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 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 build_all_agenda_slices, get_wg_name_list
from ietf.meeting.helpers import get_all_assignments_from_schedule 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 @ensure_csrf_cookie
def edit_meeting_schedule(request, num=None, owner=None, name=None): def edit_meeting_schedule(request, num=None, owner=None, name=None):
"""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 # Need to coordinate this list with types of session requests
# that can be created (see, e.g., SessionQuerySet.requests()) # that can be created (see, e.g., SessionQuerySet.requests())
IGNORE_TIMESLOT_TYPES = ('offagenda', 'reserved', 'unavail') IGNORE_TIMESLOT_TYPES = ('offagenda', 'reserved', 'unavail')
@ -532,11 +542,19 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
"hide_menu": True "hide_menu": True
}, status=403, content_type="text/html") }, 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( assignments = SchedTimeSessAssignment.objects.filter(
schedule__in=[schedule, schedule.base], schedule__in=[schedule, schedule.base],
timeslot__location__isnull=False, 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) assignments_by_session = defaultdict(list)
for a in assignments: for a in assignments:
@ -544,10 +562,11 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
tombstone_states = ['canceled', 'canceledpa', 'resched'] 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( sessions = add_event_info_to_session_qs(
Session.objects.filter( sessions.exclude(
meeting=meeting,
).exclude(
type__in=IGNORE_TIMESLOT_TYPES, type__in=IGNORE_TIMESLOT_TYPES,
).order_by('pk'), ).order_by('pk'),
requested_time=True, 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', 'resources', 'group', 'group__parent', 'group__type', 'joint_with_groups', 'purpose',
) )
timeslots_qs = TimeSlot.objects.filter( timeslots_qs = TimeSlot.objects.filter(meeting=meeting)
meeting=meeting, if include_timeslot_types is not None:
).exclude( timeslots_qs = timeslots_qs.filter(type__in=include_timeslot_types)
timeslots_qs = timeslots_qs.exclude(
type__in=IGNORE_TIMESLOT_TYPES, type__in=IGNORE_TIMESLOT_TYPES,
).prefetch_related('type').order_by('location', 'time', 'name') ).prefetch_related('type').order_by('location', 'time', 'name')
min_duration = min(t.duration for t in timeslots_qs) if timeslots_qs.count() > 0:
max_duration = max(t.duration for t in timeslots_qs) 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): def timedelta_to_css_ems(timedelta):
# we scale the session and slots a bit according to their # 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 all_days = sorted(all_days) # changes set to a list
# Note the maximum timeslot count for any room # 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. # Partition rooms into groups with identical timeslot arrangements.
# Start by discarding any roos that have no timeslots. # 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") return _json_response(False, error="Invalid parameters")
# Show only rooms that have regular sessions # 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 # Construct timeslot data for the template to render
days = prepare_timeslots_for_display(timeslots_qs, rooms) 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""" """Get queryset containing assignments to show on the agenda"""
return SchedTimeSessAssignment.objects.filter( return SchedTimeSessAssignment.objects.filter(
schedule__in=[schedule, schedule.base], 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( filtered_assignments = SchedTimeSessAssignment.objects.filter(
schedule__in=[schedule, schedule.base], schedule__in=[schedule, schedule.base],
timeslot__type__private=False, session__on_agenda=True,
) )
filtered_assignments = preprocess_assignments_for_agenda(filtered_assignments, meeting) filtered_assignments = preprocess_assignments_for_agenda(filtered_assignments, meeting)
AgendaKeywordTagger(assignments=filtered_assignments).apply() 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( assignments = SchedTimeSessAssignment.objects.filter(
schedule__in=[schedule, schedule.base], schedule__in=[schedule, schedule.base],
timeslot__type__private=False, session__on_agenda=True,
) )
assignments = preprocess_assignments_for_agenda(assignments, meeting) assignments = preprocess_assignments_for_agenda(assignments, meeting)
AgendaKeywordTagger(assignments=assignments).apply() AgendaKeywordTagger(assignments=assignments).apply()
@ -2159,7 +2189,7 @@ def agenda_json(request, num=None):
parent_acronyms = set() parent_acronyms = set()
assignments = SchedTimeSessAssignment.objects.filter( assignments = SchedTimeSessAssignment.objects.filter(
schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None], schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None],
timeslot__type__private=False, session__on_agenda=True,
).exclude( ).exclude(
session__type__in=['break', 'reg'] 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') @role_required('Secretariat')
def request_minutes(request, num=None): def request_minutes(request, num=None):
meeting = get_ietf_meeting(num) meeting = get_ietf_meeting(num)

View file

@ -23,6 +23,7 @@ class Migration(migrations.Migration):
('used', models.BooleanField(default=True)), ('used', models.BooleanField(default=True)),
('order', models.IntegerField(default=0)), ('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')])), ('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={ options={
'ordering': ['order', 'name'], 'ordering': ['order', 'name'],

View file

@ -9,19 +9,19 @@ def forward(apps, schema_editor):
SessionPurposeName = apps.get_model('name', 'SessionPurposeName') SessionPurposeName = apps.get_model('name', 'SessionPurposeName')
TimeSlotTypeName = apps.get_model('name', 'TimeSlotTypeName') TimeSlotTypeName = apps.get_model('name', 'TimeSlotTypeName')
for order, (slug, name, desc, tstypes) in enumerate(( for order, (slug, name, desc, tstypes, on_agenda) in enumerate((
('regular', 'Regular', 'Regular group session', ['regular']), ('regular', 'Regular', 'Regular group session', ['regular'], True),
('tutorial', 'Tutorial', 'Tutorial or training session', ['other']), ('tutorial', 'Tutorial', 'Tutorial or training session', ['other'], True),
('officehours', 'Office hours', 'Office hours session', ['other']), ('office_hours', 'Office hours', 'Office hours session', ['other'], True),
('coding', 'Coding', 'Coding session', ['other']), ('coding', 'Coding', 'Coding session', ['other'], True),
('admin', 'Administrative', 'Meeting administration', ['other', 'reg']), ('admin', 'Administrative', 'Meeting administration', ['other', 'reg'], True),
('social', 'Social', 'Social event or activity', ['break', 'other']), ('social', 'Social', 'Social event or activity', ['break', 'other'], True),
('plenary', 'Plenary', 'Plenary session', ['plenary']), ('plenary', 'Plenary', 'Plenary session', ['plenary'], True),
('presentation', 'Presentation', 'Presentation session', ['other', 'regular']), ('presentation', 'Presentation', 'Presentation session', ['other', 'regular'], True),
('open_meeting', 'Open meeting', 'Open meeting', ['other']), ('open_meeting', 'Open meeting', 'Open meeting', ['other'], True),
('closed_meeting', 'Closed meeting', 'Closed meeting', ['other', 'regular']), ('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: for ts_type in tstypes:
TimeSlotTypeName.objects.get(pk=ts_type) # throws an exception unless exists TimeSlotTypeName.objects.get(pk=ts_type) # throws an exception unless exists
@ -31,7 +31,8 @@ def forward(apps, schema_editor):
desc=desc, desc=desc,
used=True, used=True,
order=order, 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', help_text='Allowed TimeSlotTypeNames',
validators=[JSONForeignKeyListValidator('name.TimeSlotTypeName')], 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): class TimeSlotTypeName(NameModel):
"""Session, Break, Registration, Other, Reserved, unavail""" """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): class ConstraintName(NameModel):
"""conflict, conflic2, conflic3, bethere, timerange, time_relation, wg_adjacent""" """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)") 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 @property
def media(self): def media(self):
# get media for our formset # 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): 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 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 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 if group.features.acts_like_wg:
initial['num_session'] = min(sessions.count(), 2) if group.features.acts_like_wg else sessions.count() # 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 initial['attendees'] = sessions[0].attendees
def valid_conflict(conflict): 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 # 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) 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) meeting = get_meeting(days=14)
FormClass = get_session_form_class() 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() 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) num_sessions = int(form.cleaned_data['num_session']) + (1 if form.cleaned_data['third_session'] else 0)
# Create new session records # 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]): for count, sess_form in enumerate(form.session_forms[:num_sessions]):
new_session = Session.objects.create( new_session = sess_form.instance
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'],
)
SchedulingEvent.objects.create( SchedulingEvent.objects.create(
session=new_session, session=new_session,
status=SessionStatusName.objects.get(slug=status_slug_for_new_session(new_session, count)), 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() groups_split = form.cleaned_data.get('joint_with_groups').replace(',',' ').split()
joint = Group.objects.filter(acronym__in=groups_split) joint = Group.objects.filter(acronym__in=groups_split)
new_session.joint_with_groups.set(joint) new_session.joint_with_groups.set(joint)
new_session.save()
session_changed(new_session) session_changed(new_session)
# write constraint records # write constraint records
@ -413,6 +412,8 @@ def edit(request, acronym, num=None):
''' '''
meeting = get_meeting(num,days=14) meeting = get_meeting(num,days=14)
group = get_object_or_404(Group, acronym=acronym) 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( sessions = add_event_info_to_session_qs(
Session.objects.filter(group=group, meeting=meeting) Session.objects.filter(group=group, meeting=meeting)
).filter( ).filter(
@ -449,12 +450,14 @@ def edit(request, acronym, num=None):
if form.has_changed(): if form.has_changed():
changed_session_forms = [sf for sf in form.session_forms.forms_to_keep if sf.has_changed()] changed_session_forms = [sf for sf in form.session_forms.forms_to_keep if sf.has_changed()]
form.session_forms.save() form.session_forms.save()
for n, new_session in enumerate(form.session_forms.created_instances): for n, subform in enumerate(form.session_forms):
SchedulingEvent.objects.create( session = subform.instance
session=new_session, if session in form.session_forms.created_instances:
status_id=status_slug_for_new_session(new_session, n), SchedulingEvent.objects.create(
by=request.user.person, session=session,
) status_id=status_slug_for_new_session(session, n),
by=request.user.person,
)
for sf in changed_session_forms: for sf in changed_session_forms:
session_changed(sf.instance) session_changed(sf.instance)
@ -638,6 +641,8 @@ def new(request, acronym):
to create the request. to create the request.
''' '''
group = get_object_or_404(Group, acronym=acronym) 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) meeting = get_meeting(days=14)
session_conflicts = dict(inbound=inbound_session_conflicts_as_string(group, meeting)) session_conflicts = dict(inbound=inbound_session_conflicts_as_string(group, meeting))
is_virtual = meeting.number in settings.SECR_VIRTUAL_MEETINGS, 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> <dt>Purpose</dt>
<dd> <dd>
{{ sess_form.cleaned_data.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 %}
</dd> </dd>
{% endif %} {% endif %}
</dl> </dl>

View file

@ -55,10 +55,12 @@
function update_name_field_visibility(name_elt, purpose) { function update_name_field_visibility(name_elt, purpose) {
const row = name_elt.closest('tr'); const row = name_elt.closest('tr');
if (purpose === 'regular') { if (row) {
row.setAttribute('hidden', 'hidden'); if (purpose === 'regular') {
} else { row.setAttribute('hidden', 'hidden');
row.removeAttribute('hidden'); } else {
row.removeAttribute('hidden');
}
} }
} }
@ -72,12 +74,16 @@
} }
function add_purpose_change_handler(form) { function add_purpose_change_handler(form) {
const id_prefix = 'id_' + form.dataset.prefix; const id_prefix = 'id_' + (form.dataset.prefix ? (form.dataset.prefix + '-') : '');
const name_elt = document.getElementById(id_prefix + '-name'); const purpose_elt = document.getElementById(id_prefix + 'purpose');
const purpose_elt = document.getElementById(id_prefix + '-purpose'); if (purpose_elt.type === 'hidden') {
const type_elt = document.getElementById(id_prefix + '-type'); 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 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 // update on future changes
purpose_elt.addEventListener( purpose_elt.addEventListener(

View file

@ -16,6 +16,8 @@
.edit-meeting-schedule .edit-grid .timeslot.past-hint { filter: brightness(0.9); } .edit-meeting-schedule .edit-grid .timeslot.past-hint { filter: brightness(0.9); }
.edit-meeting-schedule .past-flag { visibility: hidden; font-size: smaller; } .edit-meeting-schedule .past-flag { visibility: hidden; font-size: smaller; }
.edit-meeting-schedule .edit-grid .timeslot.past .past-flag { visibility: visible; color: #aaaaaa; } .edit-meeting-schedule .edit-grid .timeslot.past .past-flag { visibility: visible; color: #aaaaaa; }
{# style off-agenda sessions to indicate this #}
.edit-meeting-schedule .session.off-agenda { filter: brightness(0.9); }
{# type and purpose styling #} {# type and purpose styling #}
.edit-meeting-schedule .edit-grid .timeslot.wrong-timeslot-type, .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.hidden-timeslot-type { background-color: transparent; ); }

View file

@ -1,5 +1,5 @@
<div id="session{{ session.pk }}" <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;" style="width:{{ session.layout_width }}em;"
data-duration="{{ session.requested_duration.total_seconds }}" {% if session.attendees != None %} data-duration="{{ session.requested_duration.total_seconds }}" {% if session.attendees != None %}
data-attendees="{{ session.attendees }}"{% endif %} data-attendees="{{ session.attendees }}"{% endif %}
@ -47,6 +47,7 @@
{% if session.group.parent %} {% if session.group.parent %}
&middot; <span class="session-parent">{{ session.group.parent.acronym }}</span> &middot; <span class="session-parent">{{ session.group.parent.acronym }}</span>
{% endif %} {% endif %}
{% if not session.on_agenda %}&middot; <i>off agenda</i>{% endif %}
</div> </div>
{% endif %} {% endif %}
@ -84,5 +85,7 @@
{% for s in session.other_sessions %} {% 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> <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 %} {% endfor %}
<a href="{% url 'ietf.meeting.views.edit_session' session_id=session.pk %}">Edit session</a>
</div> </div>
</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,24 +1,25 @@
{# Copyright The IETF Trust 2007-2020, All Rights Reserved #} {# 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 }} <div class="session-details-form" data-prefix="{{ form.prefix }}">
{% else %}<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 }}
{{ form.id.as_hidden }} {% else %}
{{ form.DELETE.as_hidden }} <table>
<table> <tr>
<tr> <th>{{ form.name.label_tag }}</th>
<th>{{ form.name.label_tag }}</th> <td>{{ form.name }}{{ form.purpose.errors }}</td>
<td>{{ form.name }}{{ form.purpose.errors }}</td> </tr>
</tr> <tr>
<tr> <th>{{ form.purpose.label_tag }}</th>
<th>{{ form.purpose.label_tag }}</th> <td>
<td> {{ form.purpose }} {{ form.type }}
{{ form.purpose }} {{ form.type }} {{ form.purpose.errors }}{{ form.type.errors }}
{{ form.purpose.errors }}{{ form.type.errors }} </td>
</td> </tr>
<tr> <tr>
<th>{{ form.requested_duration.label_tag }}</th> <th>{{ form.requested_duration.label_tag }}</th>
<td>{{ form.requested_duration }}{{ form.requested_duration.errors }}</td> <td>{{ form.requested_duration }}{{ form.requested_duration.errors }}</td>
</tr> </tr>
</tr> </table>
</table> {% 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> </div>
{% endif %}