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(
|
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'],
|
||||||
|
|
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)
|
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)
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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)),
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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'],
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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',
|
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)")
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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,
|
||||||
|
|
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>
|
<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>
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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; ); }
|
||||||
|
|
|
@ -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 %}
|
||||||
· <span class="session-parent">{{ session.group.parent.acronym }}</span>
|
· <span class="session-parent">{{ session.group.parent.acronym }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if not session.on_agenda %}· <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>
|
||||||
|
|
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,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 %}
|
|
Loading…
Reference in a new issue