Skip to content

Commit 3dfce7b

Browse files
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
1 parent 5cbe402 commit 3dfce7b

21 files changed

Lines changed: 323 additions & 89 deletions

ietf/group/migrations/0052_populate_groupfeatures_session_purposes.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,23 @@
77

88
default_purposes = dict(
99
adhoc=['presentation'],
10-
adm=['closed_meeting', 'officehours'],
10+
adm=['closed_meeting', 'office_hours'],
1111
ag=['regular'],
1212
area=['regular'],
13-
dir=['presentation', 'social', 'tutorial', 'regular'],
13+
dir=['open_meeting', 'presentation', 'regular', 'social', 'tutorial'],
1414
iab=['closed_meeting', 'regular'],
15-
iabasg=['open_meeting', 'closed_meeting'],
15+
iabasg=['closed_meeting', 'open_meeting'],
16+
iana=['office_hours'],
17+
iesg=['closed_meeting', 'open_meeting'],
1618
ietf=['admin', 'plenary', 'presentation', 'social'],
17-
nomcom=['closed_meeting', 'officehours'],
19+
irtf=[],
20+
ise=['office_hours'],
21+
isoc=['office_hours', 'open_meeting', 'presentation'],
22+
nomcom=['closed_meeting', 'office_hours'],
1823
program=['regular', 'tutorial'],
1924
rag=['regular'],
2025
review=['open_meeting', 'social'],
26+
rfcedtyp=['office_hours'],
2127
rg=['regular'],
2228
team=['coding', 'presentation', 'social', 'tutorial'],
2329
wg=['regular'],
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 2.2.24 on 2021-10-22 06:58
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('meeting', '0048_session_purpose'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='session',
15+
name='on_agenda',
16+
field=models.BooleanField(default=True, help_text='Is this session visible on the meeting agenda?'),
17+
),
18+
]
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Generated by Django 2.2.24 on 2021-10-22 06:58
2+
3+
from django.db import migrations, models
4+
5+
6+
def forward(apps, schema_editor):
7+
Session = apps.get_model('meeting', 'Session')
8+
SchedTimeSessAssignment = apps.get_model('meeting', 'SchedTimeSessAssignment')
9+
# find official assignments that are to private timeslots and fill in session.on_agenda
10+
private_assignments = SchedTimeSessAssignment.objects.filter(
11+
models.Q(
12+
schedule=models.F('session__meeting__schedule')
13+
) | models.Q(
14+
schedule=models.F('session__meeting__schedule__base')
15+
),
16+
timeslot__type__private=True,
17+
)
18+
Session.objects.filter(timeslotassignments__in=private_assignments).update(on_agenda=False)
19+
# Also update any sessions to match their purpose's default setting (this intentionally
20+
# overrides the timeslot settings above, but that is unlikely to matter because the
21+
# purposes will roll out at the same time as the on_agenda field)
22+
Session.objects.filter(purpose__on_agenda=False).update(on_agenda=False)
23+
Session.objects.filter(purpose__on_agenda=True).update(on_agenda=True)
24+
25+
def reverse(apps, schema_editor):
26+
Session = apps.get_model('meeting', 'Session')
27+
Session.objects.update(on_agenda=True) # restore all to default on_agenda=True state
28+
29+
class Migration(migrations.Migration):
30+
31+
dependencies = [
32+
('meeting', '0049_session_on_agenda'),
33+
]
34+
35+
operations = [
36+
migrations.RunPython(forward, reverse),
37+
]

ietf/meeting/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1173,6 +1173,7 @@ class Session(models.Model):
11731173
scheduled = models.DateTimeField(null=True, blank=True)
11741174
modified = models.DateTimeField(auto_now=True)
11751175
remote_instructions = models.CharField(blank=True,max_length=1024)
1176+
on_agenda = models.BooleanField(default=True, help_text='Is this session visible on the meeting agenda?')
11761177

11771178
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)
11781179

ietf/meeting/tests_views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,7 @@ def test_agenda_personalize(self):
430430
q = PyQuery(r.content)
431431
for assignment in SchedTimeSessAssignment.objects.filter(
432432
schedule__in=[meeting.schedule, meeting.schedule.base],
433-
timeslot__type__private=False,
433+
session__on_agenda=True,
434434
):
435435
row = q('#row-{}'.format(assignment.slug()))
436436
self.assertIsNotNone(row, 'No row for assignment {}'.format(assignment))

ietf/meeting/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@
146146
url(r'^upcoming\.ics/?$', views.upcoming_ical),
147147
url(r'^upcoming\.json/?$', views.upcoming_json),
148148
url(r'^session/(?P<session_id>\d+)/agenda_materials$', views.session_materials),
149+
url(r'^session/(?P<session_id>\d+)/edit/?', views.edit_session),
149150
# Then patterns from more specific to less
150151
url(r'^(?P<num>interim-[a-z0-9-]+)/', include(type_interim_patterns)),
151152
url(r'^(?P<num>\d+)/requests.html$', RedirectView.as_view(url='/meeting/%(num)s/requests', permanent=True)),

ietf/meeting/views.py

Lines changed: 65 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
from ietf.meeting.models import Meeting, Session, Schedule, FloorPlan, SessionPresentation, TimeSlot, SlideSubmission
5959
from ietf.meeting.models import SessionStatusName, SchedulingEvent, SchedTimeSessAssignment, Room, TimeSlotTypeName
6060
from ietf.meeting.forms import ( CustomDurationField, SwapDaysForm, SwapTimeslotsForm,
61-
TimeSlotCreateForm, TimeSlotEditForm )
61+
TimeSlotCreateForm, TimeSlotEditForm, SessionEditForm )
6262
from ietf.meeting.helpers import get_areas, get_person_by_email, get_schedule_by_name
6363
from ietf.meeting.helpers import build_all_agenda_slices, get_wg_name_list
6464
from ietf.meeting.helpers import get_all_assignments_from_schedule
@@ -500,6 +500,16 @@ def new_meeting_schedule(request, num, owner=None, name=None):
500500

501501
@ensure_csrf_cookie
502502
def edit_meeting_schedule(request, num=None, owner=None, name=None):
503+
"""Schedule editor
504+
505+
In addition to the URL parameters, accepts a query string parameter 'type'.
506+
If present, only sessions/timeslots with a TimeSlotTypeName with that slug
507+
will be included in the editor. More than one type can be enabled by passing
508+
multiple type parameters.
509+
510+
?type=regular - shows only regular sessions/timeslots (i.e., old editor behavior)
511+
?type=regular&type=other - shows both regular and other sessions/timeslots
512+
"""
503513
# Need to coordinate this list with types of session requests
504514
# that can be created (see, e.g., SessionQuerySet.requests())
505515
IGNORE_TIMESLOT_TYPES = ('offagenda', 'reserved', 'unavail')
@@ -532,22 +542,31 @@ def timeslot_locked(ts):
532542
"hide_menu": True
533543
}, status=403, content_type="text/html")
534544

545+
# See if we were given one or more 'type' query string parameters. If so, filter to that timeslot type.
546+
if 'type' in request.GET:
547+
include_timeslot_types = request.GET.getlist('type')
548+
else:
549+
include_timeslot_types = None # disables filtering by type (other than IGNORE_TIMESLOT_TYPES)
550+
535551
assignments = SchedTimeSessAssignment.objects.filter(
536552
schedule__in=[schedule, schedule.base],
537553
timeslot__location__isnull=False,
538-
# session__type='regular',
539-
).order_by('timeslot__time','timeslot__name')
554+
)
555+
if include_timeslot_types is not None:
556+
assignments = assignments.filter(session__type__in=include_timeslot_types)
557+
assignments = assignments.order_by('timeslot__time','timeslot__name')
540558

541559
assignments_by_session = defaultdict(list)
542560
for a in assignments:
543561
assignments_by_session[a.session_id].append(a)
544562

545563
tombstone_states = ['canceled', 'canceledpa', 'resched']
546564

565+
sessions = Session.objects.filter(meeting=meeting)
566+
if include_timeslot_types is not None:
567+
sessions = sessions.filter(type__in=include_timeslot_types)
547568
sessions = add_event_info_to_session_qs(
548-
Session.objects.filter(
549-
meeting=meeting,
550-
).exclude(
569+
sessions.exclude(
551570
type__in=IGNORE_TIMESLOT_TYPES,
552571
).order_by('pk'),
553572
requested_time=True,
@@ -559,14 +578,19 @@ def timeslot_locked(ts):
559578
'resources', 'group', 'group__parent', 'group__type', 'joint_with_groups', 'purpose',
560579
)
561580

562-
timeslots_qs = TimeSlot.objects.filter(
563-
meeting=meeting,
564-
).exclude(
581+
timeslots_qs = TimeSlot.objects.filter(meeting=meeting)
582+
if include_timeslot_types is not None:
583+
timeslots_qs = timeslots_qs.filter(type__in=include_timeslot_types)
584+
timeslots_qs = timeslots_qs.exclude(
565585
type__in=IGNORE_TIMESLOT_TYPES,
566586
).prefetch_related('type').order_by('location', 'time', 'name')
567587

568-
min_duration = min(t.duration for t in timeslots_qs)
569-
max_duration = max(t.duration for t in timeslots_qs)
588+
if timeslots_qs.count() > 0:
589+
min_duration = min(t.duration for t in timeslots_qs)
590+
max_duration = max(t.duration for t in timeslots_qs)
591+
else:
592+
min_duration = 1
593+
max_duration = 2
570594

571595
def timedelta_to_css_ems(timedelta):
572596
# we scale the session and slots a bit according to their
@@ -707,7 +731,10 @@ def prepare_timeslots_for_display(timeslots, rooms):
707731

708732
all_days = sorted(all_days) # changes set to a list
709733
# Note the maximum timeslot count for any room
710-
max_timeslots = max(rd['timeslot_count'] for rd in room_data.values())
734+
if len(room_data) > 0:
735+
max_timeslots = max(rd['timeslot_count'] for rd in room_data.values())
736+
else:
737+
max_timeslots = 0
711738

712739
# Partition rooms into groups with identical timeslot arrangements.
713740
# Start by discarding any roos that have no timeslots.
@@ -920,7 +947,10 @@ def _json_response(success, status=None, **extra_data):
920947
return _json_response(False, error="Invalid parameters")
921948

922949
# Show only rooms that have regular sessions
923-
rooms = meeting.room_set.filter(session_types__slug='regular')
950+
if include_timeslot_types is None:
951+
rooms = meeting.room_set.all()
952+
else:
953+
rooms = meeting.room_set.filter(session_types__slug__in=include_timeslot_types)
924954

925955
# Construct timeslot data for the template to render
926956
days = prepare_timeslots_for_display(timeslots_qs, rooms)
@@ -1583,7 +1613,7 @@ def get_assignments_for_agenda(schedule):
15831613
"""Get queryset containing assignments to show on the agenda"""
15841614
return SchedTimeSessAssignment.objects.filter(
15851615
schedule__in=[schedule, schedule.base],
1586-
timeslot__type__private=False,
1616+
session__on_agenda=True,
15871617
)
15881618

15891619

@@ -1938,7 +1968,7 @@ def week_view(request, num=None, name=None, owner=None):
19381968

19391969
filtered_assignments = SchedTimeSessAssignment.objects.filter(
19401970
schedule__in=[schedule, schedule.base],
1941-
timeslot__type__private=False,
1971+
session__on_agenda=True,
19421972
)
19431973
filtered_assignments = preprocess_assignments_for_agenda(filtered_assignments, meeting)
19441974
AgendaKeywordTagger(assignments=filtered_assignments).apply()
@@ -2121,7 +2151,7 @@ def agenda_ical(request, num=None, name=None, acronym=None, session_id=None):
21212151

21222152
assignments = SchedTimeSessAssignment.objects.filter(
21232153
schedule__in=[schedule, schedule.base],
2124-
timeslot__type__private=False,
2154+
session__on_agenda=True,
21252155
)
21262156
assignments = preprocess_assignments_for_agenda(assignments, meeting)
21272157
AgendaKeywordTagger(assignments=assignments).apply()
@@ -2159,7 +2189,7 @@ def agenda_json(request, num=None):
21592189
parent_acronyms = set()
21602190
assignments = SchedTimeSessAssignment.objects.filter(
21612191
schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None],
2162-
timeslot__type__private=False,
2192+
session__on_agenda=True,
21632193
).exclude(
21642194
session__type__in=['break', 'reg']
21652195
)
@@ -4098,6 +4128,24 @@ def create_timeslot(request, num):
40984128
)
40994129

41004130

4131+
@role_required('Secretariat')
4132+
def edit_session(request, session_id):
4133+
session = get_object_or_404(Session, pk=session_id)
4134+
if request.method == 'POST':
4135+
form = SessionEditForm(instance=session, data=request.POST)
4136+
if form.is_valid():
4137+
form.save()
4138+
return HttpResponseRedirect(
4139+
reverse('ietf.meeting.views.edit_meeting_schedule',
4140+
kwargs={'num': form.instance.meeting.number}))
4141+
else:
4142+
form = SessionEditForm(instance=session)
4143+
return render(
4144+
request,
4145+
'meeting/edit_session.html',
4146+
{'session': session, 'form': form},
4147+
)
4148+
41014149
@role_required('Secretariat')
41024150
def request_minutes(request, num=None):
41034151
meeting = get_ietf_meeting(num)

ietf/name/migrations/0035_sessionpurposename.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class Migration(migrations.Migration):
2323
('used', models.BooleanField(default=True)),
2424
('order', models.IntegerField(default=0)),
2525
('timeslot_types', jsonfield.fields.JSONField(default=[], help_text='Allowed TimeSlotTypeNames', max_length=256, validators=[ietf.name.models.JSONForeignKeyListValidator('name.TimeSlotTypeName')])),
26+
('on_agenda', models.BooleanField(default=True, help_text='Are sessions of this purpose visible on the agenda by default?')),
2627
],
2728
options={
2829
'ordering': ['order', 'name'],

ietf/name/migrations/0036_populate_sessionpurposename.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,19 @@ def forward(apps, schema_editor):
99
SessionPurposeName = apps.get_model('name', 'SessionPurposeName')
1010
TimeSlotTypeName = apps.get_model('name', 'TimeSlotTypeName')
1111

12-
for order, (slug, name, desc, tstypes) in enumerate((
13-
('regular', 'Regular', 'Regular group session', ['regular']),
14-
('tutorial', 'Tutorial', 'Tutorial or training session', ['other']),
15-
('officehours', 'Office hours', 'Office hours session', ['other']),
16-
('coding', 'Coding', 'Coding session', ['other']),
17-
('admin', 'Administrative', 'Meeting administration', ['other', 'reg']),
18-
('social', 'Social', 'Social event or activity', ['break', 'other']),
19-
('plenary', 'Plenary', 'Plenary session', ['plenary']),
20-
('presentation', 'Presentation', 'Presentation session', ['other', 'regular']),
21-
('open_meeting', 'Open meeting', 'Open meeting', ['other']),
22-
('closed_meeting', 'Closed meeting', 'Closed meeting', ['other', 'regular']),
12+
for order, (slug, name, desc, tstypes, on_agenda) in enumerate((
13+
('regular', 'Regular', 'Regular group session', ['regular'], True),
14+
('tutorial', 'Tutorial', 'Tutorial or training session', ['other'], True),
15+
('office_hours', 'Office hours', 'Office hours session', ['other'], True),
16+
('coding', 'Coding', 'Coding session', ['other'], True),
17+
('admin', 'Administrative', 'Meeting administration', ['other', 'reg'], True),
18+
('social', 'Social', 'Social event or activity', ['break', 'other'], True),
19+
('plenary', 'Plenary', 'Plenary session', ['plenary'], True),
20+
('presentation', 'Presentation', 'Presentation session', ['other', 'regular'], True),
21+
('open_meeting', 'Open meeting', 'Open meeting', ['other'], True),
22+
('closed_meeting', 'Closed meeting', 'Closed meeting', ['other', 'regular'], False),
2323
)):
24-
# verify that we're not about to use an invalid purpose
24+
# verify that we're not about to use an invalid type
2525
for ts_type in tstypes:
2626
TimeSlotTypeName.objects.get(pk=ts_type) # throws an exception unless exists
2727

@@ -31,7 +31,8 @@ def forward(apps, schema_editor):
3131
desc=desc,
3232
used=True,
3333
order=order,
34-
timeslot_types = tstypes
34+
timeslot_types = tstypes,
35+
on_agenda=on_agenda,
3536
)
3637

3738

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Generated by Django 2.2.24 on 2021-10-25 16:58
2+
3+
from django.db import migrations
4+
5+
6+
PRIVATE_TIMESLOT_SLUGS = {'lead', 'offagenda'} # from DB 2021 Oct
7+
8+
9+
def forward(apps, schema_editor):
10+
TimeSlotTypeName = apps.get_model('name', 'TimeSlotTypeName')
11+
slugs = TimeSlotTypeName.objects.filter(private=True).values_list('slug', flat=True)
12+
if set(slugs) != PRIVATE_TIMESLOT_SLUGS:
13+
# the reverse migration will not restore the database, refuse to migrate
14+
raise ValueError('Disagreement between migration data and database')
15+
16+
17+
def reverse(apps, schema_editor):
18+
TimeSlotTypeName = apps.get_model('name', 'TimeSlotTypeName')
19+
TimeSlotTypeName.objects.filter(slug__in=PRIVATE_TIMESLOT_SLUGS).update(private=True)
20+
21+
22+
class Migration(migrations.Migration):
23+
24+
dependencies = [
25+
('name', '0036_populate_sessionpurposename'),
26+
('meeting', '0050_populate_session_on_agenda'),
27+
]
28+
29+
operations = [
30+
migrations.RunPython(forward, reverse),
31+
]

0 commit comments

Comments
 (0)