Skip to content

Commit

Permalink
Update purpose/types after discussions, add on_agenda Session field, …
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
jennifer-richards committed Nov 4, 2021
1 parent 5cbe402 commit 3dfce7b
Show file tree
Hide file tree
Showing 21 changed files with 323 additions and 89 deletions.
Expand Up @@ -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'],
Expand Down
18 changes: 18 additions & 0 deletions 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?'),
),
]
37 changes: 37 additions & 0 deletions 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),
]
1 change: 1 addition & 0 deletions ietf/meeting/models.py
Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion ietf/meeting/tests_views.py
Expand Up @@ -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))
Expand Down
1 change: 1 addition & 0 deletions ietf/meeting/urls.py
Expand Up @@ -146,6 +146,7 @@
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)),
Expand Down
82 changes: 65 additions & 17 deletions ietf/meeting/views.py
Expand Up @@ -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
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -532,22 +542,31 @@ 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:
assignments_by_session[a.session_id].append(a)

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,
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
)


Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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']
)
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions ietf/name/migrations/0035_sessionpurposename.py
Expand Up @@ -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'],
Expand Down
27 changes: 14 additions & 13 deletions ietf/name/migrations/0036_populate_sessionpurposename.py
Expand Up @@ -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

Expand All @@ -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,
)


Expand Down
31 changes: 31 additions & 0 deletions 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),
]
17 changes: 17 additions & 0 deletions 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',
),
]
3 changes: 2 additions & 1 deletion ietf/name/models.py
Expand Up @@ -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)")
Expand Down

0 comments on commit 3dfce7b

Please sign in to comment.