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:
parent
5cbe402036
commit
3dfce7b850
|
@ -7,17 +7,23 @@ from django.db import migrations
|
|||
|
||||
default_purposes = dict(
|
||||
adhoc=['presentation'],
|
||||
adm=['closed_meeting', 'officehours'],
|
||||
adm=['closed_meeting', 'office_hours'],
|
||||
ag=['regular'],
|
||||
area=['regular'],
|
||||
dir=['presentation', 'social', 'tutorial', 'regular'],
|
||||
dir=['open_meeting', 'presentation', 'regular', 'social', 'tutorial'],
|
||||
iab=['closed_meeting', 'regular'],
|
||||
iabasg=['open_meeting', 'closed_meeting'],
|
||||
iabasg=['closed_meeting', 'open_meeting'],
|
||||
iana=['office_hours'],
|
||||
iesg=['closed_meeting', 'open_meeting'],
|
||||
ietf=['admin', 'plenary', 'presentation', 'social'],
|
||||
nomcom=['closed_meeting', 'officehours'],
|
||||
irtf=[],
|
||||
ise=['office_hours'],
|
||||
isoc=['office_hours', 'open_meeting', 'presentation'],
|
||||
nomcom=['closed_meeting', 'office_hours'],
|
||||
program=['regular', 'tutorial'],
|
||||
rag=['regular'],
|
||||
review=['open_meeting', 'social'],
|
||||
rfcedtyp=['office_hours'],
|
||||
rg=['regular'],
|
||||
team=['coding', 'presentation', 'social', 'tutorial'],
|
||||
wg=['regular'],
|
||||
|
|
18
ietf/meeting/migrations/0049_session_on_agenda.py
Normal file
18
ietf/meeting/migrations/0049_session_on_agenda.py
Normal 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?'),
|
||||
),
|
||||
]
|
37
ietf/meeting/migrations/0050_populate_session_on_agenda.py
Normal file
37
ietf/meeting/migrations/0050_populate_session_on_agenda.py
Normal 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),
|
||||
]
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -430,7 +430,7 @@ class MeetingTests(BaseMeetingTestCase):
|
|||
q = PyQuery(r.content)
|
||||
for assignment in SchedTimeSessAssignment.objects.filter(
|
||||
schedule__in=[meeting.schedule, meeting.schedule.base],
|
||||
timeslot__type__private=False,
|
||||
session__on_agenda=True,
|
||||
):
|
||||
row = q('#row-{}'.format(assignment.slug()))
|
||||
self.assertIsNotNone(row, 'No row for assignment {}'.format(assignment))
|
||||
|
|
|
@ -146,6 +146,7 @@ urlpatterns = [
|
|||
url(r'^upcoming\.ics/?$', views.upcoming_ical),
|
||||
url(r'^upcoming\.json/?$', views.upcoming_json),
|
||||
url(r'^session/(?P<session_id>\d+)/agenda_materials$', views.session_materials),
|
||||
url(r'^session/(?P<session_id>\d+)/edit/?', views.edit_session),
|
||||
# Then patterns from more specific to less
|
||||
url(r'^(?P<num>interim-[a-z0-9-]+)/', include(type_interim_patterns)),
|
||||
url(r'^(?P<num>\d+)/requests.html$', RedirectView.as_view(url='/meeting/%(num)s/requests', permanent=True)),
|
||||
|
|
|
@ -58,7 +58,7 @@ from ietf.mailtrigger.utils import gather_address_lists
|
|||
from ietf.meeting.models import Meeting, Session, Schedule, FloorPlan, SessionPresentation, TimeSlot, SlideSubmission
|
||||
from ietf.meeting.models import SessionStatusName, SchedulingEvent, SchedTimeSessAssignment, Room, TimeSlotTypeName
|
||||
from ietf.meeting.forms import ( CustomDurationField, SwapDaysForm, SwapTimeslotsForm,
|
||||
TimeSlotCreateForm, TimeSlotEditForm )
|
||||
TimeSlotCreateForm, TimeSlotEditForm, SessionEditForm )
|
||||
from ietf.meeting.helpers import get_areas, get_person_by_email, get_schedule_by_name
|
||||
from ietf.meeting.helpers import build_all_agenda_slices, get_wg_name_list
|
||||
from ietf.meeting.helpers import get_all_assignments_from_schedule
|
||||
|
@ -500,6 +500,16 @@ def new_meeting_schedule(request, num, owner=None, name=None):
|
|||
|
||||
@ensure_csrf_cookie
|
||||
def edit_meeting_schedule(request, num=None, owner=None, name=None):
|
||||
"""Schedule editor
|
||||
|
||||
In addition to the URL parameters, accepts a query string parameter 'type'.
|
||||
If present, only sessions/timeslots with a TimeSlotTypeName with that slug
|
||||
will be included in the editor. More than one type can be enabled by passing
|
||||
multiple type parameters.
|
||||
|
||||
?type=regular - shows only regular sessions/timeslots (i.e., old editor behavior)
|
||||
?type=regular&type=other - shows both regular and other sessions/timeslots
|
||||
"""
|
||||
# Need to coordinate this list with types of session requests
|
||||
# that can be created (see, e.g., SessionQuerySet.requests())
|
||||
IGNORE_TIMESLOT_TYPES = ('offagenda', 'reserved', 'unavail')
|
||||
|
@ -532,11 +542,19 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
|
|||
"hide_menu": True
|
||||
}, status=403, content_type="text/html")
|
||||
|
||||
# See if we were given one or more 'type' query string parameters. If so, filter to that timeslot type.
|
||||
if 'type' in request.GET:
|
||||
include_timeslot_types = request.GET.getlist('type')
|
||||
else:
|
||||
include_timeslot_types = None # disables filtering by type (other than IGNORE_TIMESLOT_TYPES)
|
||||
|
||||
assignments = SchedTimeSessAssignment.objects.filter(
|
||||
schedule__in=[schedule, schedule.base],
|
||||
timeslot__location__isnull=False,
|
||||
# session__type='regular',
|
||||
).order_by('timeslot__time','timeslot__name')
|
||||
)
|
||||
if include_timeslot_types is not None:
|
||||
assignments = assignments.filter(session__type__in=include_timeslot_types)
|
||||
assignments = assignments.order_by('timeslot__time','timeslot__name')
|
||||
|
||||
assignments_by_session = defaultdict(list)
|
||||
for a in assignments:
|
||||
|
@ -544,10 +562,11 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
|
|||
|
||||
tombstone_states = ['canceled', 'canceledpa', 'resched']
|
||||
|
||||
sessions = Session.objects.filter(meeting=meeting)
|
||||
if include_timeslot_types is not None:
|
||||
sessions = sessions.filter(type__in=include_timeslot_types)
|
||||
sessions = add_event_info_to_session_qs(
|
||||
Session.objects.filter(
|
||||
meeting=meeting,
|
||||
).exclude(
|
||||
sessions.exclude(
|
||||
type__in=IGNORE_TIMESLOT_TYPES,
|
||||
).order_by('pk'),
|
||||
requested_time=True,
|
||||
|
@ -559,14 +578,19 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
|
|||
'resources', 'group', 'group__parent', 'group__type', 'joint_with_groups', 'purpose',
|
||||
)
|
||||
|
||||
timeslots_qs = TimeSlot.objects.filter(
|
||||
meeting=meeting,
|
||||
).exclude(
|
||||
timeslots_qs = TimeSlot.objects.filter(meeting=meeting)
|
||||
if include_timeslot_types is not None:
|
||||
timeslots_qs = timeslots_qs.filter(type__in=include_timeslot_types)
|
||||
timeslots_qs = timeslots_qs.exclude(
|
||||
type__in=IGNORE_TIMESLOT_TYPES,
|
||||
).prefetch_related('type').order_by('location', 'time', 'name')
|
||||
|
||||
if timeslots_qs.count() > 0:
|
||||
min_duration = min(t.duration for t in timeslots_qs)
|
||||
max_duration = max(t.duration for t in timeslots_qs)
|
||||
else:
|
||||
min_duration = 1
|
||||
max_duration = 2
|
||||
|
||||
def timedelta_to_css_ems(timedelta):
|
||||
# we scale the session and slots a bit according to their
|
||||
|
@ -707,7 +731,10 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
|
|||
|
||||
all_days = sorted(all_days) # changes set to a list
|
||||
# Note the maximum timeslot count for any room
|
||||
if len(room_data) > 0:
|
||||
max_timeslots = max(rd['timeslot_count'] for rd in room_data.values())
|
||||
else:
|
||||
max_timeslots = 0
|
||||
|
||||
# Partition rooms into groups with identical timeslot arrangements.
|
||||
# Start by discarding any roos that have no timeslots.
|
||||
|
@ -920,7 +947,10 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
|
|||
return _json_response(False, error="Invalid parameters")
|
||||
|
||||
# Show only rooms that have regular sessions
|
||||
rooms = meeting.room_set.filter(session_types__slug='regular')
|
||||
if include_timeslot_types is None:
|
||||
rooms = meeting.room_set.all()
|
||||
else:
|
||||
rooms = meeting.room_set.filter(session_types__slug__in=include_timeslot_types)
|
||||
|
||||
# Construct timeslot data for the template to render
|
||||
days = prepare_timeslots_for_display(timeslots_qs, rooms)
|
||||
|
@ -1583,7 +1613,7 @@ def get_assignments_for_agenda(schedule):
|
|||
"""Get queryset containing assignments to show on the agenda"""
|
||||
return SchedTimeSessAssignment.objects.filter(
|
||||
schedule__in=[schedule, schedule.base],
|
||||
timeslot__type__private=False,
|
||||
session__on_agenda=True,
|
||||
)
|
||||
|
||||
|
||||
|
@ -1938,7 +1968,7 @@ def week_view(request, num=None, name=None, owner=None):
|
|||
|
||||
filtered_assignments = SchedTimeSessAssignment.objects.filter(
|
||||
schedule__in=[schedule, schedule.base],
|
||||
timeslot__type__private=False,
|
||||
session__on_agenda=True,
|
||||
)
|
||||
filtered_assignments = preprocess_assignments_for_agenda(filtered_assignments, meeting)
|
||||
AgendaKeywordTagger(assignments=filtered_assignments).apply()
|
||||
|
@ -2121,7 +2151,7 @@ def agenda_ical(request, num=None, name=None, acronym=None, session_id=None):
|
|||
|
||||
assignments = SchedTimeSessAssignment.objects.filter(
|
||||
schedule__in=[schedule, schedule.base],
|
||||
timeslot__type__private=False,
|
||||
session__on_agenda=True,
|
||||
)
|
||||
assignments = preprocess_assignments_for_agenda(assignments, meeting)
|
||||
AgendaKeywordTagger(assignments=assignments).apply()
|
||||
|
@ -2159,7 +2189,7 @@ def agenda_json(request, num=None):
|
|||
parent_acronyms = set()
|
||||
assignments = SchedTimeSessAssignment.objects.filter(
|
||||
schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None],
|
||||
timeslot__type__private=False,
|
||||
session__on_agenda=True,
|
||||
).exclude(
|
||||
session__type__in=['break', 'reg']
|
||||
)
|
||||
|
@ -4098,6 +4128,24 @@ def create_timeslot(request, num):
|
|||
)
|
||||
|
||||
|
||||
@role_required('Secretariat')
|
||||
def edit_session(request, session_id):
|
||||
session = get_object_or_404(Session, pk=session_id)
|
||||
if request.method == 'POST':
|
||||
form = SessionEditForm(instance=session, data=request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return HttpResponseRedirect(
|
||||
reverse('ietf.meeting.views.edit_meeting_schedule',
|
||||
kwargs={'num': form.instance.meeting.number}))
|
||||
else:
|
||||
form = SessionEditForm(instance=session)
|
||||
return render(
|
||||
request,
|
||||
'meeting/edit_session.html',
|
||||
{'session': session, 'form': form},
|
||||
)
|
||||
|
||||
@role_required('Secretariat')
|
||||
def request_minutes(request, num=None):
|
||||
meeting = get_ietf_meeting(num)
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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
ietf/name/migrations/0038_remove_timeslottypename_private.py
Normal file
17
ietf/name/migrations/0038_remove_timeslottypename_private.py
Normal 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',
|
||||
),
|
||||
]
|
|
@ -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)")
|
||||
|
|
|
@ -287,7 +287,7 @@ class SessionForm(forms.Form):
|
|||
@property
|
||||
def media(self):
|
||||
# get media for our formset
|
||||
return super().media + self.session_forms.media
|
||||
return super().media + self.session_forms.media + forms.Media(js=('secr/js/session_form.js',))
|
||||
|
||||
|
||||
class VirtualSessionForm(SessionForm):
|
||||
|
|
|
@ -60,8 +60,13 @@ def get_initial_session(sessions, prune_conflicts=False):
|
|||
constraints = group.constraint_source_set.filter(meeting=meeting) # all constraints with this group as source
|
||||
conflicts = constraints.filter(name__is_group_conflict=True) # only the group conflict constraints
|
||||
|
||||
if group.features.acts_like_wg:
|
||||
# even if there are three sessions requested, the old form has 2 in this field
|
||||
initial['num_session'] = min(sessions.count(), 2) if group.features.acts_like_wg else sessions.count()
|
||||
initial['num_session'] = min(sessions.count(), 2)
|
||||
initial['third_session'] = sessions.count() > 2
|
||||
else:
|
||||
initial['num_session'] = sessions.count()
|
||||
initial['third_session'] = False
|
||||
initial['attendees'] = sessions[0].attendees
|
||||
|
||||
def valid_conflict(conflict):
|
||||
|
@ -274,6 +279,8 @@ def confirm(request, acronym):
|
|||
'''
|
||||
# FIXME: this should be using form.is_valid/form.cleaned_data - invalid input will make it crash
|
||||
group = get_object_or_404(Group,acronym=acronym)
|
||||
if len(group.features.session_purposes) == 0:
|
||||
raise Http404(f'Cannot request sessions for group "{acronym}"')
|
||||
meeting = get_meeting(days=14)
|
||||
FormClass = get_session_form_class()
|
||||
|
||||
|
@ -316,18 +323,9 @@ def confirm(request, acronym):
|
|||
add_event_info_to_session_qs(Session.objects.filter(group=group, meeting=meeting)).filter(current_status__in=['canceled', 'notmeet']).delete()
|
||||
num_sessions = int(form.cleaned_data['num_session']) + (1 if form.cleaned_data['third_session'] else 0)
|
||||
# Create new session records
|
||||
# Should really use sess_form.save(), but needs data from the main form as well. Need to sort that out properly.
|
||||
form.session_forms.save()
|
||||
for count, sess_form in enumerate(form.session_forms[:num_sessions]):
|
||||
new_session = Session.objects.create(
|
||||
meeting=meeting,
|
||||
group=group,
|
||||
attendees=form.cleaned_data['attendees'],
|
||||
requested_duration=sess_form.cleaned_data['requested_duration'],
|
||||
name=sess_form.cleaned_data['name'],
|
||||
comments=form.cleaned_data['comments'],
|
||||
purpose=sess_form.cleaned_data['purpose'],
|
||||
type=sess_form.cleaned_data['type'],
|
||||
)
|
||||
new_session = sess_form.instance
|
||||
SchedulingEvent.objects.create(
|
||||
session=new_session,
|
||||
status=SessionStatusName.objects.get(slug=status_slug_for_new_session(new_session, count)),
|
||||
|
@ -342,6 +340,7 @@ def confirm(request, acronym):
|
|||
groups_split = form.cleaned_data.get('joint_with_groups').replace(',',' ').split()
|
||||
joint = Group.objects.filter(acronym__in=groups_split)
|
||||
new_session.joint_with_groups.set(joint)
|
||||
new_session.save()
|
||||
session_changed(new_session)
|
||||
|
||||
# write constraint records
|
||||
|
@ -413,6 +412,8 @@ def edit(request, acronym, num=None):
|
|||
'''
|
||||
meeting = get_meeting(num,days=14)
|
||||
group = get_object_or_404(Group, acronym=acronym)
|
||||
if len(group.features.session_purposes) == 0:
|
||||
raise Http404(f'Cannot request sessions for group "{acronym}"')
|
||||
sessions = add_event_info_to_session_qs(
|
||||
Session.objects.filter(group=group, meeting=meeting)
|
||||
).filter(
|
||||
|
@ -449,10 +450,12 @@ def edit(request, acronym, num=None):
|
|||
if form.has_changed():
|
||||
changed_session_forms = [sf for sf in form.session_forms.forms_to_keep if sf.has_changed()]
|
||||
form.session_forms.save()
|
||||
for n, new_session in enumerate(form.session_forms.created_instances):
|
||||
for n, subform in enumerate(form.session_forms):
|
||||
session = subform.instance
|
||||
if session in form.session_forms.created_instances:
|
||||
SchedulingEvent.objects.create(
|
||||
session=new_session,
|
||||
status_id=status_slug_for_new_session(new_session, n),
|
||||
session=session,
|
||||
status_id=status_slug_for_new_session(session, n),
|
||||
by=request.user.person,
|
||||
)
|
||||
for sf in changed_session_forms:
|
||||
|
@ -638,6 +641,8 @@ def new(request, acronym):
|
|||
to create the request.
|
||||
'''
|
||||
group = get_object_or_404(Group, acronym=acronym)
|
||||
if len(group.features.session_purposes) == 0:
|
||||
raise Http404(f'Cannot request sessions for group "{acronym}"')
|
||||
meeting = get_meeting(days=14)
|
||||
session_conflicts = dict(inbound=inbound_session_conflicts_as_string(group, meeting))
|
||||
is_virtual = meeting.number in settings.SECR_VIRTUAL_MEETINGS,
|
||||
|
|
28
ietf/secr/static/secr/js/session_form.js
Normal file
28
ietf/secr/static/secr/js/session_form.js
Normal 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);
|
||||
})();
|
|
@ -13,7 +13,7 @@
|
|||
<dt>Purpose</dt>
|
||||
<dd>
|
||||
{{ sess_form.cleaned_data.purpose }}
|
||||
{% if sess_form.cleaned_data.purpose.timeslot_types|length > 1 %}({{ sess_form.cleaned_data.type }}{% endif %}
|
||||
{% if sess_form.cleaned_data.purpose.timeslot_types|length > 1 %}({{ sess_form.cleaned_data.type }}){% endif %}
|
||||
</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
|
|
|
@ -55,12 +55,14 @@
|
|||
|
||||
function update_name_field_visibility(name_elt, purpose) {
|
||||
const row = name_elt.closest('tr');
|
||||
if (row) {
|
||||
if (purpose === 'regular') {
|
||||
row.setAttribute('hidden', 'hidden');
|
||||
} else {
|
||||
row.removeAttribute('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Factory for event handler with a closure */
|
||||
function purpose_change_handler(name_elt, type_elt, type_options, allowed_types) {
|
||||
|
@ -72,12 +74,16 @@
|
|||
}
|
||||
|
||||
function add_purpose_change_handler(form) {
|
||||
const id_prefix = 'id_' + form.dataset.prefix;
|
||||
const name_elt = document.getElementById(id_prefix + '-name');
|
||||
const purpose_elt = document.getElementById(id_prefix + '-purpose');
|
||||
const type_elt = document.getElementById(id_prefix + '-type');
|
||||
const id_prefix = 'id_' + (form.dataset.prefix ? (form.dataset.prefix + '-') : '');
|
||||
const purpose_elt = document.getElementById(id_prefix + 'purpose');
|
||||
if (purpose_elt.type === 'hidden') {
|
||||
return; // element is hidden, so nothing to do
|
||||
}
|
||||
const name_elt = document.getElementById(id_prefix + 'name');
|
||||
const type_elt = document.getElementById(id_prefix + 'type');
|
||||
const type_options = type_elt.getElementsByTagName('option');
|
||||
const allowed_types = JSON.parse(type_elt.dataset.allowedOptions);
|
||||
const allowed_types = (type_elt.dataset.allowedOptions) ?
|
||||
JSON.parse(type_elt.dataset.allowedOptions) : [];
|
||||
|
||||
// update on future changes
|
||||
purpose_elt.addEventListener(
|
||||
|
|
|
@ -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; ); }
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<div id="session{{ session.pk }}"
|
||||
class="session {% if not session.group.parent.scheduling_color %}untoggleable-by-parent{% endif %} {% if session.parent_acronym %}parent-{{ session.parent_acronym }}{% endif %} purpose-{% firstof session.purpose.slug 'regular' %} {% if session.readonly %}readonly{% endif %}"
|
||||
class="session {% if not session.group.parent.scheduling_color %}untoggleable-by-parent{% endif %} {% if session.parent_acronym %}parent-{{ session.parent_acronym }}{% endif %} purpose-{% firstof session.purpose.slug 'regular' %} {% if session.readonly %}readonly{% endif %} {% if not session.on_agenda %}off-agenda{% endif %}"
|
||||
style="width:{{ session.layout_width }}em;"
|
||||
data-duration="{{ session.requested_duration.total_seconds }}" {% if session.attendees != None %}
|
||||
data-attendees="{{ session.attendees }}"{% endif %}
|
||||
|
@ -47,6 +47,7 @@
|
|||
{% if session.group.parent %}
|
||||
· <span class="session-parent">{{ session.group.parent.acronym }}</span>
|
||||
{% endif %}
|
||||
{% if not session.on_agenda %}· <i>off agenda</i>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
@ -84,5 +85,7 @@
|
|||
{% for s in session.other_sessions %}
|
||||
<div class="other-session" data-othersessionid="{{ s.pk }}"><i class="fa fa-calendar"></i> Other session <span class="time" data-scheduled="scheduled: {time}" data-notscheduled="not yet scheduled"></span></div>
|
||||
{% endfor %}
|
||||
|
||||
<a href="{% url 'ietf.meeting.views.edit_session' session_id=session.pk %}">Edit session</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
27
ietf/templates/meeting/edit_session.html
Normal file
27
ietf/templates/meeting/edit_session.html
Normal 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 %}
|
|
@ -1,8 +1,7 @@
|
|||
{# 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 %}<div class="session-details-form" data-prefix="{{ form.prefix }}">
|
||||
{{ form.id.as_hidden }}
|
||||
{{ form.DELETE.as_hidden }}
|
||||
<div class="session-details-form" data-prefix="{{ form.prefix }}">
|
||||
{% if hidden %}{{ form.name.as_hidden }}{{ form.purpose.as_hidden }}{{ form.type.as_hidden }}{{ form.requested_duration.as_hidden }}
|
||||
{% else %}
|
||||
<table>
|
||||
<tr>
|
||||
<th>{{ form.name.label_tag }}</th>
|
||||
|
@ -14,11 +13,13 @@
|
|||
{{ form.purpose }} {{ form.type }}
|
||||
{{ form.purpose.errors }}{{ form.type.errors }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ form.requested_duration.label_tag }}</th>
|
||||
<td>{{ form.requested_duration }}{{ form.requested_duration.errors }}</td>
|
||||
</tr>
|
||||
</tr>
|
||||
</table>
|
||||
{% 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>
|
||||
{% endif %}
|
Loading…
Reference in a new issue