Snapshot of dev work to add session purpose annotation
- Legacy-Id: 19415
This commit is contained in:
parent
3386e59a61
commit
1054f90873
|
@ -613,6 +613,93 @@ def action_holder_badge(action_holder):
|
||||||
else:
|
else:
|
||||||
return '' # no alert needed
|
return '' # no alert needed
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def is_regular_agenda_item(assignment):
|
||||||
|
"""Is this agenda item a regular session item?
|
||||||
|
|
||||||
|
A regular item appears as a sub-entry in a timeslot within the agenda
|
||||||
|
|
||||||
|
>>> from collections import namedtuple # use to build mock objects
|
||||||
|
>>> mock_timeslot = namedtuple('t2', ['slug'])
|
||||||
|
>>> mock_assignment = namedtuple('t1', ['slot_type']) # slot_type must be a callable
|
||||||
|
>>> factory = lambda t: mock_assignment(slot_type=lambda: mock_timeslot(slug=t))
|
||||||
|
>>> is_regular_agenda_item(factory('regular'))
|
||||||
|
True
|
||||||
|
|
||||||
|
>>> any(is_regular_agenda_item(factory(t)) for t in ['plenary', 'break', 'reg', 'other', 'officehours'])
|
||||||
|
False
|
||||||
|
"""
|
||||||
|
return assignment.slot_type().slug == 'regular'
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def is_plenary_agenda_item(assignment):
|
||||||
|
"""Is this agenda item a regular session item?
|
||||||
|
|
||||||
|
A regular item appears as a sub-entry in a timeslot within the agenda
|
||||||
|
|
||||||
|
>>> from collections import namedtuple # use to build mock objects
|
||||||
|
>>> mock_timeslot = namedtuple('t2', ['slug'])
|
||||||
|
>>> mock_assignment = namedtuple('t1', ['slot_type']) # slot_type must be a callable
|
||||||
|
>>> factory = lambda t: mock_assignment(slot_type=lambda: mock_timeslot(slug=t))
|
||||||
|
>>> is_plenary_agenda_item(factory('plenary'))
|
||||||
|
True
|
||||||
|
|
||||||
|
>>> any(is_plenary_agenda_item(factory(t)) for t in ['regular', 'break', 'reg', 'other', 'officehours'])
|
||||||
|
False
|
||||||
|
"""
|
||||||
|
return assignment.slot_type().slug == 'plenary'
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def is_special_agenda_item(assignment):
|
||||||
|
"""Is this agenda item a special item?
|
||||||
|
|
||||||
|
Special items appear as top-level agenda entries with their own timeslot information.
|
||||||
|
|
||||||
|
>>> from collections import namedtuple # use to build mock objects
|
||||||
|
>>> mock_timeslot = namedtuple('t2', ['slug'])
|
||||||
|
>>> mock_assignment = namedtuple('t1', ['slot_type']) # slot_type must be a callable
|
||||||
|
>>> factory = lambda t: mock_assignment(slot_type=lambda: mock_timeslot(slug=t))
|
||||||
|
>>> all(is_special_agenda_item(factory(t)) for t in ['break', 'reg', 'other', 'officehours'])
|
||||||
|
True
|
||||||
|
|
||||||
|
>>> any(is_special_agenda_item(factory(t)) for t in ['regular', 'plenary'])
|
||||||
|
False
|
||||||
|
"""
|
||||||
|
return assignment.slot_type().slug in [
|
||||||
|
'break',
|
||||||
|
'reg',
|
||||||
|
'other',
|
||||||
|
'officehours',
|
||||||
|
]
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def should_show_agenda_session_buttons(assignment):
|
||||||
|
"""Should this agenda item show the session buttons (jabber link, etc)?
|
||||||
|
|
||||||
|
In IETF-111 and earlier, office hours sessions were designated by a name ending
|
||||||
|
with ' office hours' and belonged to the IESG or some other group. This led to
|
||||||
|
incorrect session buttons being displayed. Suppress session buttons for
|
||||||
|
when name ends with 'office hours' in the pre-111 meetings.
|
||||||
|
>>> from collections import namedtuple # use to build mock objects
|
||||||
|
>>> mock_meeting = namedtuple('t3', ['number'])
|
||||||
|
>>> mock_session = namedtuple('t2', ['name'])
|
||||||
|
>>> mock_assignment = namedtuple('t1', ['meeting', 'session']) # meeting must be a callable
|
||||||
|
>>> factory = lambda num, name: mock_assignment(session=mock_session(name), meeting=lambda: mock_meeting(num))
|
||||||
|
>>> test_cases = [('105', 'acme office hours'), ('111', 'acme office hours')]
|
||||||
|
>>> any(should_show_agenda_session_buttons(factory(*tc)) for tc in test_cases)
|
||||||
|
False
|
||||||
|
>>> test_cases = [('interim-2020-acme-112', 'acme'), ('112', 'acme'), ('150', 'acme'), ('105', 'acme'),]
|
||||||
|
>>> test_cases.extend([('111', 'acme'), ('interim-2020-acme-112', 'acme office hours')])
|
||||||
|
>>> test_cases.extend([('112', 'acme office hours'), ('150', 'acme office hours')])
|
||||||
|
>>> all(should_show_agenda_session_buttons(factory(*tc)) for tc in test_cases)
|
||||||
|
True
|
||||||
|
"""
|
||||||
|
num = assignment.meeting().number
|
||||||
|
if num.isdigit() and int(num) <= settings.MEETING_LEGACY_OFFICE_HOURS_END:
|
||||||
|
return not assignment.session.name.lower().endswith(' office hours')
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag
|
@register.simple_tag
|
||||||
def absurl(viewname, **kwargs):
|
def absurl(viewname, **kwargs):
|
||||||
|
|
|
@ -188,6 +188,7 @@ class GroupFeaturesAdmin(admin.ModelAdmin):
|
||||||
'customize_workflow',
|
'customize_workflow',
|
||||||
'is_schedulable',
|
'is_schedulable',
|
||||||
'show_on_agenda',
|
'show_on_agenda',
|
||||||
|
'agenda_filter_type',
|
||||||
'req_subm_approval',
|
'req_subm_approval',
|
||||||
'agenda_type',
|
'agenda_type',
|
||||||
'material_types',
|
'material_types',
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Copyright The IETF Trust 2021 All Rights Reserved
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('name', '0033_populate_agendafiltertypename'),
|
||||||
|
('group', '0048_has_session_materials'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='groupfeatures',
|
||||||
|
name='agenda_filter_type',
|
||||||
|
field=models.ForeignKey(default='none', on_delete=django.db.models.deletion.PROTECT, to='name.AgendaFilterTypeName'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Copyright The IETF Trust 2021 All Rights Reserved
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def forward(apps, schema_editor):
|
||||||
|
GroupFeatures = apps.get_model('group', 'GroupFeatures')
|
||||||
|
|
||||||
|
# map AgendaFilterTypeName slug to group types - unlisted get 'none'
|
||||||
|
filter_types = dict(
|
||||||
|
# list previously hard coded in agenda view, plus 'review'
|
||||||
|
normal={'wg', 'ag', 'rg', 'rag', 'iab', 'program', 'review'},
|
||||||
|
heading={'area', 'ietf', 'irtf'},
|
||||||
|
special={'team', 'adhoc'},
|
||||||
|
)
|
||||||
|
|
||||||
|
for ft, group_types in filter_types.items():
|
||||||
|
for gf in GroupFeatures.objects.filter(type__slug__in=group_types):
|
||||||
|
gf.agenda_filter_type_id = ft
|
||||||
|
gf.save()
|
||||||
|
|
||||||
|
|
||||||
|
def reverse(apps, schema_editor):
|
||||||
|
pass # nothing to do, model will be deleted anyway
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('group', '0049_groupfeatures_agenda_filter_type'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(forward, reverse),
|
||||||
|
]
|
24
ietf/group/migrations/0051_groupfeatures_session_purposes.py
Normal file
24
ietf/group/migrations/0051_groupfeatures_session_purposes.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# Copyright The IETF Trust 2021 All Rights Reserved
|
||||||
|
|
||||||
|
# Generated by Django 2.2.24 on 2021-09-26 11:29
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import ietf.group.models
|
||||||
|
import ietf.name.models
|
||||||
|
import jsonfield.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('group', '0050_populate_groupfeatures_agenda_filter_type'),
|
||||||
|
('name', '0035_sessionpurposename'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='groupfeatures',
|
||||||
|
name='session_purposes',
|
||||||
|
field=jsonfield.fields.JSONField(default=[], help_text='Allowed session purposes for this group type', max_length=256, validators=[ietf.group.models.JSONForeignKeyListValidator(ietf.name.models.SessionPurposeName)]),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,48 @@
|
||||||
|
# Copyright The IETF Trust 2021 All Rights Reserved
|
||||||
|
|
||||||
|
# Generated by Django 2.2.24 on 2021-09-26 11:29
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
default_purposes = dict(
|
||||||
|
dir=['presentation', 'social', 'tutorial'],
|
||||||
|
ietf=['admin', 'presentation', 'social'],
|
||||||
|
rg=['session'],
|
||||||
|
team=['coding', 'presentation', 'social', 'tutorial'],
|
||||||
|
wg=['session'],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def forward(apps, schema_editor):
|
||||||
|
GroupFeatures = apps.get_model('group', 'GroupFeatures')
|
||||||
|
SessionPurposeName = apps.get_model('name', 'SessionPurposeName')
|
||||||
|
|
||||||
|
# verify that we're not about to use an invalid purpose
|
||||||
|
for purposes in default_purposes.values():
|
||||||
|
for purpose in purposes:
|
||||||
|
SessionPurposeName.objects.get(pk=purpose) # throws an exception unless exists
|
||||||
|
|
||||||
|
for type_, purposes in default_purposes.items():
|
||||||
|
GroupFeatures.objects.filter(
|
||||||
|
type=type_
|
||||||
|
).update(
|
||||||
|
session_purposes=purposes
|
||||||
|
)
|
||||||
|
|
||||||
|
def reverse(apps, schema_editor):
|
||||||
|
GroupFeatures = apps.get_model('group', 'GroupFeatures')
|
||||||
|
GroupFeatures.objects.update(session_purposes=[]) # clear back out to default
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('group', '0051_groupfeatures_session_purposes'),
|
||||||
|
('name', '0036_populate_sessionpurposename'),
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(forward, reverse),
|
||||||
|
]
|
|
@ -13,7 +13,7 @@ from urllib.parse import urljoin
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.validators import RegexValidator
|
from django.core.validators import RegexValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.deletion import CASCADE
|
from django.db.models.deletion import CASCADE, PROTECT
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
#from simple_history.models import HistoricalRecords
|
#from simple_history.models import HistoricalRecords
|
||||||
|
@ -21,11 +21,13 @@ from django.dispatch import receiver
|
||||||
import debug # pyflakes:ignore
|
import debug # pyflakes:ignore
|
||||||
|
|
||||||
from ietf.group.colors import fg_group_colors, bg_group_colors
|
from ietf.group.colors import fg_group_colors, bg_group_colors
|
||||||
from ietf.name.models import GroupStateName, GroupTypeName, DocTagName, GroupMilestoneStateName, RoleName, AgendaTypeName, ExtResourceName
|
from ietf.name.models import (GroupStateName, GroupTypeName, DocTagName, GroupMilestoneStateName, RoleName,
|
||||||
|
AgendaTypeName, AgendaFilterTypeName, ExtResourceName, SessionPurposeName)
|
||||||
from ietf.person.models import Email, Person
|
from ietf.person.models import Email, Person
|
||||||
from ietf.utils.mail import formataddr, send_mail_text
|
from ietf.utils.mail import formataddr, send_mail_text
|
||||||
from ietf.utils import log
|
from ietf.utils import log
|
||||||
from ietf.utils.models import ForeignKey, OneToOneField
|
from ietf.utils.models import ForeignKey, OneToOneField
|
||||||
|
from ietf.utils.validators import JSONForeignKeyListValidator
|
||||||
|
|
||||||
|
|
||||||
class GroupInfo(models.Model):
|
class GroupInfo(models.Model):
|
||||||
|
@ -250,6 +252,7 @@ validate_comma_separated_roles = RegexValidator(
|
||||||
code='invalid',
|
code='invalid',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class GroupFeatures(models.Model):
|
class GroupFeatures(models.Model):
|
||||||
type = OneToOneField(GroupTypeName, primary_key=True, null=False, related_name='features')
|
type = OneToOneField(GroupTypeName, primary_key=True, null=False, related_name='features')
|
||||||
#history = HistoricalRecords()
|
#history = HistoricalRecords()
|
||||||
|
@ -277,6 +280,7 @@ class GroupFeatures(models.Model):
|
||||||
customize_workflow = models.BooleanField("Workflow", default=False)
|
customize_workflow = models.BooleanField("Workflow", default=False)
|
||||||
is_schedulable = models.BooleanField("Schedulable",default=False)
|
is_schedulable = models.BooleanField("Schedulable",default=False)
|
||||||
show_on_agenda = models.BooleanField("On Agenda", default=False)
|
show_on_agenda = models.BooleanField("On Agenda", default=False)
|
||||||
|
agenda_filter_type = models.ForeignKey(AgendaFilterTypeName, default='none', on_delete=PROTECT)
|
||||||
req_subm_approval = models.BooleanField("Subm. Approval", default=False)
|
req_subm_approval = models.BooleanField("Subm. Approval", default=False)
|
||||||
#
|
#
|
||||||
agenda_type = models.ForeignKey(AgendaTypeName, null=True, default="ietf", on_delete=CASCADE)
|
agenda_type = models.ForeignKey(AgendaTypeName, null=True, default="ietf", on_delete=CASCADE)
|
||||||
|
@ -290,7 +294,10 @@ class GroupFeatures(models.Model):
|
||||||
groupman_authroles = jsonfield.JSONField(max_length=128, blank=False, default=["Secretariat",])
|
groupman_authroles = jsonfield.JSONField(max_length=128, blank=False, default=["Secretariat",])
|
||||||
matman_roles = jsonfield.JSONField(max_length=128, blank=False, default=["ad","chair","delegate","secr"])
|
matman_roles = jsonfield.JSONField(max_length=128, blank=False, default=["ad","chair","delegate","secr"])
|
||||||
role_order = jsonfield.JSONField(max_length=128, blank=False, default=["chair","secr","member"],
|
role_order = jsonfield.JSONField(max_length=128, blank=False, default=["chair","secr","member"],
|
||||||
help_text="The order in which roles are shown, for instance on photo pages. Enter valid JSON.")
|
help_text="The order in which roles are shown, for instance on photo pages. Enter valid JSON.")
|
||||||
|
session_purposes = jsonfield.JSONField(max_length=256, blank=False, default=[],
|
||||||
|
help_text="Allowed session purposes for this group type",
|
||||||
|
validators=[JSONForeignKeyListValidator(SessionPurposeName)])
|
||||||
|
|
||||||
|
|
||||||
class GroupHistory(GroupInfo):
|
class GroupHistory(GroupInfo):
|
||||||
|
|
130
ietf/meeting/fields.py
Normal file
130
ietf/meeting/fields.py
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
import json
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
from ietf.name.models import SessionPurposeName, TimeSlotTypeName
|
||||||
|
|
||||||
|
import debug
|
||||||
|
|
||||||
|
class SessionPurposeAndTypeWidget(forms.MultiWidget):
|
||||||
|
css_class = 'session_purpose_widget' # class to apply to all widgets
|
||||||
|
|
||||||
|
def __init__(self, purpose_choices, type_choices, *args, **kwargs):
|
||||||
|
# Avoid queries on models that need to be migrated into existence - this widget is
|
||||||
|
# instantiated during Django setup. Attempts to query, e.g., SessionPurposeName will
|
||||||
|
# prevent migrations from running.
|
||||||
|
widgets = (
|
||||||
|
forms.Select(
|
||||||
|
choices=purpose_choices,
|
||||||
|
attrs={
|
||||||
|
'class': self.css_class,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
forms.Select(
|
||||||
|
choices=type_choices,
|
||||||
|
attrs={
|
||||||
|
'class': self.css_class,
|
||||||
|
'data-allowed-options': None,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
super().__init__(widgets=widgets, *args, **kwargs)
|
||||||
|
|
||||||
|
# These queryset properties are needed to propagate changes to the querysets after initialization
|
||||||
|
# down to the widgets. The usual mechanisms in the ModelChoiceFields don't handle this for us
|
||||||
|
# because the subwidgets are not attached to Fields in the usual way.
|
||||||
|
@property
|
||||||
|
def purpose_choices(self):
|
||||||
|
return self.widgets[0].choices
|
||||||
|
|
||||||
|
@purpose_choices.setter
|
||||||
|
def purpose_choices(self, value):
|
||||||
|
self.widgets[0].choices = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type_choices(self):
|
||||||
|
return self.widgets[1].choices
|
||||||
|
|
||||||
|
@type_choices.setter
|
||||||
|
def type_choices(self, value):
|
||||||
|
self.widgets[1].choices = value
|
||||||
|
|
||||||
|
def render(self, *args, **kwargs):
|
||||||
|
# Fill in the data-allowed-options (could not do this in init because it needs to
|
||||||
|
# query SessionPurposeName, which will break the migration if done during initialization)
|
||||||
|
self.widgets[1].attrs['data-allowed-options'] = json.dumps(self._allowed_types())
|
||||||
|
return super().render(*args, **kwargs)
|
||||||
|
|
||||||
|
def decompress(self, value):
|
||||||
|
if value:
|
||||||
|
return [getattr(val, 'pk', val) for val in value]
|
||||||
|
else:
|
||||||
|
return [None, None]
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
js = ('secr/js/session_purpose_and_type_widget.js',)
|
||||||
|
|
||||||
|
def _allowed_types(self):
|
||||||
|
"""Map from purpose to allowed type values"""
|
||||||
|
return {
|
||||||
|
purpose.slug: list(purpose.timeslot_types)
|
||||||
|
for purpose in SessionPurposeName.objects.all()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SessionPurposeAndTypeField(forms.MultiValueField):
|
||||||
|
"""Field to update Session purpose and type
|
||||||
|
|
||||||
|
Uses SessionPurposeAndTypeWidget to coordinate setting the session purpose and type to valid
|
||||||
|
combinations. Its value should be a tuple with (purpose, type). Its cleaned value is a
|
||||||
|
namedtuple with purpose and value properties.
|
||||||
|
"""
|
||||||
|
def __init__(self, purpose_queryset=None, type_queryset=None, **kwargs):
|
||||||
|
if purpose_queryset is None:
|
||||||
|
purpose_queryset = SessionPurposeName.objects.none()
|
||||||
|
if type_queryset is None:
|
||||||
|
type_queryset = TimeSlotTypeName.objects.none()
|
||||||
|
fields = (
|
||||||
|
forms.ModelChoiceField(queryset=purpose_queryset, label='Purpose'),
|
||||||
|
forms.ModelChoiceField(queryset=type_queryset, label='Type'),
|
||||||
|
)
|
||||||
|
self.widget = SessionPurposeAndTypeWidget(*(field.choices for field in fields))
|
||||||
|
super().__init__(fields=fields, **kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def purpose_queryset(self):
|
||||||
|
return self.fields[0].queryset
|
||||||
|
|
||||||
|
@purpose_queryset.setter
|
||||||
|
def purpose_queryset(self, value):
|
||||||
|
self.fields[0].queryset = value
|
||||||
|
self.widget.purpose_choices = self.fields[0].choices
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type_queryset(self):
|
||||||
|
return self.fields[1].queryset
|
||||||
|
|
||||||
|
@type_queryset.setter
|
||||||
|
def type_queryset(self, value):
|
||||||
|
self.fields[1].queryset = value
|
||||||
|
self.widget.type_choices = self.fields[1].choices
|
||||||
|
|
||||||
|
def compress(self, data_list):
|
||||||
|
# Convert data from the cleaned list from the widget into a namedtuple
|
||||||
|
if data_list:
|
||||||
|
compressed = namedtuple('CompressedSessionPurposeAndType', 'purpose type')
|
||||||
|
return compressed(*data_list)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def validate(self, value):
|
||||||
|
# Additional validation - value has been passed through compress() already
|
||||||
|
if value.type.pk not in value.purpose.timeslot_types:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
'"%(type)s" is not an allowed type for the purpose "%(purpose)s"',
|
||||||
|
params={'type': value.type, 'purpose': value.purpose},
|
||||||
|
code='invalid_type',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,11 @@
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
import datetime
|
import datetime
|
||||||
|
import json
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core import validators
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.forms import BaseInlineFormSet
|
from django.forms import BaseInlineFormSet
|
||||||
|
@ -21,8 +23,9 @@ from ietf.meeting.models import Session, Meeting, Schedule, countries, timezones
|
||||||
from ietf.meeting.helpers import get_next_interim_number, make_materials_directories
|
from ietf.meeting.helpers import get_next_interim_number, make_materials_directories
|
||||||
from ietf.meeting.helpers import is_interim_meeting_approved, get_next_agenda_name
|
from ietf.meeting.helpers import is_interim_meeting_approved, get_next_agenda_name
|
||||||
from ietf.message.models import Message
|
from ietf.message.models import Message
|
||||||
|
from ietf.name.models import TimeSlotTypeName, SessionPurposeName
|
||||||
from ietf.person.models import Person
|
from ietf.person.models import Person
|
||||||
from ietf.utils.fields import DatepickerDateField, DurationField, MultiEmailField
|
from ietf.utils.fields import DatepickerDateField, DurationField, MultiEmailField, DatepickerSplitDateTimeWidget
|
||||||
from ietf.utils.validators import ( validate_file_size, validate_mime_type,
|
from ietf.utils.validators import ( validate_file_size, validate_mime_type,
|
||||||
validate_file_extension, validate_no_html_frame)
|
validate_file_extension, validate_no_html_frame)
|
||||||
|
|
||||||
|
@ -44,7 +47,8 @@ class GroupModelChoiceField(forms.ModelChoiceField):
|
||||||
return obj.acronym
|
return obj.acronym
|
||||||
|
|
||||||
class CustomDurationField(DurationField):
|
class CustomDurationField(DurationField):
|
||||||
'''Custom DurationField to display as HH:MM (no seconds)'''
|
"""Custom DurationField to display as HH:MM (no seconds)"""
|
||||||
|
widget = forms.TextInput(dict(placeholder='HH:MM'))
|
||||||
def prepare_value(self, value):
|
def prepare_value(self, value):
|
||||||
if isinstance(value, datetime.timedelta):
|
if isinstance(value, datetime.timedelta):
|
||||||
return duration_string(value)
|
return duration_string(value)
|
||||||
|
@ -417,3 +421,212 @@ class SwapTimeslotsForm(forms.Form):
|
||||||
self.fields['origin_timeslot'].queryset = meeting.timeslot_set.all()
|
self.fields['origin_timeslot'].queryset = meeting.timeslot_set.all()
|
||||||
self.fields['target_timeslot'].queryset = meeting.timeslot_set.all()
|
self.fields['target_timeslot'].queryset = meeting.timeslot_set.all()
|
||||||
self.fields['rooms'].queryset = meeting.room_set.all()
|
self.fields['rooms'].queryset = meeting.room_set.all()
|
||||||
|
|
||||||
|
|
||||||
|
class TimeSlotDurationField(CustomDurationField):
|
||||||
|
"""Duration field for TimeSlot edit / create forms"""
|
||||||
|
default_validators=[
|
||||||
|
validators.MinValueValidator(datetime.timedelta(seconds=0)),
|
||||||
|
validators.MaxValueValidator(datetime.timedelta(hours=12)),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
kwargs.setdefault('help_text', 'Duration of timeslot in hours and minutes')
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class TimeSlotEditForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = TimeSlot
|
||||||
|
fields = ('name', 'type', 'time', 'duration', 'show_location', 'location')
|
||||||
|
field_classes = dict(
|
||||||
|
time=forms.SplitDateTimeField,
|
||||||
|
duration=TimeSlotDurationField
|
||||||
|
)
|
||||||
|
widgets = dict(
|
||||||
|
time=DatepickerSplitDateTimeWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(TimeSlotEditForm, self).__init__(*args, **kwargs)
|
||||||
|
self.fields['location'].queryset = self.instance.meeting.room_set.all()
|
||||||
|
|
||||||
|
|
||||||
|
class TimeSlotCreateForm(forms.Form):
|
||||||
|
name = forms.CharField(max_length=255)
|
||||||
|
type = forms.ModelChoiceField(queryset=TimeSlotTypeName.objects.all(), initial='regular')
|
||||||
|
days = forms.TypedMultipleChoiceField(
|
||||||
|
label='Meeting days',
|
||||||
|
widget=forms.CheckboxSelectMultiple,
|
||||||
|
coerce=lambda s: datetime.date.fromordinal(int(s)),
|
||||||
|
empty_value=None,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
other_date = DatepickerDateField(
|
||||||
|
required=False,
|
||||||
|
help_text='Optional date outside the official meeting dates',
|
||||||
|
date_format="yyyy-mm-dd",
|
||||||
|
picker_settings={"autoclose": "1"},
|
||||||
|
)
|
||||||
|
|
||||||
|
time = forms.TimeField(
|
||||||
|
help_text='Time to create timeslot on each selected date',
|
||||||
|
widget=forms.TimeInput(dict(placeholder='HH:MM'))
|
||||||
|
)
|
||||||
|
duration = TimeSlotDurationField()
|
||||||
|
show_location = forms.BooleanField(required=False, initial=True)
|
||||||
|
locations = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=Room.objects.none(),
|
||||||
|
widget=forms.CheckboxSelectMultiple,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, meeting, *args, **kwargs):
|
||||||
|
super(TimeSlotCreateForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
meeting_days = [
|
||||||
|
meeting.date + datetime.timedelta(days=n)
|
||||||
|
for n in range(meeting.days)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Fill in dynamic field properties
|
||||||
|
self.fields['days'].choices = self._day_choices(meeting_days)
|
||||||
|
self.fields['other_date'].widget.attrs['data-date-default-view-date'] = meeting.date
|
||||||
|
self.fields['other_date'].widget.attrs['data-date-dates-disabled'] = ','.join(
|
||||||
|
d.isoformat() for d in meeting_days
|
||||||
|
)
|
||||||
|
self.fields['locations'].queryset = meeting.room_set.order_by('name')
|
||||||
|
|
||||||
|
def clean_other_date(self):
|
||||||
|
# Because other_date is not required, failed field validation does not automatically
|
||||||
|
# invalidate the form. It should, otherwise a typo may be silently ignored.
|
||||||
|
if self.data.get('other_date') and not self.cleaned_data.get('other_date'):
|
||||||
|
raise ValidationError('Enter a valid date or leave field blank.')
|
||||||
|
return self.cleaned_data.get('other_date', None)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
# Merge other_date and days fields
|
||||||
|
try:
|
||||||
|
other_date = self.cleaned_data.pop('other_date')
|
||||||
|
except KeyError:
|
||||||
|
other_date = None
|
||||||
|
|
||||||
|
self.cleaned_data['days'] = self.cleaned_data.get('days') or []
|
||||||
|
if other_date is not None:
|
||||||
|
self.cleaned_data['days'].append(other_date)
|
||||||
|
if len(self.cleaned_data['days']) == 0:
|
||||||
|
self.add_error('days', ValidationError('Please select a day or specify a date'))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _day_choices(days):
|
||||||
|
"""Generates an iterable of value, label pairs for a choice field
|
||||||
|
|
||||||
|
Uses toordinal() to represent dates - would prefer to use isoformat(),
|
||||||
|
but fromisoformat() is not available in python 3.6..
|
||||||
|
"""
|
||||||
|
choices = [
|
||||||
|
(str(day.toordinal()), day.strftime('%A ({})'.format(day.isoformat())))
|
||||||
|
for day in days
|
||||||
|
]
|
||||||
|
return choices
|
||||||
|
|
||||||
|
|
||||||
|
class DurationChoiceField(forms.ChoiceField):
|
||||||
|
duration_choices = (('3600', '60 minutes'), ('7200', '120 minutes'))
|
||||||
|
|
||||||
|
# todo expand range of choices and make configurable
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(
|
||||||
|
choices=(('','--Please select'), *self.duration_choices),
|
||||||
|
*args, **kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
def prepare_value(self, value):
|
||||||
|
"""Converts incoming value into string used for the option value"""
|
||||||
|
if value:
|
||||||
|
return str(int(value.total_seconds())) if hasattr(value, 'total_seconds') else str(value)
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def clean(self, value):
|
||||||
|
"""Convert option value string back to a timedelta"""
|
||||||
|
val = super().clean(value)
|
||||||
|
if val == '':
|
||||||
|
return None
|
||||||
|
return datetime.timedelta(seconds=int(val))
|
||||||
|
|
||||||
|
|
||||||
|
class SessionDetailsForm(forms.ModelForm):
|
||||||
|
requested_duration = DurationChoiceField()
|
||||||
|
|
||||||
|
def __init__(self, group, *args, **kwargs):
|
||||||
|
session_purposes = group.features.session_purposes
|
||||||
|
kwargs.setdefault('initial', {})
|
||||||
|
kwargs['initial'].setdefault(
|
||||||
|
'purpose',
|
||||||
|
session_purposes[0] if len(session_purposes) > 0 else None,
|
||||||
|
)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.fields['type'].widget.attrs.update({
|
||||||
|
'data-allowed-options': json.dumps({
|
||||||
|
purpose.slug: list(purpose.timeslot_types)
|
||||||
|
for purpose in SessionPurposeName.objects.all()
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
self.fields['purpose'].queryset = SessionPurposeName.objects.filter(pk__in=session_purposes)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Session
|
||||||
|
fields = ('name', 'short', 'purpose', 'type', 'requested_duration', 'remote_instructions')
|
||||||
|
labels = {'requested_duration': 'Length'}
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
js = ('ietf/js/meeting/session_details_form.js',)
|
||||||
|
|
||||||
|
|
||||||
|
class SessionDetailsInlineFormset(forms.BaseInlineFormSet):
|
||||||
|
def __init__(self, group, meeting, queryset=None, *args, **kwargs):
|
||||||
|
self._meeting = meeting
|
||||||
|
self.created_instances = []
|
||||||
|
|
||||||
|
# Restrict sessions to the meeting and group. The instance
|
||||||
|
# property handles one of these for free.
|
||||||
|
kwargs['instance'] = group
|
||||||
|
if queryset is None:
|
||||||
|
queryset = Session._default_manager
|
||||||
|
if self._meeting.pk is not None:
|
||||||
|
queryset = queryset.filter(meeting=self._meeting)
|
||||||
|
else:
|
||||||
|
queryset = queryset.none()
|
||||||
|
kwargs['queryset'] = queryset.not_deleted()
|
||||||
|
|
||||||
|
kwargs.setdefault('form_kwargs', {})
|
||||||
|
kwargs['form_kwargs'].update({'group': group})
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def save_new(self, form, commit=True):
|
||||||
|
form.instance.meeting = self._meeting
|
||||||
|
super().save_new(form, commit)
|
||||||
|
|
||||||
|
def save(self, commit=True):
|
||||||
|
existing_instances = set(form.instance for form in self.forms if form.instance.pk)
|
||||||
|
saved = super().save(commit)
|
||||||
|
self.created_instances = [inst for inst in saved if inst not in existing_instances]
|
||||||
|
return saved
|
||||||
|
|
||||||
|
@property
|
||||||
|
def forms_to_keep(self):
|
||||||
|
"""Get the not-deleted forms"""
|
||||||
|
return [f for f in self.forms if f not in self.deleted_forms]
|
||||||
|
|
||||||
|
SessionDetailsFormSet = forms.inlineformset_factory(
|
||||||
|
Group,
|
||||||
|
Session,
|
||||||
|
formset=SessionDetailsInlineFormset,
|
||||||
|
form=SessionDetailsForm,
|
||||||
|
can_delete=True,
|
||||||
|
can_order=False,
|
||||||
|
min_num=1,
|
||||||
|
max_num=3,
|
||||||
|
extra=3,
|
||||||
|
)
|
|
@ -30,10 +30,13 @@ from ietf.mailtrigger.utils import gather_address_lists
|
||||||
from ietf.person.models import Person
|
from ietf.person.models import Person
|
||||||
from ietf.meeting.models import Meeting, Schedule, TimeSlot, SchedTimeSessAssignment, ImportantDate, SchedulingEvent, Session
|
from ietf.meeting.models import Meeting, Schedule, TimeSlot, SchedTimeSessAssignment, ImportantDate, SchedulingEvent, Session
|
||||||
from ietf.meeting.utils import session_requested_by, add_event_info_to_session_qs
|
from ietf.meeting.utils import session_requested_by, add_event_info_to_session_qs
|
||||||
from ietf.name.models import ImportantDateName
|
from ietf.name.models import ImportantDateName, SessionPurposeName
|
||||||
|
from ietf.utils import log
|
||||||
from ietf.utils.history import find_history_active_at, find_history_replacements_active_at
|
from ietf.utils.history import find_history_active_at, find_history_replacements_active_at
|
||||||
from ietf.utils.mail import send_mail
|
from ietf.utils.mail import send_mail
|
||||||
from ietf.utils.pipe import pipe
|
from ietf.utils.pipe import pipe
|
||||||
|
from ietf.utils.text import xslugify
|
||||||
|
|
||||||
|
|
||||||
def find_ads_for_meeting(meeting):
|
def find_ads_for_meeting(meeting):
|
||||||
ads = []
|
ads = []
|
||||||
|
@ -229,7 +232,7 @@ def preprocess_assignments_for_agenda(assignments_queryset, meeting, extra_prefe
|
||||||
|
|
||||||
l = sessions_for_groups.get((a.session.group, a.session.type_id), [])
|
l = sessions_for_groups.get((a.session.group, a.session.type_id), [])
|
||||||
a.session.order_number = l.index(a) + 1 if a in l else 0
|
a.session.order_number = l.index(a) + 1 if a in l else 0
|
||||||
|
|
||||||
parents = Group.objects.filter(pk__in=parent_id_set)
|
parents = Group.objects.filter(pk__in=parent_id_set)
|
||||||
parent_replacements = find_history_replacements_active_at(parents, meeting_time)
|
parent_replacements = find_history_replacements_active_at(parents, meeting_time)
|
||||||
|
|
||||||
|
@ -253,54 +256,459 @@ def preprocess_assignments_for_agenda(assignments_queryset, meeting, extra_prefe
|
||||||
|
|
||||||
return assignments
|
return assignments
|
||||||
|
|
||||||
def is_regular_agenda_filter_group(group):
|
|
||||||
"""Should this group appear in the 'regular' agenda filter button lists?"""
|
|
||||||
return group.type_id in ('wg', 'rg', 'ag', 'rag', 'iab', 'program')
|
|
||||||
|
|
||||||
def tag_assignments_with_filter_keywords(assignments):
|
class AgendaKeywordTool:
|
||||||
"""Add keywords for agenda filtering
|
"""Base class for agenda keyword-related organizers
|
||||||
|
|
||||||
Keywords are all lower case.
|
The purpose of this class is to hold utility methods and data needed by keyword generation
|
||||||
|
helper classes. It ensures consistency of, e.g., definitions of when to use legacy keywords or what
|
||||||
|
timeslot types should be used to define filters.
|
||||||
"""
|
"""
|
||||||
for a in assignments:
|
def __init__(self, *, assignments=None, sessions=None):
|
||||||
a.filter_keywords = {a.timeslot.type.slug.lower()}
|
# n.b., single star argument means only keyword parameters are allowed when calling constructor
|
||||||
a.filter_keywords.update(filter_keywords_for_session(a.session))
|
if assignments is not None and sessions is None:
|
||||||
a.filter_keywords = sorted(list(a.filter_keywords))
|
self.assignments = assignments
|
||||||
|
self.sessions = [a.session for a in self.assignments if a.session]
|
||||||
|
elif sessions is not None and assignments is None:
|
||||||
|
self.assignments = None
|
||||||
|
self.sessions = sessions
|
||||||
|
else:
|
||||||
|
raise RuntimeError('Exactly one of assignments or sessions must be specified')
|
||||||
|
|
||||||
def filter_keywords_for_session(session):
|
self.meeting = self.sessions[0].meeting if len(self.sessions) > 0 else None
|
||||||
keywords = {session.type.slug.lower()}
|
|
||||||
group = getattr(session, 'historic_group', session.group)
|
def _use_legacy_keywords(self):
|
||||||
if group is not None:
|
"""Should legacy keyword handling be used for this meeting?"""
|
||||||
if group.state_id == 'bof':
|
# Only IETF meetings need legacy handling. These are identified
|
||||||
keywords.add('bof')
|
# by having a purely numeric meeting.number.
|
||||||
keywords.add(group.acronym.lower())
|
return (self.meeting is not None
|
||||||
specific_kw = filter_keyword_for_specific_session(session)
|
and self.meeting.number.isdigit()
|
||||||
|
and int(self.meeting.number) <= settings.MEETING_LEGACY_OFFICE_HOURS_END)
|
||||||
|
|
||||||
|
# Helper methods
|
||||||
|
@staticmethod
|
||||||
|
def _get_group(s):
|
||||||
|
"""Get group of a session, handling historic groups"""
|
||||||
|
return getattr(s, 'historic_group', s.group)
|
||||||
|
|
||||||
|
def _get_group_parent(self, s):
|
||||||
|
"""Get parent of a group or parent of a session's group, handling historic groups"""
|
||||||
|
g = self._get_group(s) if isinstance(s, Session) else s # accept a group or a session arg
|
||||||
|
return getattr(g, 'historic_parent', g.parent)
|
||||||
|
|
||||||
|
def _purpose_keyword(self, purpose):
|
||||||
|
"""Get the keyword corresponding to a session purpose"""
|
||||||
|
return purpose.slug.lower()
|
||||||
|
|
||||||
|
def _group_keyword(self, group):
|
||||||
|
"""Get the keyword corresponding to a session group"""
|
||||||
|
return group.acronym.lower()
|
||||||
|
|
||||||
|
def _session_name_keyword(self, session):
|
||||||
|
"""Get the keyword identifying a session by name"""
|
||||||
|
return xslugify(session.name) if session.name else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filterable_purposes(self):
|
||||||
|
return SessionPurposeName.objects.exclude(slug='session').order_by('name')
|
||||||
|
|
||||||
|
|
||||||
|
class AgendaFilterOrganizer(AgendaKeywordTool):
|
||||||
|
"""Helper class to organize agenda filters given a list of assignments or sessions
|
||||||
|
|
||||||
|
Either assignments or sessions must be specified (but not both). Keywords should be applied
|
||||||
|
to these items before calling either of the 'get_' methods, otherwise some special filters
|
||||||
|
may not be included (e.g., 'BoF' or 'Plenary'). If historic_group and/or historic_parent
|
||||||
|
attributes are present, these will be used instead of group/parent.
|
||||||
|
|
||||||
|
The organizer will process its inputs once, when one of its get_ methods is first called.
|
||||||
|
|
||||||
|
Terminology:
|
||||||
|
* column: group of related buttons, usually with a heading button.
|
||||||
|
* heading: button at the top of a column, e.g. an area. Has a keyword that applies to all in its column.
|
||||||
|
* category: a set of columns displayed as separate from other categories
|
||||||
|
* group filters: filters whose keywords derive from the group owning the session, such as for working groups
|
||||||
|
* non-group filters: filters whose keywords come from something other than a session's group
|
||||||
|
* special filters: group filters of type "special" that have no heading, end up in the catch-all column
|
||||||
|
* extra filters: ad hoc filters created based on the extra_labels list, go in the catch-all column
|
||||||
|
* catch-all column: column with no heading where extra filters and special filters are gathered
|
||||||
|
"""
|
||||||
|
# group acronyms in this list will never be used as filter buttons
|
||||||
|
exclude_acronyms = ('iesg', 'ietf', 'secretariat')
|
||||||
|
# extra keywords to include in the no-heading column if they apply to any sessions
|
||||||
|
extra_labels = ('BoF', 'Plenary')
|
||||||
|
# group types whose acronyms should be word-capitalized
|
||||||
|
capitalized_group_types = ('team',)
|
||||||
|
# group types whose acronyms should be all-caps
|
||||||
|
uppercased_group_types = ('area', 'ietf', 'irtf')
|
||||||
|
# check that the group labeling sets are disjoint
|
||||||
|
assert(set(capitalized_group_types).isdisjoint(uppercased_group_types))
|
||||||
|
# group acronyms that need special handling
|
||||||
|
special_group_labels = dict(edu='EDU', iepg='IEPG')
|
||||||
|
|
||||||
|
def __init__(self, *, single_category=False, **kwargs):
|
||||||
|
super(AgendaFilterOrganizer, self).__init__(**kwargs)
|
||||||
|
self.single_category = single_category
|
||||||
|
# filled in when _organize_filters() is called
|
||||||
|
self.filter_categories = None
|
||||||
|
self.special_filters = None
|
||||||
|
|
||||||
|
def get_non_area_keywords(self):
|
||||||
|
"""Get list of any 'non-area' (aka 'special') keywords
|
||||||
|
|
||||||
|
These are the keywords corresponding to the right-most, headingless button column.
|
||||||
|
"""
|
||||||
|
if self.special_filters is None:
|
||||||
|
self._organize_filters()
|
||||||
|
return [sf['keyword'] for sf in self.special_filters['children']]
|
||||||
|
|
||||||
|
def get_filter_categories(self):
|
||||||
|
"""Get a list of filter categories
|
||||||
|
|
||||||
|
If single_category is True, this will be a list with one element. Otherwise it
|
||||||
|
may have multiple elements. Each element is a list of filter columns.
|
||||||
|
"""
|
||||||
|
if self.filter_categories is None:
|
||||||
|
self._organize_filters()
|
||||||
|
return self.filter_categories
|
||||||
|
|
||||||
|
def _organize_filters(self):
|
||||||
|
"""Process inputs to construct and categorize filter lists"""
|
||||||
|
headings, special = self._group_filter_headings()
|
||||||
|
self.filter_categories = self._categorize_group_filters(headings)
|
||||||
|
|
||||||
|
# Create an additional category with non-group filters and special/extra filters
|
||||||
|
non_group_category = self._non_group_filters()
|
||||||
|
|
||||||
|
# special filters include self.extra_labels and any 'special' group filters
|
||||||
|
self.special_filters = self._extra_filters()
|
||||||
|
for g in special:
|
||||||
|
self.special_filters['children'].append(self._group_filter_entry(g))
|
||||||
|
if len(self.special_filters['children']) > 0:
|
||||||
|
self.special_filters['children'].sort(key=self._group_sort_key)
|
||||||
|
non_group_category.append(self.special_filters)
|
||||||
|
|
||||||
|
# if we have any additional filters, add them
|
||||||
|
if len(non_group_category) > 0:
|
||||||
|
if self.single_category:
|
||||||
|
# if a single category is requested, just add them to that category
|
||||||
|
self.filter_categories[0].extend(non_group_category)
|
||||||
|
else:
|
||||||
|
# otherwise add these as a separate category
|
||||||
|
self.filter_categories.append(non_group_category)
|
||||||
|
|
||||||
|
def _group_filter_headings(self):
|
||||||
|
"""Collect group-based filters
|
||||||
|
|
||||||
|
Output is a tuple (dict(group->set), set). The dict keys are groups to be used as headings
|
||||||
|
with sets of child groups as associated values. The set is 'special' groups that have no
|
||||||
|
heading group.
|
||||||
|
"""
|
||||||
|
# groups in the schedule that have a historic_parent group
|
||||||
|
groups = set(self._get_group(s) for s in self.sessions
|
||||||
|
if s
|
||||||
|
and self._get_group(s))
|
||||||
|
log.assertion('len(groups) == len(set(g.acronym for g in groups))') # no repeated acros
|
||||||
|
|
||||||
|
group_parents = set(self._get_group_parent(g) for g in groups if self._get_group_parent(g))
|
||||||
|
log.assertion('len(group_parents) == len(set(gp.acronym for gp in group_parents))') # no repeated acros
|
||||||
|
|
||||||
|
all_groups = groups.union(group_parents)
|
||||||
|
all_groups.difference_update([g for g in all_groups if g.acronym in self.exclude_acronyms])
|
||||||
|
headings = {g: set()
|
||||||
|
for g in all_groups
|
||||||
|
if g.features.agenda_filter_type_id == 'heading'}
|
||||||
|
special = set(g for g in all_groups
|
||||||
|
if g.features.agenda_filter_type_id == 'special')
|
||||||
|
|
||||||
|
for g in groups:
|
||||||
|
if g.features.agenda_filter_type_id == 'normal':
|
||||||
|
# normal filter group with a heading parent goes in that column
|
||||||
|
p = self._get_group_parent(g)
|
||||||
|
if p in headings:
|
||||||
|
headings[p].add(g)
|
||||||
|
else:
|
||||||
|
# normal filter group with no heading parent is 'special'
|
||||||
|
special.add(g)
|
||||||
|
|
||||||
|
return headings, special
|
||||||
|
|
||||||
|
def _categorize_group_filters(self, headings):
|
||||||
|
"""Categorize the group-based filters
|
||||||
|
|
||||||
|
Returns a list of one or more categories of filter columns. When single_category is True,
|
||||||
|
it will always be only one category.
|
||||||
|
"""
|
||||||
|
area_category = [] # headings are area groups
|
||||||
|
non_area_category = [] # headings are non-area groups
|
||||||
|
|
||||||
|
for h in headings:
|
||||||
|
if h.type_id == 'area' or self.single_category:
|
||||||
|
area_category.append(self._group_filter_column(h, headings[h]))
|
||||||
|
else:
|
||||||
|
non_area_category.append(self._group_filter_column(h, headings[h]))
|
||||||
|
area_category.sort(key=self._group_sort_key)
|
||||||
|
if self.single_category:
|
||||||
|
return [area_category]
|
||||||
|
non_area_category.sort(key=self._group_sort_key)
|
||||||
|
return [area_category, non_area_category]
|
||||||
|
|
||||||
|
def _non_group_filters(self):
|
||||||
|
"""Get list of non-group filter columns
|
||||||
|
|
||||||
|
Empty columns will be omitted.
|
||||||
|
"""
|
||||||
|
if self.sessions is None:
|
||||||
|
sessions = [a.session for a in self.assignments]
|
||||||
|
else:
|
||||||
|
sessions = self.sessions
|
||||||
|
|
||||||
|
# Call legacy version for older meetings
|
||||||
|
if self._use_legacy_keywords():
|
||||||
|
return self._legacy_non_group_filters()
|
||||||
|
|
||||||
|
# Not using legacy version
|
||||||
|
filter_cols = []
|
||||||
|
for purpose in self.filterable_purposes:
|
||||||
|
# Map label to its keyword, discarding duplicate labels.
|
||||||
|
# This does what we want as long as sessions with the same
|
||||||
|
# name and purpose belong to the same group.
|
||||||
|
sessions_by_name = {
|
||||||
|
session.name: session
|
||||||
|
for session in sessions if session.purpose == purpose
|
||||||
|
}
|
||||||
|
if len(sessions_by_name) > 0:
|
||||||
|
# keyword needs to match what's tagged in filter_keywords_for_session()
|
||||||
|
heading_kw = self._purpose_keyword(purpose)
|
||||||
|
children = []
|
||||||
|
for name, session in sessions_by_name.items():
|
||||||
|
children.append(self._filter_entry(
|
||||||
|
label=name,
|
||||||
|
keyword=self._session_name_keyword(session),
|
||||||
|
toggled_by=[self._group_keyword(session.group)] if session.group else None,
|
||||||
|
is_bof=False,
|
||||||
|
))
|
||||||
|
column = self._filter_column(
|
||||||
|
label=purpose.name,
|
||||||
|
keyword=heading_kw,
|
||||||
|
children=children,
|
||||||
|
)
|
||||||
|
filter_cols.append(column)
|
||||||
|
|
||||||
|
return filter_cols
|
||||||
|
|
||||||
|
def _legacy_non_group_filters(self):
|
||||||
|
"""Get list of non-group filters for older meetings
|
||||||
|
|
||||||
|
Returns a list of filter columns
|
||||||
|
"""
|
||||||
|
if self.assignments is None:
|
||||||
|
return [] # can only use timeslot type when we have assignments
|
||||||
|
|
||||||
|
office_hours_items = set()
|
||||||
|
suffix = ' office hours'
|
||||||
|
for a in self.assignments:
|
||||||
|
if a.session.name.lower().endswith(suffix):
|
||||||
|
office_hours_items.add((a.session.name[:-len(suffix)].strip(), a.session.group))
|
||||||
|
|
||||||
|
headings = []
|
||||||
|
# currently we only do office hours
|
||||||
|
if len(office_hours_items) > 0:
|
||||||
|
column = self._filter_column(
|
||||||
|
label='Office Hours',
|
||||||
|
keyword='officehours',
|
||||||
|
children=[
|
||||||
|
self._filter_entry(
|
||||||
|
label=label,
|
||||||
|
keyword=f'{label.lower().replace(" ", "")}-officehours',
|
||||||
|
toggled_by=[self._group_keyword(group)] if group else None,
|
||||||
|
is_bof=False,
|
||||||
|
)
|
||||||
|
for label, group in sorted(office_hours_items, key=lambda item: item[0].upper())
|
||||||
|
])
|
||||||
|
headings.append(column)
|
||||||
|
return headings
|
||||||
|
|
||||||
|
def _extra_filters(self):
|
||||||
|
"""Get list of filters corresponding to self.extra_labels"""
|
||||||
|
item_source = self.assignments or self.sessions or []
|
||||||
|
candidates = set(self.extra_labels)
|
||||||
|
return self._filter_column(
|
||||||
|
label=None,
|
||||||
|
keyword=None,
|
||||||
|
children=[
|
||||||
|
self._filter_entry(label=label, keyword=xslugify(label), toggled_by=[], is_bof=False)
|
||||||
|
for label in candidates if any(
|
||||||
|
# Keep only those that will affect at least one session
|
||||||
|
[label.lower() in item.filter_keywords for item in item_source]
|
||||||
|
)]
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _filter_entry(label, keyword, is_bof, toggled_by=None):
|
||||||
|
"""Construct a filter entry representation"""
|
||||||
|
# get our own copy of the list for toggled_by
|
||||||
|
if toggled_by is None:
|
||||||
|
toggled_by = []
|
||||||
|
if is_bof:
|
||||||
|
toggled_by = ['bof'] + toggled_by
|
||||||
|
return dict(
|
||||||
|
label=label,
|
||||||
|
keyword=keyword,
|
||||||
|
toggled_by=toggled_by,
|
||||||
|
is_bof=is_bof,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _filter_column(self, label, keyword, children):
|
||||||
|
"""Construct a filter column given a label, keyword, and list of child entries"""
|
||||||
|
entry = self._filter_entry(label, keyword, False) # heading
|
||||||
|
entry['children'] = children
|
||||||
|
# all children should be controlled by the heading keyword
|
||||||
|
if keyword:
|
||||||
|
for child in children:
|
||||||
|
if keyword not in child['toggled_by']:
|
||||||
|
child['toggled_by'] = [keyword] + child['toggled_by']
|
||||||
|
return entry
|
||||||
|
|
||||||
|
def _group_label(self, group):
|
||||||
|
"""Generate a label for a group filter button"""
|
||||||
|
label = group.acronym
|
||||||
|
if label in self.special_group_labels:
|
||||||
|
return self.special_group_labels[label]
|
||||||
|
elif group.type_id in self.capitalized_group_types:
|
||||||
|
return label.capitalize()
|
||||||
|
elif group.type_id in self.uppercased_group_types:
|
||||||
|
return label.upper()
|
||||||
|
return label
|
||||||
|
|
||||||
|
def _group_filter_entry(self, group):
|
||||||
|
"""Construct a filter_entry for a group filter button"""
|
||||||
|
return self._filter_entry(
|
||||||
|
label=self._group_label(group),
|
||||||
|
keyword=self._group_keyword(group),
|
||||||
|
toggled_by=[self._group_keyword(group.parent)] if group.parent else None,
|
||||||
|
is_bof=group.is_bof(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _group_filter_column(self, heading, children):
|
||||||
|
"""Construct a filter column given a heading group and a list of its child groups"""
|
||||||
|
return self._filter_column(
|
||||||
|
label=None if heading is None else self._group_label(heading),
|
||||||
|
keyword=self._group_keyword(heading),
|
||||||
|
children=sorted([self._group_filter_entry(g) for g in children], key=self._group_sort_key),
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _group_sort_key(g):
|
||||||
|
return 'zzzzzzzz' if g is None else g['label'].upper() # sort blank to the end
|
||||||
|
|
||||||
|
|
||||||
|
class AgendaKeywordTagger(AgendaKeywordTool):
|
||||||
|
"""Class for applying keywords to agenda timeslot assignments.
|
||||||
|
|
||||||
|
This is the other side of the agenda filtering: AgendaFilterOrganizer generates the
|
||||||
|
filter buttons, this applies keywords to the entries being filtered.
|
||||||
|
"""
|
||||||
|
def apply(self):
|
||||||
|
"""Apply tags to sessions / assignments"""
|
||||||
|
if self.assignments is not None:
|
||||||
|
self._tag_assignments_with_filter_keywords()
|
||||||
|
else:
|
||||||
|
self._tag_sessions_with_filter_keywords()
|
||||||
|
|
||||||
|
def apply_session_keywords(self):
|
||||||
|
"""Tag each item with its session-specific keyword"""
|
||||||
|
if self.assignments is not None:
|
||||||
|
for a in self.assignments:
|
||||||
|
a.session_keyword = self.filter_keyword_for_specific_session(a.session)
|
||||||
|
else:
|
||||||
|
for s in self.sessions:
|
||||||
|
s.session_keyword = self.filter_keyword_for_specific_session(s)
|
||||||
|
|
||||||
|
def _is_regular_agenda_filter_group(self, group):
|
||||||
|
"""Should this group appear in the 'regular' agenda filter button lists?"""
|
||||||
|
parent = self._get_group_parent(group)
|
||||||
|
return (
|
||||||
|
group.features.agenda_filter_type_id == 'normal'
|
||||||
|
and parent
|
||||||
|
and parent.features.agenda_filter_type_id == 'heading'
|
||||||
|
)
|
||||||
|
|
||||||
|
def _tag_assignments_with_filter_keywords(self):
|
||||||
|
"""Add keywords for agenda filtering
|
||||||
|
|
||||||
|
Keywords are all lower case.
|
||||||
|
"""
|
||||||
|
for a in self.assignments:
|
||||||
|
a.filter_keywords = {a.slot_type().slug.lower()}
|
||||||
|
a.filter_keywords.update(self._filter_keywords_for_assignment(a))
|
||||||
|
a.filter_keywords = sorted(list(a.filter_keywords))
|
||||||
|
|
||||||
|
def _tag_sessions_with_filter_keywords(self):
|
||||||
|
for s in self.sessions:
|
||||||
|
s.filter_keywords = self._filter_keywords_for_session(s)
|
||||||
|
s.filter_keywords = sorted(list(s.filter_keywords))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _legacy_extra_session_keywords(session):
|
||||||
|
"""Get extra keywords for a session at a legacy meeting"""
|
||||||
|
office_hours_match = re.match(r'^ *\w+(?: +\w+)* +office hours *$', session.name, re.IGNORECASE)
|
||||||
|
if office_hours_match is not None:
|
||||||
|
suffix = 'officehours'
|
||||||
|
return [
|
||||||
|
'officehours',
|
||||||
|
session.name.lower().replace(' ', '')[:-len(suffix)] + '-officehours',
|
||||||
|
]
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _filter_keywords_for_session(self, session):
|
||||||
|
keywords = set()
|
||||||
|
if session.purpose in self.filterable_purposes:
|
||||||
|
keywords.add(self._purpose_keyword(session.purpose))
|
||||||
|
|
||||||
|
group = self._get_group(session)
|
||||||
|
if group is not None:
|
||||||
|
if group.state_id == 'bof':
|
||||||
|
keywords.add('bof')
|
||||||
|
keywords.add(self._group_keyword(group))
|
||||||
|
specific_kw = self.filter_keyword_for_specific_session(session)
|
||||||
if specific_kw is not None:
|
if specific_kw is not None:
|
||||||
keywords.add(specific_kw)
|
keywords.add(specific_kw)
|
||||||
area = getattr(group, 'historic_parent', group.parent)
|
|
||||||
|
kw = self._session_name_keyword(session)
|
||||||
|
if kw:
|
||||||
|
keywords.add(kw)
|
||||||
|
|
||||||
# Only sessions belonging to "regular" groups should respond to the
|
# Only sessions belonging to "regular" groups should respond to the
|
||||||
# parent group filter keyword (often the 'area'). This must match
|
# parent group filter keyword (often the 'area'). This must match
|
||||||
# the test used by the agenda() view to decide whether a group
|
# the test used by the agenda() view to decide whether a group
|
||||||
# gets an area or non-area filter button.
|
# gets an area or non-area filter button.
|
||||||
if is_regular_agenda_filter_group(group) and area is not None:
|
if self._is_regular_agenda_filter_group(group):
|
||||||
keywords.add(area.acronym.lower())
|
area = self._get_group_parent(group)
|
||||||
office_hours_match = re.match(r'^ *\w+(?: +\w+)* +office hours *$', session.name, re.IGNORECASE)
|
if area is not None:
|
||||||
if office_hours_match is not None:
|
keywords.add(self._group_keyword(area))
|
||||||
keywords.update(['officehours', session.name.lower().replace(' ', '')])
|
|
||||||
return sorted(list(keywords))
|
|
||||||
|
|
||||||
def filter_keyword_for_specific_session(session):
|
if self._use_legacy_keywords():
|
||||||
"""Get keyword that identifies a specific session
|
keywords.update(self._legacy_extra_session_keywords(session))
|
||||||
|
|
||||||
|
return sorted(keywords)
|
||||||
|
|
||||||
|
def _filter_keywords_for_assignment(self, assignment):
|
||||||
|
keywords = self._filter_keywords_for_session(assignment.session)
|
||||||
|
return sorted(keywords)
|
||||||
|
|
||||||
|
def filter_keyword_for_specific_session(self, session):
|
||||||
|
"""Get keyword that identifies a specific session
|
||||||
|
|
||||||
|
Returns None if the session cannot be selected individually.
|
||||||
|
"""
|
||||||
|
group = self._get_group(session)
|
||||||
|
if group is None:
|
||||||
|
return None
|
||||||
|
kw = self._group_keyword(group) # start with this
|
||||||
|
token = session.docname_token_only_for_multiple()
|
||||||
|
return kw if token is None else '{}-{}'.format(kw, token)
|
||||||
|
|
||||||
Returns None if the session cannot be selected individually.
|
|
||||||
"""
|
|
||||||
group = getattr(session, 'historic_group', session.group)
|
|
||||||
if group is None:
|
|
||||||
return None
|
|
||||||
kw = group.acronym.lower() # start with this
|
|
||||||
token = session.docname_token_only_for_multiple()
|
|
||||||
return kw if token is None else '{}-{}'.format(kw, token)
|
|
||||||
|
|
||||||
def read_session_file(type, num, doc):
|
def read_session_file(type, num, doc):
|
||||||
# XXXX FIXME: the path fragment in the code below should be moved to
|
# XXXX FIXME: the path fragment in the code below should be moved to
|
||||||
|
|
21
ietf/meeting/migrations/0049_session_purpose.py
Normal file
21
ietf/meeting/migrations/0049_session_purpose.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# Generated by Django 2.2.24 on 2021-09-16 18:04
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import django.db.models.deletion
|
||||||
|
import ietf.utils.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('name', '0036_populate_sessionpurposename'),
|
||||||
|
('meeting', '0048_auto_20211008_0907'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='session',
|
||||||
|
name='purpose',
|
||||||
|
field=ietf.utils.models.ForeignKey(help_text='Purpose of the session', null=True, on_delete=django.db.models.deletion.CASCADE, to='name.SessionPurposeName'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -26,6 +26,7 @@ from django.conf import settings
|
||||||
# mostly used by json_dict()
|
# mostly used by json_dict()
|
||||||
#from django.template.defaultfilters import slugify, date as date_format, time as time_format
|
#from django.template.defaultfilters import slugify, date as date_format, time as time_format
|
||||||
from django.template.defaultfilters import date as date_format
|
from django.template.defaultfilters import date as date_format
|
||||||
|
from django.urls import reverse as urlreverse
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
@ -36,6 +37,7 @@ from ietf.group.utils import can_manage_materials
|
||||||
from ietf.name.models import (
|
from ietf.name.models import (
|
||||||
MeetingTypeName, TimeSlotTypeName, SessionStatusName, ConstraintName, RoomResourceName,
|
MeetingTypeName, TimeSlotTypeName, SessionStatusName, ConstraintName, RoomResourceName,
|
||||||
ImportantDateName, TimerangeName, SlideSubmissionStatusName, ProceedingsMaterialTypeName,
|
ImportantDateName, TimerangeName, SlideSubmissionStatusName, ProceedingsMaterialTypeName,
|
||||||
|
SessionPurposeName,
|
||||||
)
|
)
|
||||||
from ietf.person.models import Person
|
from ietf.person.models import Person
|
||||||
from ietf.utils.decorators import memoize
|
from ietf.utils.decorators import memoize
|
||||||
|
@ -338,6 +340,13 @@ class Meeting(models.Model):
|
||||||
}
|
}
|
||||||
|
|
||||||
def build_timeslices(self):
|
def build_timeslices(self):
|
||||||
|
"""Get unique day/time/timeslot data for meeting
|
||||||
|
|
||||||
|
Returns a list of days, time intervals for each day, and timeslots for each day,
|
||||||
|
with repeated days/time intervals removed. Ignores timeslots that do not have a
|
||||||
|
location. The slots return value contains only one TimeSlot for each distinct
|
||||||
|
time interval.
|
||||||
|
"""
|
||||||
days = [] # the days of the meetings
|
days = [] # the days of the meetings
|
||||||
time_slices = {} # the times on each day
|
time_slices = {} # the times on each day
|
||||||
slots = {}
|
slots = {}
|
||||||
|
@ -359,8 +368,9 @@ class Meeting(models.Model):
|
||||||
|
|
||||||
days.sort()
|
days.sort()
|
||||||
for ymd in time_slices:
|
for ymd in time_slices:
|
||||||
|
# Make sure these sort the same way
|
||||||
time_slices[ymd].sort()
|
time_slices[ymd].sort()
|
||||||
slots[ymd].sort(key=lambda x: x.time)
|
slots[ymd].sort(key=lambda x: (x.time, x.duration))
|
||||||
return days,time_slices,slots
|
return days,time_slices,slots
|
||||||
|
|
||||||
# this functions makes a list of timeslices and rooms, and
|
# this functions makes a list of timeslices and rooms, and
|
||||||
|
@ -487,6 +497,18 @@ class Room(models.Model):
|
||||||
'capacity': self.capacity,
|
'capacity': self.capacity,
|
||||||
}
|
}
|
||||||
# floorplan support
|
# floorplan support
|
||||||
|
def floorplan_url(self):
|
||||||
|
mtg_num = self.meeting.get_number()
|
||||||
|
if not mtg_num:
|
||||||
|
return None
|
||||||
|
elif mtg_num <= settings.FLOORPLAN_LAST_LEGACY_MEETING:
|
||||||
|
base_url = settings.FLOORPLAN_LEGACY_BASE_URL.format(meeting=self.meeting)
|
||||||
|
elif self.floorplan:
|
||||||
|
base_url = urlreverse('ietf.meeting.views.floor_plan', kwargs=dict(num=mtg_num))
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
return f'{base_url}?room={xslugify(self.name)}'
|
||||||
|
|
||||||
def left(self):
|
def left(self):
|
||||||
return min(self.x1, self.x2) if (self.x1 and self.x2) else 0
|
return min(self.x1, self.x2) if (self.x1 and self.x2) else 0
|
||||||
def top(self):
|
def top(self):
|
||||||
|
@ -878,6 +900,14 @@ class SchedTimeSessAssignment(models.Model):
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def meeting(self):
|
||||||
|
"""Get the meeting to which this assignment belongs"""
|
||||||
|
return self.session.meeting
|
||||||
|
|
||||||
|
def slot_type(self):
|
||||||
|
"""Get the TimeSlotTypeName that applies to this assignment"""
|
||||||
|
return self.timeslot.type
|
||||||
|
|
||||||
def json_url(self):
|
def json_url(self):
|
||||||
if not hasattr(self, '_cached_json_url'):
|
if not hasattr(self, '_cached_json_url'):
|
||||||
self._cached_json_url = "/meeting/%s/agenda/%s/%s/session/%u.json" % (
|
self._cached_json_url = "/meeting/%s/agenda/%s/%s/session/%u.json" % (
|
||||||
|
@ -928,12 +958,12 @@ class SchedTimeSessAssignment(models.Model):
|
||||||
|
|
||||||
g = getattr(self.session, "historic_group", None) or self.session.group
|
g = getattr(self.session, "historic_group", None) or self.session.group
|
||||||
|
|
||||||
if self.timeslot.type_id in ('break', 'reg', 'other'):
|
if self.timeslot.type.slug in ('break', 'reg', 'other'):
|
||||||
components.append(g.acronym)
|
components.append(g.acronym)
|
||||||
components.append(slugify(self.session.name))
|
components.append(slugify(self.session.name))
|
||||||
|
|
||||||
if self.timeslot.type_id in ('regular', 'plenary'):
|
if self.timeslot.type.slug in ('regular', 'plenary'):
|
||||||
if self.timeslot.type_id == "plenary":
|
if self.timeslot.type.slug == "plenary":
|
||||||
components.append("1plenary")
|
components.append("1plenary")
|
||||||
else:
|
else:
|
||||||
p = getattr(g, "historic_parent", None) or g.parent
|
p = getattr(g, "historic_parent", None) or g.parent
|
||||||
|
@ -1098,6 +1128,13 @@ class SessionQuerySet(models.QuerySet):
|
||||||
"""
|
"""
|
||||||
return self.with_current_status().exclude(current_status__in=Session.CANCELED_STATUSES)
|
return self.with_current_status().exclude(current_status__in=Session.CANCELED_STATUSES)
|
||||||
|
|
||||||
|
def not_deleted(self):
|
||||||
|
"""Queryset containing all sessions not deleted
|
||||||
|
|
||||||
|
Results annotated with current_status
|
||||||
|
"""
|
||||||
|
return self.with_current_status().exclude(current_status='deleted')
|
||||||
|
|
||||||
def that_can_meet(self):
|
def that_can_meet(self):
|
||||||
"""Queryset containing sessions that can meet
|
"""Queryset containing sessions that can meet
|
||||||
|
|
||||||
|
@ -1120,6 +1157,7 @@ class Session(models.Model):
|
||||||
meeting = ForeignKey(Meeting)
|
meeting = ForeignKey(Meeting)
|
||||||
name = models.CharField(blank=True, max_length=255, help_text="Name of session, in case the session has a purpose rather than just being a group meeting.")
|
name = models.CharField(blank=True, max_length=255, help_text="Name of session, in case the session has a purpose rather than just being a group meeting.")
|
||||||
short = models.CharField(blank=True, max_length=32, help_text="Short version of 'name' above, for use in filenames.")
|
short = models.CharField(blank=True, max_length=32, help_text="Short version of 'name' above, for use in filenames.")
|
||||||
|
purpose = ForeignKey(SessionPurposeName, null=True, help_text='Purpose of the session')
|
||||||
type = ForeignKey(TimeSlotTypeName)
|
type = ForeignKey(TimeSlotTypeName)
|
||||||
group = ForeignKey(Group) # The group type historically determined the session type. BOFs also need to be added as a group. Note that not all meeting requests have a natural group to associate with.
|
group = ForeignKey(Group) # The group type historically determined the session type. BOFs also need to be added as a group. Note that not all meeting requests have a natural group to associate with.
|
||||||
joint_with_groups = models.ManyToManyField(Group, related_name='sessions_joint_in',blank=True)
|
joint_with_groups = models.ManyToManyField(Group, related_name='sessions_joint_in',blank=True)
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
from django import template
|
from django import template
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from ietf.utils.text import xslugify
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@ -67,4 +69,86 @@ def webcal_url(context, viewname, *args, **kwargs):
|
||||||
return 'webcal://{}{}'.format(
|
return 'webcal://{}{}'.format(
|
||||||
context.request.get_host(),
|
context.request.get_host(),
|
||||||
reverse(viewname, args=args, kwargs=kwargs)
|
reverse(viewname, args=args, kwargs=kwargs)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def assignment_display_name(assignment):
|
||||||
|
"""Get name for an assignment"""
|
||||||
|
if assignment.session.type.slug == 'session' and assignment.session.historic_group:
|
||||||
|
return assignment.session.historic_group.name
|
||||||
|
return assignment.session.name or assignment.timeslot.name
|
||||||
|
|
||||||
|
|
||||||
|
class AnchorNode(template.Node):
|
||||||
|
"""Template node for a conditionally-included anchor
|
||||||
|
|
||||||
|
If self.resolve_url() returns a URL, the contents of the nodelist will be rendered inside
|
||||||
|
<a href="{{ self.resolve_url() }}"> ... </a>. If it returns None, the <a> will be omitted.
|
||||||
|
The contents will be rendered in either case.
|
||||||
|
"""
|
||||||
|
def __init__(self, nodelist):
|
||||||
|
self.nodelist = nodelist
|
||||||
|
|
||||||
|
def resolve_url(self, context):
|
||||||
|
raise NotImplementedError('Subclasses must define this method')
|
||||||
|
|
||||||
|
def render(self, context):
|
||||||
|
url = self.resolve_url(context)
|
||||||
|
if url:
|
||||||
|
return '<a href="{}">{}</a>'.format(url, self.nodelist.render(context))
|
||||||
|
else:
|
||||||
|
return self.nodelist.render(context)
|
||||||
|
|
||||||
|
|
||||||
|
class AgendaAnchorNode(AnchorNode):
|
||||||
|
"""Template node for the agenda_anchor tag"""
|
||||||
|
def __init__(self, session, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.session = template.Variable(session)
|
||||||
|
|
||||||
|
def resolve_url(self, context):
|
||||||
|
sess = self.session.resolve(context)
|
||||||
|
agenda = sess.agenda()
|
||||||
|
if agenda:
|
||||||
|
return agenda.get_href()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@register.tag
|
||||||
|
def agenda_anchor(parser, token):
|
||||||
|
"""Block tag that wraps its content in a link to the session agenda, if any"""
|
||||||
|
try:
|
||||||
|
tag_name, sess_var = token.split_contents()
|
||||||
|
except ValueError:
|
||||||
|
raise template.TemplateSyntaxError('agenda_anchor requires a single argument')
|
||||||
|
nodelist = parser.parse(('end_agenda_anchor',))
|
||||||
|
parser.delete_first_token() # delete the end tag
|
||||||
|
return AgendaAnchorNode(sess_var, nodelist)
|
||||||
|
|
||||||
|
|
||||||
|
class LocationAnchorNode(AnchorNode):
|
||||||
|
"""Template node for the location_anchor tag"""
|
||||||
|
def __init__(self, timeslot, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.timeslot = template.Variable(timeslot)
|
||||||
|
|
||||||
|
def resolve_url(self, context):
|
||||||
|
ts = self.timeslot.resolve(context)
|
||||||
|
if ts.show_location and ts.location:
|
||||||
|
return ts.location.floorplan_url()
|
||||||
|
return None
|
||||||
|
|
||||||
|
@register.tag
|
||||||
|
def location_anchor(parser, token):
|
||||||
|
"""Block tag that wraps its content in a link to the timeslot location
|
||||||
|
|
||||||
|
If the timeslot has no location information or is marked with show_location=False,
|
||||||
|
the anchor tag is omitted.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
tag_name, ts_var = token.split_contents()
|
||||||
|
except ValueError:
|
||||||
|
raise template.TemplateSyntaxError('location_anchor requires a single argument')
|
||||||
|
nodelist = parser.parse(('end_location_anchor',))
|
||||||
|
parser.delete_first_token() # delete the end tag
|
||||||
|
return LocationAnchorNode(ts_var, nodelist)
|
||||||
|
|
20
ietf/meeting/templatetags/tests.py
Normal file
20
ietf/meeting/templatetags/tests.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# Copyright The IETF Trust 2009-2020, All Rights Reserved
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from ietf.meeting.templatetags.agenda_custom_tags import AnchorNode
|
||||||
|
from ietf.utils.test_utils import TestCase
|
||||||
|
|
||||||
|
|
||||||
|
class AgendaCustomTagsTests(TestCase):
|
||||||
|
def test_anchor_node_subclasses_implement_resolve_url(self):
|
||||||
|
"""Check that AnchorNode subclasses implement the resolve_url method
|
||||||
|
|
||||||
|
This will only catch errors in subclasses defined in the agenda_custom_tags.py module.
|
||||||
|
"""
|
||||||
|
for subclass in AnchorNode.__subclasses__():
|
||||||
|
try:
|
||||||
|
subclass.resolve_url(None, None)
|
||||||
|
except NotImplementedError:
|
||||||
|
self.fail(f'{subclass.__name__} must implement resolve_url() method')
|
||||||
|
except:
|
||||||
|
pass # any other failure ok since we used garbage inputs
|
|
@ -1,78 +1,96 @@
|
||||||
# Copyright The IETF Trust 2020, All Rights Reserved
|
# Copyright The IETF Trust 2020, All Rights Reserved
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
import copy
|
||||||
|
|
||||||
|
from django.test import override_settings
|
||||||
|
|
||||||
from ietf.group.factories import GroupFactory
|
from ietf.group.factories import GroupFactory
|
||||||
from ietf.meeting.factories import SessionFactory, MeetingFactory
|
from ietf.group.models import Group
|
||||||
from ietf.meeting.helpers import tag_assignments_with_filter_keywords
|
from ietf.meeting.factories import SessionFactory, MeetingFactory, TimeSlotFactory
|
||||||
|
from ietf.meeting.helpers import AgendaFilterOrganizer, AgendaKeywordTagger
|
||||||
|
from ietf.meeting.test_data import make_meeting_test_data
|
||||||
from ietf.utils.test_utils import TestCase
|
from ietf.utils.test_utils import TestCase
|
||||||
|
|
||||||
|
|
||||||
class HelpersTests(TestCase):
|
# override the legacy office hours setting to guarantee consistency with the tests
|
||||||
def do_test_tag_assignments_with_filter_keywords(self, bof=False, historic=None):
|
@override_settings(MEETING_LEGACY_OFFICE_HOURS_END=111)
|
||||||
|
class AgendaKeywordTaggerTests(TestCase):
|
||||||
|
def do_test_tag_assignments_with_filter_keywords(self, meeting_num, bof=False, historic=None):
|
||||||
"""Assignments should be tagged properly
|
"""Assignments should be tagged properly
|
||||||
|
|
||||||
The historic param can be None, group, or parent, to specify whether to test
|
The historic param can be None, group, or parent, to specify whether to test
|
||||||
with no historic_group, a historic_group but no historic_parent, or both.
|
with no historic_group, a historic_group but no historic_parent, or both.
|
||||||
"""
|
"""
|
||||||
meeting_types = ['regular', 'plenary']
|
session_types = ['regular', 'plenary']
|
||||||
|
# decide whether meeting should use legacy keywords (for office hours)
|
||||||
|
legacy_keywords = meeting_num <= 111
|
||||||
|
|
||||||
|
# create meeting and groups
|
||||||
|
meeting = MeetingFactory(type_id='ietf', number=meeting_num)
|
||||||
group_state_id = 'bof' if bof else 'active'
|
group_state_id = 'bof' if bof else 'active'
|
||||||
group = GroupFactory(state_id=group_state_id)
|
group = GroupFactory(state_id=group_state_id)
|
||||||
historic_group = GroupFactory(state_id=group_state_id)
|
|
||||||
historic_parent = GroupFactory(type_id='area')
|
|
||||||
|
|
||||||
if historic == 'parent':
|
# Set up the historic group and parent if needed. Keep track of these as expected_*
|
||||||
historic_group.historic_parent = historic_parent
|
# for later reference. If not using historic group or parent, fall back to the non-historic
|
||||||
|
# groups.
|
||||||
# Create meeting and sessions
|
|
||||||
meeting = MeetingFactory()
|
|
||||||
for meeting_type in meeting_types:
|
|
||||||
sess = SessionFactory(group=group, meeting=meeting, type_id=meeting_type)
|
|
||||||
ts = sess.timeslotassignments.first().timeslot
|
|
||||||
ts.type = sess.type
|
|
||||||
ts.save()
|
|
||||||
|
|
||||||
# Create an office hours session in the group's area (i.e., parent). This is not
|
|
||||||
# currently really needed, but will protect against areas and groups diverging
|
|
||||||
# in a way that breaks keywording.
|
|
||||||
office_hours = SessionFactory(
|
|
||||||
name='some office hours',
|
|
||||||
group=group.parent,
|
|
||||||
meeting=meeting,
|
|
||||||
type_id='other'
|
|
||||||
)
|
|
||||||
ts = office_hours.timeslotassignments.first().timeslot
|
|
||||||
ts.type = office_hours.type
|
|
||||||
ts.save()
|
|
||||||
|
|
||||||
assignments = meeting.schedule.assignments.all()
|
|
||||||
orig_num_assignments = len(assignments)
|
|
||||||
|
|
||||||
# Set up historic groups if needed
|
|
||||||
if historic:
|
if historic:
|
||||||
for a in assignments:
|
expected_group = GroupFactory(state_id=group_state_id)
|
||||||
if a.session != office_hours:
|
if historic == 'parent':
|
||||||
a.session.historic_group = historic_group
|
expected_area = GroupFactory(type_id='area')
|
||||||
|
expected_group.historic_parent = expected_area
|
||||||
# Execute the method under test
|
else:
|
||||||
tag_assignments_with_filter_keywords(assignments)
|
expected_area = expected_group.parent
|
||||||
|
|
||||||
# Assert expected results
|
|
||||||
self.assertEqual(len(assignments), orig_num_assignments, 'Should not change number of assignments')
|
|
||||||
|
|
||||||
if historic:
|
|
||||||
expected_group = historic_group
|
|
||||||
expected_area = historic_parent if historic == 'parent' else historic_group.parent
|
|
||||||
else:
|
else:
|
||||||
expected_group = group
|
expected_group = group
|
||||||
expected_area = group.parent
|
expected_area = group.parent
|
||||||
|
|
||||||
|
# create the ordinary sessions
|
||||||
|
for session_type in session_types:
|
||||||
|
sess = SessionFactory(group=group, meeting=meeting, type_id=session_type, add_to_schedule=False)
|
||||||
|
sess.timeslotassignments.create(
|
||||||
|
timeslot=TimeSlotFactory(meeting=meeting, type_id=session_type),
|
||||||
|
schedule=meeting.schedule,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create an office hours session in the group's area (i.e., parent). Handle this separately
|
||||||
|
# from other session creation to test legacy office hours naming.
|
||||||
|
office_hours = SessionFactory(
|
||||||
|
name='some office hours',
|
||||||
|
group=Group.objects.get(acronym='iesg') if legacy_keywords else expected_area,
|
||||||
|
meeting=meeting,
|
||||||
|
type_id='other' if legacy_keywords else 'officehours',
|
||||||
|
add_to_schedule=False,
|
||||||
|
)
|
||||||
|
office_hours.timeslotassignments.create(
|
||||||
|
timeslot=TimeSlotFactory(meeting=meeting, type_id=office_hours.type_id),
|
||||||
|
schedule=meeting.schedule,
|
||||||
|
)
|
||||||
|
|
||||||
|
assignments = meeting.schedule.assignments.all()
|
||||||
|
orig_num_assignments = len(assignments)
|
||||||
|
|
||||||
|
# Set up historic groups if needed. We've already set the office hours group properly
|
||||||
|
# so skip that session. The expected_group will already have its historic_parent set
|
||||||
|
# if historic == 'parent'
|
||||||
|
if historic:
|
||||||
|
for a in assignments:
|
||||||
|
if a.session != office_hours:
|
||||||
|
a.session.historic_group = expected_group
|
||||||
|
|
||||||
|
# Execute the method under test
|
||||||
|
AgendaKeywordTagger(assignments=assignments).apply()
|
||||||
|
|
||||||
|
# Assert expected results
|
||||||
|
self.assertEqual(len(assignments), orig_num_assignments, 'Should not change number of assignments')
|
||||||
|
|
||||||
for assignment in assignments:
|
for assignment in assignments:
|
||||||
expected_filter_keywords = {assignment.timeslot.type.slug, assignment.session.type.slug}
|
expected_filter_keywords = {assignment.slot_type().slug, assignment.session.type.slug}
|
||||||
|
|
||||||
if assignment.session == office_hours:
|
if assignment.session == office_hours:
|
||||||
expected_filter_keywords.update([
|
expected_filter_keywords.update([
|
||||||
group.parent.acronym,
|
office_hours.group.acronym,
|
||||||
'officehours',
|
'officehours',
|
||||||
'someofficehours',
|
'some-officehours' if legacy_keywords else '{}-officehours'.format(expected_area.acronym),
|
||||||
])
|
])
|
||||||
else:
|
else:
|
||||||
expected_filter_keywords.update([
|
expected_filter_keywords.update([
|
||||||
|
@ -92,9 +110,149 @@ class HelpersTests(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_tag_assignments_with_filter_keywords(self):
|
def test_tag_assignments_with_filter_keywords(self):
|
||||||
self.do_test_tag_assignments_with_filter_keywords()
|
# use distinct meeting numbers > 111 for non-legacy keyword tests
|
||||||
self.do_test_tag_assignments_with_filter_keywords(historic='group')
|
self.do_test_tag_assignments_with_filter_keywords(112)
|
||||||
self.do_test_tag_assignments_with_filter_keywords(historic='parent')
|
self.do_test_tag_assignments_with_filter_keywords(113, historic='group')
|
||||||
self.do_test_tag_assignments_with_filter_keywords(bof=True)
|
self.do_test_tag_assignments_with_filter_keywords(114, historic='parent')
|
||||||
self.do_test_tag_assignments_with_filter_keywords(bof=True, historic='group')
|
self.do_test_tag_assignments_with_filter_keywords(115, bof=True)
|
||||||
self.do_test_tag_assignments_with_filter_keywords(bof=True, historic='parent')
|
self.do_test_tag_assignments_with_filter_keywords(116, bof=True, historic='group')
|
||||||
|
self.do_test_tag_assignments_with_filter_keywords(117, bof=True, historic='parent')
|
||||||
|
|
||||||
|
|
||||||
|
def test_tag_assignments_with_filter_keywords_legacy(self):
|
||||||
|
# use distinct meeting numbers <= 111 for legacy keyword tests
|
||||||
|
self.do_test_tag_assignments_with_filter_keywords(101)
|
||||||
|
self.do_test_tag_assignments_with_filter_keywords(102, historic='group')
|
||||||
|
self.do_test_tag_assignments_with_filter_keywords(103, historic='parent')
|
||||||
|
self.do_test_tag_assignments_with_filter_keywords(104, bof=True)
|
||||||
|
self.do_test_tag_assignments_with_filter_keywords(105, bof=True, historic='group')
|
||||||
|
self.do_test_tag_assignments_with_filter_keywords(106, bof=True, historic='parent')
|
||||||
|
|
||||||
|
|
||||||
|
class AgendaFilterOrganizerTests(TestCase):
|
||||||
|
def test_get_filter_categories(self):
|
||||||
|
# set up
|
||||||
|
meeting = make_meeting_test_data()
|
||||||
|
|
||||||
|
# create extra groups for testing
|
||||||
|
iab = Group.objects.get(acronym='iab')
|
||||||
|
iab_child = GroupFactory(type_id='iab', parent=iab)
|
||||||
|
irtf = Group.objects.get(acronym='irtf')
|
||||||
|
irtf_child = GroupFactory(parent=irtf, state_id='bof')
|
||||||
|
|
||||||
|
# non-area group sessions
|
||||||
|
SessionFactory(group=iab_child, meeting=meeting)
|
||||||
|
SessionFactory(group=irtf_child, meeting=meeting)
|
||||||
|
|
||||||
|
# office hours session
|
||||||
|
SessionFactory(
|
||||||
|
group=Group.objects.get(acronym='farfut'),
|
||||||
|
name='FARFUT office hours',
|
||||||
|
meeting=meeting
|
||||||
|
)
|
||||||
|
|
||||||
|
expected = [
|
||||||
|
[
|
||||||
|
# area category
|
||||||
|
{'label': 'FARFUT', 'keyword': 'farfut', 'is_bof': False,
|
||||||
|
'children': [
|
||||||
|
{'label': 'ames', 'keyword': 'ames', 'is_bof': False},
|
||||||
|
{'label': 'mars', 'keyword': 'mars', 'is_bof': False},
|
||||||
|
]},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
# non-area category
|
||||||
|
{'label': 'IAB', 'keyword': 'iab', 'is_bof': False,
|
||||||
|
'children': [
|
||||||
|
{'label': iab_child.acronym, 'keyword': iab_child.acronym, 'is_bof': False},
|
||||||
|
]},
|
||||||
|
{'label': 'IRTF', 'keyword': 'irtf', 'is_bof': False,
|
||||||
|
'children': [
|
||||||
|
{'label': irtf_child.acronym, 'keyword': irtf_child.acronym, 'is_bof': True},
|
||||||
|
]},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
# non-group category
|
||||||
|
{'label': 'Office Hours', 'keyword': 'officehours', 'is_bof': False,
|
||||||
|
'children': [
|
||||||
|
{'label': 'FARFUT', 'keyword': 'farfut-officehours', 'is_bof': False}
|
||||||
|
]},
|
||||||
|
{'label': None, 'keyword': None,'is_bof': False,
|
||||||
|
'children': [
|
||||||
|
{'label': 'BoF', 'keyword': 'bof', 'is_bof': False},
|
||||||
|
{'label': 'Plenary', 'keyword': 'plenary', 'is_bof': False},
|
||||||
|
]},
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
||||||
|
# when using sessions instead of assignments, won't get timeslot-type based filters
|
||||||
|
expected_with_sessions = copy.deepcopy(expected)
|
||||||
|
expected_with_sessions[-1].pop(0) # pops 'office hours' column
|
||||||
|
|
||||||
|
# put all the above together for single-column tests
|
||||||
|
expected_single_category = [
|
||||||
|
sorted(sum(expected, []), key=lambda col: col['label'] or 'zzzzz')
|
||||||
|
]
|
||||||
|
expected_single_category_with_sessions = [
|
||||||
|
sorted(sum(expected_with_sessions, []), key=lambda col: col['label'] or 'zzzzz')
|
||||||
|
]
|
||||||
|
|
||||||
|
###
|
||||||
|
# test using sessions
|
||||||
|
sessions = meeting.session_set.all()
|
||||||
|
AgendaKeywordTagger(sessions=sessions).apply()
|
||||||
|
|
||||||
|
# default
|
||||||
|
filter_organizer = AgendaFilterOrganizer(sessions=sessions)
|
||||||
|
self.assertEqual(filter_organizer.get_filter_categories(), expected_with_sessions)
|
||||||
|
|
||||||
|
# single-column
|
||||||
|
filter_organizer = AgendaFilterOrganizer(sessions=sessions, single_category=True)
|
||||||
|
self.assertEqual(filter_organizer.get_filter_categories(), expected_single_category_with_sessions)
|
||||||
|
|
||||||
|
###
|
||||||
|
# test again using assignments
|
||||||
|
assignments = meeting.schedule.assignments.all()
|
||||||
|
AgendaKeywordTagger(assignments=assignments).apply()
|
||||||
|
|
||||||
|
# default
|
||||||
|
filter_organizer = AgendaFilterOrganizer(assignments=assignments)
|
||||||
|
self.assertEqual(filter_organizer.get_filter_categories(), expected)
|
||||||
|
|
||||||
|
# single-column
|
||||||
|
filter_organizer = AgendaFilterOrganizer(assignments=assignments, single_category=True)
|
||||||
|
self.assertEqual(filter_organizer.get_filter_categories(), expected_single_category)
|
||||||
|
|
||||||
|
def test_get_non_area_keywords(self):
|
||||||
|
# set up
|
||||||
|
meeting = make_meeting_test_data()
|
||||||
|
|
||||||
|
# create a session in a 'special' group, which should then appear in the non-area keywords
|
||||||
|
team = GroupFactory(type_id='team')
|
||||||
|
SessionFactory(group=team, meeting=meeting)
|
||||||
|
|
||||||
|
# and a BoF
|
||||||
|
bof = GroupFactory(state_id='bof')
|
||||||
|
SessionFactory(group=bof, meeting=meeting)
|
||||||
|
|
||||||
|
expected = sorted(['bof', 'plenary', team.acronym.lower()])
|
||||||
|
|
||||||
|
###
|
||||||
|
# by sessions
|
||||||
|
sessions = meeting.session_set.all()
|
||||||
|
AgendaKeywordTagger(sessions=sessions).apply()
|
||||||
|
filter_organizer = AgendaFilterOrganizer(sessions=sessions)
|
||||||
|
self.assertEqual(filter_organizer.get_non_area_keywords(), expected)
|
||||||
|
|
||||||
|
filter_organizer = AgendaFilterOrganizer(sessions=sessions, single_category=True)
|
||||||
|
self.assertEqual(filter_organizer.get_non_area_keywords(), expected)
|
||||||
|
|
||||||
|
###
|
||||||
|
# by assignments
|
||||||
|
assignments = meeting.schedule.assignments.all()
|
||||||
|
AgendaKeywordTagger(assignments=assignments).apply()
|
||||||
|
filter_organizer = AgendaFilterOrganizer(assignments=assignments)
|
||||||
|
self.assertEqual(filter_organizer.get_non_area_keywords(), expected)
|
||||||
|
|
||||||
|
filter_organizer = AgendaFilterOrganizer(assignments=assignments, single_category=True)
|
||||||
|
self.assertEqual(filter_organizer.get_non_area_keywords(), expected)
|
|
@ -1197,7 +1197,7 @@ class AgendaTests(IetfSeleniumTestCase):
|
||||||
for item in self.get_expected_items():
|
for item in self.get_expected_items():
|
||||||
if item.session.name:
|
if item.session.name:
|
||||||
label = item.session.name
|
label = item.session.name
|
||||||
elif item.timeslot.type_id == 'break':
|
elif item.slot_type().slug == 'break':
|
||||||
label = item.timeslot.name
|
label = item.timeslot.name
|
||||||
elif item.session.group:
|
elif item.session.group:
|
||||||
label = item.session.group.name
|
label = item.session.group.name
|
||||||
|
@ -1308,6 +1308,7 @@ class AgendaTests(IetfSeleniumTestCase):
|
||||||
self.assert_agenda_item_visibility([group_acronym])
|
self.assert_agenda_item_visibility([group_acronym])
|
||||||
|
|
||||||
# Click the group button again
|
# Click the group button again
|
||||||
|
group_button = self.get_agenda_filter_group_button(wait, group_acronym)
|
||||||
group_button.click()
|
group_button.click()
|
||||||
|
|
||||||
# Check visibility
|
# Check visibility
|
||||||
|
@ -1479,7 +1480,7 @@ class AgendaTests(IetfSeleniumTestCase):
|
||||||
ics_url = self.absreverse('ietf.meeting.views.agenda_ical')
|
ics_url = self.absreverse('ietf.meeting.views.agenda_ical')
|
||||||
|
|
||||||
# parse out the events
|
# parse out the events
|
||||||
agenda_rows = self.driver.find_elements_by_css_selector('[id^="row-"]')
|
agenda_rows = self.driver.find_elements_by_css_selector('[id^="row-"]:not(.info)')
|
||||||
visible_rows = [r for r in agenda_rows if r.is_displayed()]
|
visible_rows = [r for r in agenda_rows if r.is_displayed()]
|
||||||
sessions = [self.session_from_agenda_row_id(row.get_attribute("id"))
|
sessions = [self.session_from_agenda_row_id(row.get_attribute("id"))
|
||||||
for row in visible_rows]
|
for row in visible_rows]
|
||||||
|
@ -1736,6 +1737,16 @@ class AgendaTests(IetfSeleniumTestCase):
|
||||||
self.fail('iframe href not updated to contain selected time zone')
|
self.fail('iframe href not updated to contain selected time zone')
|
||||||
|
|
||||||
def test_agenda_session_selection(self):
|
def test_agenda_session_selection(self):
|
||||||
|
# create a second mars session to test selection of specific sessions
|
||||||
|
SessionFactory(
|
||||||
|
meeting=self.meeting,
|
||||||
|
group__acronym='mars',
|
||||||
|
add_to_schedule=False,
|
||||||
|
).timeslotassignments.create(
|
||||||
|
timeslot=TimeSlotFactory(meeting=self.meeting, duration=datetime.timedelta(minutes=60)),
|
||||||
|
schedule=self.meeting.schedule,
|
||||||
|
)
|
||||||
|
|
||||||
wait = WebDriverWait(self.driver, 2)
|
wait = WebDriverWait(self.driver, 2)
|
||||||
url = self.absreverse('ietf.meeting.views.agenda_personalize', kwargs={'num': self.meeting.number})
|
url = self.absreverse('ietf.meeting.views.agenda_personalize', kwargs={'num': self.meeting.number})
|
||||||
self.driver.get(url)
|
self.driver.get(url)
|
||||||
|
@ -1752,47 +1763,52 @@ class AgendaTests(IetfSeleniumTestCase):
|
||||||
'Sessions were selected before being clicked',
|
'Sessions were selected before being clicked',
|
||||||
)
|
)
|
||||||
|
|
||||||
mars_checkbox = self.driver.find_element_by_css_selector('input[type="checkbox"][name="selected-sessions"][data-filter-item="mars"]')
|
mars_sessa_checkbox = self.driver.find_element_by_css_selector('input[type="checkbox"][name="selected-sessions"][data-filter-item="mars-sessa"]')
|
||||||
|
mars_sessb_checkbox = self.driver.find_element_by_css_selector('input[type="checkbox"][name="selected-sessions"][data-filter-item="mars-sessb"]')
|
||||||
|
farfut_button = self.driver.find_element_by_css_selector('button[data-filter-item="farfut"]')
|
||||||
break_checkbox = self.driver.find_element_by_css_selector('input[type="checkbox"][name="selected-sessions"][data-filter-item="secretariat-sessb"]')
|
break_checkbox = self.driver.find_element_by_css_selector('input[type="checkbox"][name="selected-sessions"][data-filter-item="secretariat-sessb"]')
|
||||||
registration_checkbox = self.driver.find_element_by_css_selector('input[type="checkbox"][name="selected-sessions"][data-filter-item="secretariat-sessa"]')
|
registration_checkbox = self.driver.find_element_by_css_selector('input[type="checkbox"][name="selected-sessions"][data-filter-item="secretariat-sessa"]')
|
||||||
secretariat_button = self.driver.find_element_by_css_selector('button[data-filter-item="secretariat"]')
|
|
||||||
|
|
||||||
mars_checkbox.click() # select mars session
|
mars_sessa_checkbox.click() # select mars session
|
||||||
try:
|
try:
|
||||||
wait.until(
|
wait.until(
|
||||||
lambda driver: all('?show=mars' in el.get_attribute('href') for el in elements_to_check)
|
lambda driver: all('?show=mars-sessa' in el.get_attribute('href') for el in elements_to_check)
|
||||||
)
|
)
|
||||||
except TimeoutException:
|
except TimeoutException:
|
||||||
self.fail('Some agenda links were not updated when mars session was selected')
|
self.fail('Some agenda links were not updated when mars session was selected')
|
||||||
self.assertTrue(mars_checkbox.is_selected(), 'mars session checkbox was not selected after being clicked')
|
self.assertTrue(mars_sessa_checkbox.is_selected(), 'mars session A checkbox was not selected after being clicked')
|
||||||
|
self.assertFalse(mars_sessb_checkbox.is_selected(), 'mars session B checkbox was selected without being clicked')
|
||||||
self.assertFalse(break_checkbox.is_selected(), 'break checkbox was selected without being clicked')
|
self.assertFalse(break_checkbox.is_selected(), 'break checkbox was selected without being clicked')
|
||||||
self.assertFalse(registration_checkbox.is_selected(), 'registration checkbox was selected without being clicked')
|
self.assertFalse(registration_checkbox.is_selected(), 'registration checkbox was selected without being clicked')
|
||||||
|
|
||||||
mars_checkbox.click() # deselect mars session
|
mars_sessa_checkbox.click() # deselect mars session
|
||||||
try:
|
try:
|
||||||
wait.until(
|
wait.until(
|
||||||
lambda driver: not any('?show=mars' in el.get_attribute('href') for el in elements_to_check)
|
lambda driver: not any('?show=mars-sessa' in el.get_attribute('href') for el in elements_to_check)
|
||||||
)
|
)
|
||||||
except TimeoutException:
|
except TimeoutException:
|
||||||
self.fail('Some agenda links were not updated when mars session was de-selected')
|
self.fail('Some agenda links were not updated when mars session was de-selected')
|
||||||
self.assertFalse(mars_checkbox.is_selected(), 'mars session checkbox was still selected after being clicked')
|
self.assertFalse(mars_sessa_checkbox.is_selected(), 'mars session A checkbox was still selected after being clicked')
|
||||||
|
self.assertFalse(mars_sessb_checkbox.is_selected(), 'mars session B checkbox was selected without being clicked')
|
||||||
self.assertFalse(break_checkbox.is_selected(), 'break checkbox was selected without being clicked')
|
self.assertFalse(break_checkbox.is_selected(), 'break checkbox was selected without being clicked')
|
||||||
self.assertFalse(registration_checkbox.is_selected(), 'registration checkbox was selected without being clicked')
|
self.assertFalse(registration_checkbox.is_selected(), 'registration checkbox was selected without being clicked')
|
||||||
|
|
||||||
secretariat_button.click() # turn on all secretariat sessions
|
farfut_button.click() # turn on all farfut area sessions
|
||||||
|
mars_sessa_checkbox.click() # but turn off mars session a
|
||||||
break_checkbox.click() # also select the break
|
break_checkbox.click() # also select the break
|
||||||
|
|
||||||
try:
|
try:
|
||||||
wait.until(
|
wait.until(
|
||||||
lambda driver: all(
|
lambda driver: all(
|
||||||
'?show=secretariat&hide=secretariat-sessb' in el.get_attribute('href')
|
'?show=farfut,secretariat-sessb&hide=mars-sessa' in el.get_attribute('href')
|
||||||
for el in elements_to_check
|
for el in elements_to_check
|
||||||
))
|
))
|
||||||
except TimeoutException:
|
except TimeoutException:
|
||||||
self.fail('Some agenda links were not updated when secretariat group but not break was selected')
|
self.fail('Some agenda links were not updated when farfut area was selected')
|
||||||
self.assertFalse(mars_checkbox.is_selected(), 'mars session checkbox was unexpectedly selected')
|
self.assertFalse(mars_sessa_checkbox.is_selected(), 'mars session A checkbox was unexpectedly selected')
|
||||||
self.assertFalse(break_checkbox.is_selected(), 'break checkbox was unexpectedly selected')
|
self.assertTrue(mars_sessb_checkbox.is_selected(), 'mars session B checkbox was expected to be selected')
|
||||||
self.assertTrue(registration_checkbox.is_selected(), 'registration checkbox was expected to be selected')
|
self.assertTrue(break_checkbox.is_selected(), 'break checkbox was expected to be selected')
|
||||||
|
self.assertFalse(registration_checkbox.is_selected(), 'registration checkbox was unexpectedly selected')
|
||||||
|
|
||||||
@ifSeleniumEnabled
|
@ifSeleniumEnabled
|
||||||
class WeekviewTests(IetfSeleniumTestCase):
|
class WeekviewTests(IetfSeleniumTestCase):
|
||||||
|
@ -1814,7 +1830,7 @@ class WeekviewTests(IetfSeleniumTestCase):
|
||||||
for item in self.get_expected_items():
|
for item in self.get_expected_items():
|
||||||
if item.session.name:
|
if item.session.name:
|
||||||
expected_name = item.session.name
|
expected_name = item.session.name
|
||||||
elif item.timeslot.type_id == 'break':
|
elif item.slot_type().slug == 'break':
|
||||||
expected_name = item.timeslot.name
|
expected_name = item.timeslot.name
|
||||||
else:
|
else:
|
||||||
expected_name = item.session.group.name
|
expected_name = item.session.group.name
|
||||||
|
@ -1839,7 +1855,7 @@ class WeekviewTests(IetfSeleniumTestCase):
|
||||||
for item in self.get_expected_items():
|
for item in self.get_expected_items():
|
||||||
if item.session.name:
|
if item.session.name:
|
||||||
expected_name = item.session.name
|
expected_name = item.session.name
|
||||||
elif item.timeslot.type_id == 'break':
|
elif item.slot_type().slug == 'break':
|
||||||
expected_name = item.timeslot.name
|
expected_name = item.timeslot.name
|
||||||
else:
|
else:
|
||||||
expected_name = item.session.group.name
|
expected_name = item.session.group.name
|
||||||
|
@ -2580,6 +2596,165 @@ class ProceedingsMaterialTests(IetfSeleniumTestCase):
|
||||||
'URL field should be shown by default')
|
'URL field should be shown by default')
|
||||||
|
|
||||||
|
|
||||||
|
class EditTimeslotsTests(IetfSeleniumTestCase):
|
||||||
|
"""Test the timeslot editor"""
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.meeting: Meeting = MeetingFactory(
|
||||||
|
type_id='ietf',
|
||||||
|
number=120,
|
||||||
|
date=datetime.datetime.today() + datetime.timedelta(days=10),
|
||||||
|
populate_schedule=False,
|
||||||
|
)
|
||||||
|
self.edit_timeslot_url = self.absreverse(
|
||||||
|
'ietf.meeting.views.edit_timeslots',
|
||||||
|
kwargs=dict(num=self.meeting.number),
|
||||||
|
)
|
||||||
|
self.wait = WebDriverWait(self.driver, 2)
|
||||||
|
|
||||||
|
def do_delete_test(self, selector, keep, delete, cancel=False):
|
||||||
|
self.login('secretary')
|
||||||
|
self.driver.get(self.edit_timeslot_url)
|
||||||
|
delete_button = self.wait.until(
|
||||||
|
expected_conditions.element_to_be_clickable(
|
||||||
|
(By.CSS_SELECTOR, selector)
|
||||||
|
))
|
||||||
|
delete_button.click()
|
||||||
|
|
||||||
|
if cancel:
|
||||||
|
cancel_button = self.wait.until(
|
||||||
|
expected_conditions.element_to_be_clickable(
|
||||||
|
(By.CSS_SELECTOR, '#delete-modal button[data-dismiss="modal"]')
|
||||||
|
))
|
||||||
|
cancel_button.click()
|
||||||
|
else:
|
||||||
|
confirm_button = self.wait.until(
|
||||||
|
expected_conditions.element_to_be_clickable(
|
||||||
|
(By.CSS_SELECTOR, '#confirm-delete-button')
|
||||||
|
))
|
||||||
|
confirm_button.click()
|
||||||
|
|
||||||
|
self.wait.until(
|
||||||
|
expected_conditions.invisibility_of_element_located(
|
||||||
|
(By.CSS_SELECTOR, '#delete-modal')
|
||||||
|
))
|
||||||
|
|
||||||
|
if cancel:
|
||||||
|
keep.extend(delete)
|
||||||
|
delete = []
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
TimeSlot.objects.filter(pk__in=[ts.pk for ts in delete]).count(),
|
||||||
|
0,
|
||||||
|
'Not all expected timeslots deleted',
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
TimeSlot.objects.filter(pk__in=[ts.pk for ts in keep]).count(),
|
||||||
|
len(keep),
|
||||||
|
'Not all expected timeslots kept'
|
||||||
|
)
|
||||||
|
|
||||||
|
def do_delete_timeslot_test(self, cancel=False):
|
||||||
|
delete = [TimeSlotFactory(meeting=self.meeting)]
|
||||||
|
keep = [TimeSlotFactory(meeting=self.meeting)]
|
||||||
|
|
||||||
|
self.do_delete_test(
|
||||||
|
'#timeslot-table #timeslot{} .delete-button'.format(delete[0].pk),
|
||||||
|
keep,
|
||||||
|
delete
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_delete_timeslot(self):
|
||||||
|
"""Delete button for a timeslot should delete that timeslot"""
|
||||||
|
self.do_delete_timeslot_test(cancel=False)
|
||||||
|
|
||||||
|
def test_delete_timeslot_cancel(self):
|
||||||
|
"""Timeslot should not be deleted on cancel"""
|
||||||
|
self.do_delete_timeslot_test(cancel=True)
|
||||||
|
|
||||||
|
def do_delete_time_interval_test(self, cancel=False):
|
||||||
|
delete_day = self.meeting.date.date()
|
||||||
|
delete_time = datetime.time(hour=10)
|
||||||
|
other_day = self.meeting.get_meeting_date(1).date()
|
||||||
|
other_time = datetime.time(hour=12)
|
||||||
|
duration = datetime.timedelta(minutes=60)
|
||||||
|
|
||||||
|
delete: [TimeSlot] = TimeSlotFactory.create_batch(
|
||||||
|
2,
|
||||||
|
meeting=self.meeting,
|
||||||
|
time=datetime.datetime.combine(delete_day, delete_time),
|
||||||
|
duration=duration)
|
||||||
|
|
||||||
|
keep: [TimeSlot] = [
|
||||||
|
TimeSlotFactory(
|
||||||
|
meeting=self.meeting,
|
||||||
|
time=datetime.datetime.combine(day, time),
|
||||||
|
duration=duration
|
||||||
|
)
|
||||||
|
for (day, time) in (
|
||||||
|
# combinations of day/time that should not be deleted
|
||||||
|
(delete_day, other_time),
|
||||||
|
(other_day, delete_time),
|
||||||
|
(other_day, other_time),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
selector = (
|
||||||
|
'#timeslot-table '
|
||||||
|
'.delete-button[data-delete-scope="column"]'
|
||||||
|
'[data-col-id="{}T{}-{}"]'.format(
|
||||||
|
delete_day.isoformat(),
|
||||||
|
delete_time.strftime('%H:%M'),
|
||||||
|
(datetime.datetime.combine(delete_day, delete_time) + duration).strftime(
|
||||||
|
'%H:%M'
|
||||||
|
))
|
||||||
|
)
|
||||||
|
self.do_delete_test(selector, keep, delete, cancel)
|
||||||
|
|
||||||
|
def test_delete_time_interval(self):
|
||||||
|
"""Delete button for a time interval should delete all timeslots in that interval"""
|
||||||
|
self.do_delete_time_interval_test(cancel=False)
|
||||||
|
|
||||||
|
def test_delete_time_interval_cancel(self):
|
||||||
|
"""Should not delete a time interval on cancel"""
|
||||||
|
self.do_delete_time_interval_test(cancel=True)
|
||||||
|
|
||||||
|
def do_delete_day_test(self, cancel=False):
|
||||||
|
delete_day = self.meeting.date.date()
|
||||||
|
times = [datetime.time(hour=10), datetime.time(hour=12)]
|
||||||
|
other_days = [self.meeting.get_meeting_date(d).date() for d in range(1, 3)]
|
||||||
|
|
||||||
|
delete: [TimeSlot] = [
|
||||||
|
TimeSlotFactory(
|
||||||
|
meeting=self.meeting,
|
||||||
|
time=datetime.datetime.combine(delete_day, time),
|
||||||
|
) for time in times
|
||||||
|
]
|
||||||
|
|
||||||
|
keep: [TimeSlot] = [
|
||||||
|
TimeSlotFactory(
|
||||||
|
meeting=self.meeting,
|
||||||
|
time=datetime.datetime.combine(day, time),
|
||||||
|
) for day in other_days for time in times
|
||||||
|
]
|
||||||
|
|
||||||
|
selector = (
|
||||||
|
'#timeslot-table '
|
||||||
|
'.delete-button[data-delete-scope="day"][data-date-id="{}"]'.format(
|
||||||
|
delete_day.isoformat()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.do_delete_test(selector, keep, delete, cancel)
|
||||||
|
|
||||||
|
def test_delete_day(self):
|
||||||
|
"""Delete button for a day should delete all timeslots on that day"""
|
||||||
|
self.do_delete_day_test(cancel=False)
|
||||||
|
|
||||||
|
def test_delete_day_cancel(self):
|
||||||
|
"""Should not delete a day on cancel"""
|
||||||
|
self.do_delete_day_test(cancel=True)
|
||||||
|
|
||||||
|
|
||||||
# The following are useful debugging tools
|
# The following are useful debugging tools
|
||||||
|
|
||||||
# If you add this to a LiveServerTestCase and run just this test, you can browse
|
# If you add this to a LiveServerTestCase and run just this test, you can browse
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -53,6 +53,8 @@ type_ietf_only_patterns = [
|
||||||
url(r'^agendas/diff/$', views.diff_schedules),
|
url(r'^agendas/diff/$', views.diff_schedules),
|
||||||
url(r'^agenda/new/$', views.new_meeting_schedule),
|
url(r'^agenda/new/$', views.new_meeting_schedule),
|
||||||
url(r'^timeslots/edit$', views.edit_timeslots),
|
url(r'^timeslots/edit$', views.edit_timeslots),
|
||||||
|
url(r'^timeslot/new$', views.create_timeslot),
|
||||||
|
url(r'^timeslot/(?P<slot_id>\d+)/edit$', views.edit_timeslot),
|
||||||
url(r'^timeslot/(?P<slot_id>\d+)/edittype$', views.edit_timeslot_type),
|
url(r'^timeslot/(?P<slot_id>\d+)/edittype$', views.edit_timeslot_type),
|
||||||
url(r'^rooms$', ajax.timeslot_roomsurl),
|
url(r'^rooms$', ajax.timeslot_roomsurl),
|
||||||
url(r'^room/(?P<roomid>\d+).json$', ajax.timeslot_roomurl),
|
url(r'^room/(?P<roomid>\d+).json$', ajax.timeslot_roomurl),
|
||||||
|
|
|
@ -529,6 +529,14 @@ def swap_meeting_schedule_timeslot_assignments(schedule, source_timeslots, targe
|
||||||
for a in lts_assignments:
|
for a in lts_assignments:
|
||||||
a.delete()
|
a.delete()
|
||||||
|
|
||||||
|
def bulk_create_timeslots(meeting, times, locations, other_props):
|
||||||
|
"""Creates identical timeslots for Cartesian product of times and locations"""
|
||||||
|
for time in times:
|
||||||
|
for loc in locations:
|
||||||
|
properties = dict(time=time, location=loc)
|
||||||
|
properties.update(other_props)
|
||||||
|
meeting.timeslot_set.create(**properties)
|
||||||
|
|
||||||
def preprocess_meeting_important_dates(meetings):
|
def preprocess_meeting_important_dates(meetings):
|
||||||
for m in meetings:
|
for m in meetings:
|
||||||
m.cached_updated = m.updated()
|
m.cached_updated = m.updated()
|
||||||
|
|
|
@ -57,16 +57,17 @@ from ietf.ietfauth.utils import role_required, has_role, user_is_person
|
||||||
from ietf.mailtrigger.utils import gather_address_lists
|
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 )
|
||||||
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
|
||||||
from ietf.meeting.helpers import get_modified_from_assignments
|
from ietf.meeting.helpers import get_modified_from_assignments
|
||||||
from ietf.meeting.helpers import get_wg_list, find_ads_for_meeting
|
from ietf.meeting.helpers import get_wg_list, find_ads_for_meeting
|
||||||
from ietf.meeting.helpers import get_meeting, get_ietf_meeting, get_current_ietf_meeting_num
|
from ietf.meeting.helpers import get_meeting, get_ietf_meeting, get_current_ietf_meeting_num
|
||||||
from ietf.meeting.helpers import get_schedule, schedule_permissions, is_regular_agenda_filter_group
|
from ietf.meeting.helpers import get_schedule, schedule_permissions
|
||||||
from ietf.meeting.helpers import preprocess_assignments_for_agenda, read_agenda_file
|
from ietf.meeting.helpers import preprocess_assignments_for_agenda, read_agenda_file
|
||||||
from ietf.meeting.helpers import filter_keywords_for_session, tag_assignments_with_filter_keywords, filter_keyword_for_specific_session
|
from ietf.meeting.helpers import AgendaFilterOrganizer, AgendaKeywordTagger
|
||||||
from ietf.meeting.helpers import convert_draft_to_pdf, get_earliest_session_date
|
from ietf.meeting.helpers import convert_draft_to_pdf, get_earliest_session_date
|
||||||
from ietf.meeting.helpers import can_view_interim_request, can_approve_interim_request
|
from ietf.meeting.helpers import can_view_interim_request, can_approve_interim_request
|
||||||
from ietf.meeting.helpers import can_edit_interim_request
|
from ietf.meeting.helpers import can_edit_interim_request
|
||||||
|
@ -84,7 +85,7 @@ from ietf.meeting.utils import current_session_status
|
||||||
from ietf.meeting.utils import data_for_meetings_overview
|
from ietf.meeting.utils import data_for_meetings_overview
|
||||||
from ietf.meeting.utils import preprocess_constraints_for_meeting_schedule_editor
|
from ietf.meeting.utils import preprocess_constraints_for_meeting_schedule_editor
|
||||||
from ietf.meeting.utils import diff_meeting_schedules, prefetch_schedule_diff_objects
|
from ietf.meeting.utils import diff_meeting_schedules, prefetch_schedule_diff_objects
|
||||||
from ietf.meeting.utils import swap_meeting_schedule_timeslot_assignments
|
from ietf.meeting.utils import swap_meeting_schedule_timeslot_assignments, bulk_create_timeslots
|
||||||
from ietf.meeting.utils import preprocess_meeting_important_dates
|
from ietf.meeting.utils import preprocess_meeting_important_dates
|
||||||
from ietf.message.utils import infer_message
|
from ietf.message.utils import infer_message
|
||||||
from ietf.name.models import SlideSubmissionStatusName, ProceedingsMaterialTypeName
|
from ietf.name.models import SlideSubmissionStatusName, ProceedingsMaterialTypeName
|
||||||
|
@ -365,16 +366,49 @@ def edit_timeslots(request, num=None):
|
||||||
|
|
||||||
meeting = get_meeting(num)
|
meeting = get_meeting(num)
|
||||||
|
|
||||||
time_slices,date_slices,slots = meeting.build_timeslices()
|
if request.method == 'POST':
|
||||||
|
# handle AJAX requests
|
||||||
|
action = request.POST.get('action')
|
||||||
|
if action == 'delete':
|
||||||
|
# delete a timeslot
|
||||||
|
# Parameters:
|
||||||
|
# slot_id: comma-separated list of TimeSlot PKs to delete
|
||||||
|
slot_id = request.POST.get('slot_id')
|
||||||
|
if slot_id is None:
|
||||||
|
return HttpResponseBadRequest('missing slot_id')
|
||||||
|
slot_ids = [id.strip() for id in slot_id.split(',')]
|
||||||
|
try:
|
||||||
|
timeslots = meeting.timeslot_set.filter(pk__in=slot_ids)
|
||||||
|
except ValueError:
|
||||||
|
return HttpResponseBadRequest('invalid slot_id specification')
|
||||||
|
missing_ids = set(slot_ids).difference(str(ts.pk) for ts in timeslots)
|
||||||
|
if len(missing_ids) != 0:
|
||||||
|
return HttpResponseNotFound('TimeSlot ids not found in meeting {}: {}'.format(
|
||||||
|
meeting.number,
|
||||||
|
', '.join(sorted(missing_ids))
|
||||||
|
))
|
||||||
|
timeslots.delete()
|
||||||
|
return HttpResponse(content='; '.join('Deleted TimeSlot {}'.format(id) for id in slot_ids))
|
||||||
|
else:
|
||||||
|
return HttpResponseBadRequest('unknown action')
|
||||||
|
|
||||||
|
# Labels here differ from those in the build_timeslices() method. The labels here are
|
||||||
|
# relative to the table: time_slices are the row headings (ie, days), date_slices are
|
||||||
|
# the column headings (i.e., time intervals), and slots are the per-day list of time slots
|
||||||
|
# (with only one time slot per unique time/duration)
|
||||||
|
time_slices, date_slices, slots = meeting.build_timeslices()
|
||||||
|
|
||||||
ts_list = deque()
|
ts_list = deque()
|
||||||
rooms = meeting.room_set.order_by("capacity","name","id")
|
rooms = meeting.room_set.order_by("capacity","name","id")
|
||||||
for room in rooms:
|
for room in rooms:
|
||||||
for day in time_slices:
|
for day in time_slices:
|
||||||
for slice in date_slices[day]:
|
for slice in date_slices[day]:
|
||||||
ts_list.append(room.timeslot_set.filter(time=slice[0],duration=datetime.timedelta(seconds=slice[2])).first())
|
ts_list.append(room.timeslot_set.filter(time=slice[0],duration=datetime.timedelta(seconds=slice[2])))
|
||||||
|
|
||||||
|
|
||||||
|
# Grab these in one query each to identify sessions that are in use and should be handled with care
|
||||||
|
ts_with_official_assignments = meeting.timeslot_set.filter(sessionassignments__schedule=meeting.schedule)
|
||||||
|
ts_with_any_assignments = meeting.timeslot_set.filter(sessionassignments__isnull=False)
|
||||||
|
|
||||||
return render(request, "meeting/timeslot_edit.html",
|
return render(request, "meeting/timeslot_edit.html",
|
||||||
{"rooms":rooms,
|
{"rooms":rooms,
|
||||||
"time_slices":time_slices,
|
"time_slices":time_slices,
|
||||||
|
@ -382,6 +416,8 @@ def edit_timeslots(request, num=None):
|
||||||
"date_slices":date_slices,
|
"date_slices":date_slices,
|
||||||
"meeting":meeting,
|
"meeting":meeting,
|
||||||
"ts_list":ts_list,
|
"ts_list":ts_list,
|
||||||
|
"ts_with_official_assignments": ts_with_official_assignments,
|
||||||
|
"ts_with_any_assignments": ts_with_any_assignments,
|
||||||
})
|
})
|
||||||
|
|
||||||
class NewScheduleForm(forms.ModelForm):
|
class NewScheduleForm(forms.ModelForm):
|
||||||
|
@ -496,7 +532,7 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
|
||||||
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',
|
# session__type='regular',
|
||||||
).order_by('timeslot__time','timeslot__name')
|
).order_by('timeslot__time','timeslot__name')
|
||||||
|
|
||||||
assignments_by_session = defaultdict(list)
|
assignments_by_session = defaultdict(list)
|
||||||
|
@ -508,7 +544,7 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
|
||||||
sessions = add_event_info_to_session_qs(
|
sessions = add_event_info_to_session_qs(
|
||||||
Session.objects.filter(
|
Session.objects.filter(
|
||||||
meeting=meeting,
|
meeting=meeting,
|
||||||
type='regular',
|
# type='regular',
|
||||||
).order_by('pk'),
|
).order_by('pk'),
|
||||||
requested_time=True,
|
requested_time=True,
|
||||||
requested_by=True,
|
requested_by=True,
|
||||||
|
@ -519,7 +555,10 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
|
||||||
'resources', 'group', 'group__parent', 'group__type', 'joint_with_groups',
|
'resources', 'group', 'group__parent', 'group__type', 'joint_with_groups',
|
||||||
)
|
)
|
||||||
|
|
||||||
timeslots_qs = TimeSlot.objects.filter(meeting=meeting, type='regular').prefetch_related('type').order_by('location', 'time', 'name')
|
timeslots_qs = TimeSlot.objects.filter(
|
||||||
|
meeting=meeting,
|
||||||
|
# type='regular',
|
||||||
|
).prefetch_related('type').order_by('location', 'time', 'name')
|
||||||
|
|
||||||
min_duration = min(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)
|
max_duration = max(t.duration for t in timeslots_qs)
|
||||||
|
@ -552,7 +591,7 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
|
||||||
s.requested_by_person = requested_by_lookup.get(s.requested_by)
|
s.requested_by_person = requested_by_lookup.get(s.requested_by)
|
||||||
|
|
||||||
s.scheduling_label = "???"
|
s.scheduling_label = "???"
|
||||||
if s.group:
|
if (s.purpose is None or s.purpose.slug == 'regular') and s.group:
|
||||||
s.scheduling_label = s.group.acronym
|
s.scheduling_label = s.group.acronym
|
||||||
elif s.name:
|
elif s.name:
|
||||||
s.scheduling_label = s.name
|
s.scheduling_label = s.name
|
||||||
|
@ -1035,10 +1074,13 @@ class TimeSlotForm(forms.Form):
|
||||||
if not short:
|
if not short:
|
||||||
self.add_error('short', 'When scheduling this type of time slot, a short name is required')
|
self.add_error('short', 'When scheduling this type of time slot, a short name is required')
|
||||||
|
|
||||||
if self.timeslot and self.timeslot.type_id == 'regular' and self.active_assignment and ts_type.pk != self.timeslot.type_id:
|
if self.timeslot and self.timeslot.type.slug == 'regular' and self.active_assignment and ts_type.slug != self.timeslot.type.slug:
|
||||||
self.add_error('type', "Can't change type on time slots for regular sessions when a session has been assigned")
|
self.add_error('type', "Can't change type on time slots for regular sessions when a session has been assigned")
|
||||||
|
|
||||||
if self.active_assignment and self.active_assignment.session.group != self.cleaned_data.get('group') and self.active_assignment.session.materials.exists() and self.timeslot.type_id != 'regular':
|
if (self.active_assignment
|
||||||
|
and self.active_assignment.session.group != self.cleaned_data.get('group')
|
||||||
|
and self.active_assignment.session.materials.exists()
|
||||||
|
and self.timeslot.type.slug != 'regular'):
|
||||||
self.add_error('group', "Can't change group after materials have been uploaded")
|
self.add_error('group', "Can't change group after materials have been uploaded")
|
||||||
|
|
||||||
|
|
||||||
|
@ -1450,6 +1492,7 @@ def list_schedules(request, num):
|
||||||
return render(request, "meeting/schedule_list.html", {
|
return render(request, "meeting/schedule_list.html", {
|
||||||
'meeting': meeting,
|
'meeting': meeting,
|
||||||
'schedule_groups': schedule_groups,
|
'schedule_groups': schedule_groups,
|
||||||
|
'can_edit_timeslots': is_secretariat,
|
||||||
})
|
})
|
||||||
|
|
||||||
class DiffSchedulesForm(forms.Form):
|
class DiffSchedulesForm(forms.Form):
|
||||||
|
@ -1523,110 +1566,6 @@ def get_assignments_for_agenda(schedule):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def extract_groups_hierarchy(prepped_assignments):
|
|
||||||
"""Extract groups hierarchy for agenda display
|
|
||||||
|
|
||||||
It's a little bit complicated because we can be dealing with historic groups.
|
|
||||||
"""
|
|
||||||
seen = set()
|
|
||||||
groups = [a.session.historic_group for a in prepped_assignments
|
|
||||||
if a.session
|
|
||||||
and a.session.historic_group
|
|
||||||
and is_regular_agenda_filter_group(a.session.historic_group)
|
|
||||||
and a.session.historic_group.historic_parent]
|
|
||||||
group_parents = []
|
|
||||||
for g in groups:
|
|
||||||
if g.historic_parent.acronym not in seen:
|
|
||||||
group_parents.append(g.historic_parent)
|
|
||||||
seen.add(g.historic_parent.acronym)
|
|
||||||
|
|
||||||
seen = set()
|
|
||||||
for p in group_parents:
|
|
||||||
p.group_list = []
|
|
||||||
for g in groups:
|
|
||||||
if g.acronym not in seen and g.historic_parent.acronym == p.acronym:
|
|
||||||
p.group_list.append(g)
|
|
||||||
seen.add(g.acronym)
|
|
||||||
|
|
||||||
p.group_list.sort(key=lambda g: g.acronym)
|
|
||||||
return group_parents
|
|
||||||
|
|
||||||
|
|
||||||
def prepare_filter_keywords(tagged_assignments, group_parents):
|
|
||||||
#
|
|
||||||
# The agenda_filter template expects a list of categorized header buttons, each
|
|
||||||
# with a list of children. Make two categories: the IETF areas and the other parent groups.
|
|
||||||
# We also pass a list of 'extra' buttons - currently Office Hours and miscellaneous filters.
|
|
||||||
# All but the last of these are additionally used by the agenda.html template to make
|
|
||||||
# a list of filtered ical buttons. The last group is ignored for this.
|
|
||||||
area_group_filters = []
|
|
||||||
other_group_filters = []
|
|
||||||
extra_filters = []
|
|
||||||
|
|
||||||
for p in group_parents:
|
|
||||||
new_filter = dict(
|
|
||||||
label=p.acronym.upper(),
|
|
||||||
keyword=p.acronym.lower(),
|
|
||||||
children=[
|
|
||||||
dict(
|
|
||||||
label=g.acronym,
|
|
||||||
keyword=g.acronym.lower(),
|
|
||||||
is_bof=g.is_bof(),
|
|
||||||
) for g in p.group_list
|
|
||||||
]
|
|
||||||
)
|
|
||||||
if p.type.slug == 'area':
|
|
||||||
area_group_filters.append(new_filter)
|
|
||||||
else:
|
|
||||||
other_group_filters.append(new_filter)
|
|
||||||
|
|
||||||
office_hours_labels = set()
|
|
||||||
for a in tagged_assignments:
|
|
||||||
suffix = ' office hours'
|
|
||||||
if a.session.name.lower().endswith(suffix):
|
|
||||||
office_hours_labels.add(a.session.name[:-len(suffix)].strip())
|
|
||||||
|
|
||||||
if len(office_hours_labels) > 0:
|
|
||||||
# keyword needs to match what's tagged in filter_keywords_for_session()
|
|
||||||
extra_filters.append(dict(
|
|
||||||
label='Office Hours',
|
|
||||||
keyword='officehours',
|
|
||||||
children=[
|
|
||||||
dict(
|
|
||||||
label=label,
|
|
||||||
keyword=label.lower().replace(' ', '')+'officehours',
|
|
||||||
is_bof=False,
|
|
||||||
) for label in office_hours_labels
|
|
||||||
]
|
|
||||||
))
|
|
||||||
|
|
||||||
# Keywords that should appear in 'non-area' column
|
|
||||||
non_area_labels = [
|
|
||||||
'BOF', 'EDU', 'Hackathon', 'IEPG', 'IESG', 'IETF', 'Plenary', 'Secretariat', 'Tools',
|
|
||||||
]
|
|
||||||
# Remove any unused non-area keywords
|
|
||||||
non_area_filters = [
|
|
||||||
dict(label=label, keyword=label.lower(), is_bof=False)
|
|
||||||
for label in non_area_labels if any([
|
|
||||||
label.lower() in assignment.filter_keywords
|
|
||||||
for assignment in tagged_assignments
|
|
||||||
])
|
|
||||||
]
|
|
||||||
if len(non_area_filters) > 0:
|
|
||||||
extra_filters.append(dict(
|
|
||||||
label=None,
|
|
||||||
keyword=None,
|
|
||||||
children=non_area_filters,
|
|
||||||
))
|
|
||||||
|
|
||||||
area_group_filters.sort(key=lambda p:p['label'])
|
|
||||||
other_group_filters.sort(key=lambda p:p['label'])
|
|
||||||
filter_categories = [category
|
|
||||||
for category in [area_group_filters, other_group_filters, extra_filters]
|
|
||||||
if len(category) > 0]
|
|
||||||
return filter_categories, non_area_labels
|
|
||||||
|
|
||||||
|
|
||||||
@ensure_csrf_cookie
|
@ensure_csrf_cookie
|
||||||
def agenda(request, num=None, name=None, base=None, ext=None, owner=None, utc=""):
|
def agenda(request, num=None, name=None, base=None, ext=None, owner=None, utc=""):
|
||||||
base = base if base else 'agenda'
|
base = base if base else 'agenda'
|
||||||
|
@ -1639,7 +1578,7 @@ def agenda(request, num=None, name=None, base=None, ext=None, owner=None, utc=""
|
||||||
|
|
||||||
# We do not have the appropriate data in the datatracker for IETF 64 and earlier.
|
# We do not have the appropriate data in the datatracker for IETF 64 and earlier.
|
||||||
# So that we're not producing misleading pages...
|
# So that we're not producing misleading pages...
|
||||||
|
|
||||||
assert num is None or num.isdigit()
|
assert num is None or num.isdigit()
|
||||||
|
|
||||||
meeting = get_ietf_meeting(num)
|
meeting = get_ietf_meeting(num)
|
||||||
|
@ -1667,17 +1606,13 @@ def agenda(request, num=None, name=None, base=None, ext=None, owner=None, utc=""
|
||||||
get_assignments_for_agenda(schedule),
|
get_assignments_for_agenda(schedule),
|
||||||
meeting
|
meeting
|
||||||
)
|
)
|
||||||
tag_assignments_with_filter_keywords(filtered_assignments)
|
AgendaKeywordTagger(assignments=filtered_assignments).apply()
|
||||||
|
|
||||||
# Done processing for CSV output
|
# Done processing for CSV output
|
||||||
if ext == ".csv":
|
if ext == ".csv":
|
||||||
return agenda_csv(schedule, filtered_assignments)
|
return agenda_csv(schedule, filtered_assignments)
|
||||||
|
|
||||||
# Now prep the filter UI
|
filter_organizer = AgendaFilterOrganizer(assignments=filtered_assignments)
|
||||||
filter_categories, non_area_labels = prepare_filter_keywords(
|
|
||||||
filtered_assignments,
|
|
||||||
extract_groups_hierarchy(filtered_assignments),
|
|
||||||
)
|
|
||||||
|
|
||||||
is_current_meeting = (num is None) or (num == get_current_ietf_meeting_num())
|
is_current_meeting = (num is None) or (num == get_current_ietf_meeting_num())
|
||||||
|
|
||||||
|
@ -1685,8 +1620,8 @@ def agenda(request, num=None, name=None, base=None, ext=None, owner=None, utc=""
|
||||||
"schedule": schedule,
|
"schedule": schedule,
|
||||||
"filtered_assignments": filtered_assignments,
|
"filtered_assignments": filtered_assignments,
|
||||||
"updated": updated,
|
"updated": updated,
|
||||||
"filter_categories": filter_categories,
|
"filter_categories": filter_organizer.get_filter_categories(),
|
||||||
"non_area_keywords": [label.lower() for label in non_area_labels],
|
"non_area_keywords": filter_organizer.get_non_area_keywords(),
|
||||||
"now": datetime.datetime.now().astimezone(pytz.UTC),
|
"now": datetime.datetime.now().astimezone(pytz.UTC),
|
||||||
"timezone": meeting.time_zone,
|
"timezone": meeting.time_zone,
|
||||||
"is_current_meeting": is_current_meeting,
|
"is_current_meeting": is_current_meeting,
|
||||||
|
@ -1728,23 +1663,23 @@ def agenda_csv(schedule, filtered_assignments):
|
||||||
row.append(item.timeslot.time.strftime("%H%M"))
|
row.append(item.timeslot.time.strftime("%H%M"))
|
||||||
row.append(item.timeslot.end_time().strftime("%H%M"))
|
row.append(item.timeslot.end_time().strftime("%H%M"))
|
||||||
|
|
||||||
if item.timeslot.type_id == "break":
|
if item.slot_type().slug == "break":
|
||||||
row.append(item.timeslot.type.name)
|
row.append(item.slot_type().name)
|
||||||
row.append(schedule.meeting.break_area)
|
row.append(schedule.meeting.break_area)
|
||||||
row.append("")
|
row.append("")
|
||||||
row.append("")
|
row.append("")
|
||||||
row.append("")
|
row.append("")
|
||||||
row.append(item.timeslot.name)
|
row.append(item.timeslot.name)
|
||||||
row.append("b{}".format(item.timeslot.pk))
|
row.append("b{}".format(item.timeslot.pk))
|
||||||
elif item.timeslot.type_id == "reg":
|
elif item.slot_type().slug == "reg":
|
||||||
row.append(item.timeslot.type.name)
|
row.append(item.slot_type().name)
|
||||||
row.append(schedule.meeting.reg_area)
|
row.append(schedule.meeting.reg_area)
|
||||||
row.append("")
|
row.append("")
|
||||||
row.append("")
|
row.append("")
|
||||||
row.append("")
|
row.append("")
|
||||||
row.append(item.timeslot.name)
|
row.append(item.timeslot.name)
|
||||||
row.append("r{}".format(item.timeslot.pk))
|
row.append("r{}".format(item.timeslot.pk))
|
||||||
elif item.timeslot.type_id == "other":
|
elif item.slot_type().slug == "other":
|
||||||
row.append("None")
|
row.append("None")
|
||||||
row.append(item.timeslot.location.name if item.timeslot.location else "")
|
row.append(item.timeslot.location.name if item.timeslot.location else "")
|
||||||
row.append("")
|
row.append("")
|
||||||
|
@ -1752,7 +1687,7 @@ def agenda_csv(schedule, filtered_assignments):
|
||||||
row.append(item.session.historic_group.historic_parent.acronym.upper() if item.session.historic_group.historic_parent else "")
|
row.append(item.session.historic_group.historic_parent.acronym.upper() if item.session.historic_group.historic_parent else "")
|
||||||
row.append(item.session.name)
|
row.append(item.session.name)
|
||||||
row.append(item.session.pk)
|
row.append(item.session.pk)
|
||||||
elif item.timeslot.type_id == "plenary":
|
elif item.slot_type().slug == "plenary":
|
||||||
row.append(item.session.name)
|
row.append(item.session.name)
|
||||||
row.append(item.timeslot.location.name if item.timeslot.location else "")
|
row.append(item.timeslot.location.name if item.timeslot.location else "")
|
||||||
row.append("")
|
row.append("")
|
||||||
|
@ -1762,7 +1697,7 @@ def agenda_csv(schedule, filtered_assignments):
|
||||||
row.append(item.session.pk)
|
row.append(item.session.pk)
|
||||||
row.append(agenda_field(item))
|
row.append(agenda_field(item))
|
||||||
row.append(slides_field(item))
|
row.append(slides_field(item))
|
||||||
elif item.timeslot.type_id == 'regular':
|
elif item.slot_type().slug == 'regular':
|
||||||
row.append(item.timeslot.name)
|
row.append(item.timeslot.name)
|
||||||
row.append(item.timeslot.location.name if item.timeslot.location else "")
|
row.append(item.timeslot.location.name if item.timeslot.location else "")
|
||||||
row.append(item.session.historic_group.historic_parent.acronym.upper() if item.session.historic_group.historic_parent else "")
|
row.append(item.session.historic_group.historic_parent.acronym.upper() if item.session.historic_group.historic_parent else "")
|
||||||
|
@ -1842,16 +1777,12 @@ def agenda_personalize(request, num):
|
||||||
get_assignments_for_agenda(meeting.schedule),
|
get_assignments_for_agenda(meeting.schedule),
|
||||||
meeting
|
meeting
|
||||||
)
|
)
|
||||||
tag_assignments_with_filter_keywords(filtered_assignments)
|
tagger = AgendaKeywordTagger(assignments=filtered_assignments)
|
||||||
for assignment in filtered_assignments:
|
tagger.apply() # annotate assignments with filter_keywords attribute
|
||||||
# may be None for some sessions
|
tagger.apply_session_keywords() # annotate assignments with session_keyword attribute
|
||||||
assignment.session_keyword = filter_keyword_for_specific_session(assignment.session)
|
|
||||||
|
|
||||||
# Now prep the filter UI
|
# Now prep the filter UI
|
||||||
filter_categories, non_area_labels = prepare_filter_keywords(
|
filter_organizer = AgendaFilterOrganizer(assignments=filtered_assignments)
|
||||||
filtered_assignments,
|
|
||||||
extract_groups_hierarchy(filtered_assignments),
|
|
||||||
)
|
|
||||||
|
|
||||||
is_current_meeting = (num is None) or (num == get_current_ietf_meeting_num())
|
is_current_meeting = (num is None) or (num == get_current_ietf_meeting_num())
|
||||||
|
|
||||||
|
@ -1862,8 +1793,8 @@ def agenda_personalize(request, num):
|
||||||
'schedule': meeting.schedule,
|
'schedule': meeting.schedule,
|
||||||
'updated': meeting.updated(),
|
'updated': meeting.updated(),
|
||||||
'filtered_assignments': filtered_assignments,
|
'filtered_assignments': filtered_assignments,
|
||||||
'filter_categories': filter_categories,
|
'filter_categories': filter_organizer.get_filter_categories(),
|
||||||
'non_area_labels': non_area_labels,
|
'non_area_labels': filter_organizer.get_non_area_keywords(),
|
||||||
'timezone': meeting.time_zone,
|
'timezone': meeting.time_zone,
|
||||||
'is_current_meeting': is_current_meeting,
|
'is_current_meeting': is_current_meeting,
|
||||||
'cache_time': 150 if is_current_meeting else 3600,
|
'cache_time': 150 if is_current_meeting else 3600,
|
||||||
|
@ -1989,7 +1920,7 @@ def week_view(request, num=None, name=None, owner=None):
|
||||||
timeslot__type__private=False,
|
timeslot__type__private=False,
|
||||||
)
|
)
|
||||||
filtered_assignments = preprocess_assignments_for_agenda(filtered_assignments, meeting)
|
filtered_assignments = preprocess_assignments_for_agenda(filtered_assignments, meeting)
|
||||||
tag_assignments_with_filter_keywords(filtered_assignments)
|
AgendaKeywordTagger(assignments=filtered_assignments).apply()
|
||||||
|
|
||||||
items = []
|
items = []
|
||||||
for a in filtered_assignments:
|
for a in filtered_assignments:
|
||||||
|
@ -1998,7 +1929,7 @@ def week_view(request, num=None, name=None, owner=None):
|
||||||
"key": str(a.timeslot.pk),
|
"key": str(a.timeslot.pk),
|
||||||
"utc_time": a.timeslot.utc_start_time().strftime("%Y%m%dT%H%MZ"), # ISO8601 compliant
|
"utc_time": a.timeslot.utc_start_time().strftime("%Y%m%dT%H%MZ"), # ISO8601 compliant
|
||||||
"duration": a.timeslot.duration.seconds,
|
"duration": a.timeslot.duration.seconds,
|
||||||
"type": a.timeslot.type.name,
|
"type": a.slot_type().name,
|
||||||
"filter_keywords": ",".join(a.filter_keywords),
|
"filter_keywords": ",".join(a.filter_keywords),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2008,10 +1939,10 @@ def week_view(request, num=None, name=None, owner=None):
|
||||||
|
|
||||||
if a.session.name:
|
if a.session.name:
|
||||||
item["name"] = a.session.name
|
item["name"] = a.session.name
|
||||||
elif a.timeslot.type_id == "break":
|
elif a.slot_type().slug == "break":
|
||||||
item["name"] = a.timeslot.name
|
item["name"] = a.timeslot.name
|
||||||
item["area"] = a.timeslot.type_id
|
item["area"] = a.slot_type().slug
|
||||||
item["group"] = a.timeslot.type_id
|
item["group"] = a.slot_type().slug
|
||||||
elif a.session.historic_group:
|
elif a.session.historic_group:
|
||||||
item["name"] = a.session.historic_group.name
|
item["name"] = a.session.historic_group.name
|
||||||
if a.session.historic_group.state_id == "bof":
|
if a.session.historic_group.state_id == "bof":
|
||||||
|
@ -2172,7 +2103,7 @@ def agenda_ical(request, num=None, name=None, acronym=None, session_id=None):
|
||||||
timeslot__type__private=False,
|
timeslot__type__private=False,
|
||||||
)
|
)
|
||||||
assignments = preprocess_assignments_for_agenda(assignments, meeting)
|
assignments = preprocess_assignments_for_agenda(assignments, meeting)
|
||||||
tag_assignments_with_filter_keywords(assignments)
|
AgendaKeywordTagger(assignments=assignments).apply()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
filt_params = parse_agenda_filter_params(request.GET)
|
filt_params = parse_agenda_filter_params(request.GET)
|
||||||
|
@ -2333,7 +2264,7 @@ def meeting_requests(request, num=None):
|
||||||
sessions = add_event_info_to_session_qs(
|
sessions = add_event_info_to_session_qs(
|
||||||
Session.objects.filter(
|
Session.objects.filter(
|
||||||
meeting__number=meeting.number,
|
meeting__number=meeting.number,
|
||||||
type__slug='regular',
|
# type__slug='regular',
|
||||||
group__parent__isnull=False
|
group__parent__isnull=False
|
||||||
),
|
),
|
||||||
requested_by=True,
|
requested_by=True,
|
||||||
|
@ -3625,37 +3556,12 @@ def upcoming(request):
|
||||||
)
|
)
|
||||||
).filter(current_status__in=('sched','canceled'))
|
).filter(current_status__in=('sched','canceled'))
|
||||||
|
|
||||||
# get groups for group UI display - same algorithm as in agenda(), but
|
|
||||||
# using group / parent instead of historic_group / historic_parent
|
|
||||||
groups = [s.group for s in interim_sessions
|
|
||||||
if s.group
|
|
||||||
and is_regular_agenda_filter_group(s.group)
|
|
||||||
and s.group.parent]
|
|
||||||
group_parents = {g.parent for g in groups if g.parent}
|
|
||||||
seen = set()
|
|
||||||
for p in group_parents:
|
|
||||||
p.group_list = []
|
|
||||||
for g in groups:
|
|
||||||
if g.acronym not in seen and g.parent.acronym == p.acronym:
|
|
||||||
p.group_list.append(g)
|
|
||||||
seen.add(g.acronym)
|
|
||||||
|
|
||||||
# only one category
|
|
||||||
filter_categories = [[
|
|
||||||
dict(
|
|
||||||
label=p.acronym,
|
|
||||||
keyword=p.acronym.lower(),
|
|
||||||
children=[dict(
|
|
||||||
label=g.acronym,
|
|
||||||
keyword=g.acronym.lower(),
|
|
||||||
is_bof=g.is_bof(),
|
|
||||||
) for g in p.group_list]
|
|
||||||
) for p in group_parents
|
|
||||||
]]
|
|
||||||
|
|
||||||
for session in interim_sessions:
|
for session in interim_sessions:
|
||||||
session.historic_group = session.group
|
session.historic_group = session.group
|
||||||
session.filter_keywords = filter_keywords_for_session(session)
|
|
||||||
|
# Set up for agenda filtering - only one filter_category here
|
||||||
|
AgendaKeywordTagger(sessions=interim_sessions).apply()
|
||||||
|
filter_organizer = AgendaFilterOrganizer(sessions=interim_sessions, single_category=True)
|
||||||
|
|
||||||
entries = list(ietf_meetings)
|
entries = list(ietf_meetings)
|
||||||
entries.extend(list(interim_sessions))
|
entries.extend(list(interim_sessions))
|
||||||
|
@ -3694,7 +3600,7 @@ def upcoming(request):
|
||||||
|
|
||||||
return render(request, 'meeting/upcoming.html', {
|
return render(request, 'meeting/upcoming.html', {
|
||||||
'entries': entries,
|
'entries': entries,
|
||||||
'filter_categories': filter_categories,
|
'filter_categories': filter_organizer.get_filter_categories(),
|
||||||
'menu_actions': actions,
|
'menu_actions': actions,
|
||||||
'menu_entries': menu_entries,
|
'menu_entries': menu_entries,
|
||||||
'selected_menu_entry': selected_menu_entry,
|
'selected_menu_entry': selected_menu_entry,
|
||||||
|
@ -3728,7 +3634,7 @@ def upcoming_ical(request):
|
||||||
'session__group', 'session__group__parent', 'timeslot', 'schedule', 'schedule__meeting'
|
'session__group', 'session__group__parent', 'timeslot', 'schedule', 'schedule__meeting'
|
||||||
).distinct())
|
).distinct())
|
||||||
|
|
||||||
tag_assignments_with_filter_keywords(assignments)
|
AgendaKeywordTagger(assignments=assignments).apply()
|
||||||
|
|
||||||
# apply filters
|
# apply filters
|
||||||
if filter_params is not None:
|
if filter_params is not None:
|
||||||
|
@ -3820,7 +3726,7 @@ def proceedings(request, num=None):
|
||||||
plenaries = sessions.filter(name__icontains='plenary').exclude(current_status='notmeet')
|
plenaries = sessions.filter(name__icontains='plenary').exclude(current_status='notmeet')
|
||||||
ietf = sessions.filter(group__parent__type__slug = 'area').exclude(group__acronym='edu')
|
ietf = sessions.filter(group__parent__type__slug = 'area').exclude(group__acronym='edu')
|
||||||
irtf = sessions.filter(group__parent__acronym = 'irtf')
|
irtf = sessions.filter(group__parent__acronym = 'irtf')
|
||||||
training = sessions.filter(group__acronym__in=['edu','iaoc'], type_id__in=['regular', 'other', ]).exclude(current_status='notmeet')
|
training = sessions.filter(group__acronym__in=['edu','iaoc'], type_id__in=['regular', 'other',]).exclude(current_status='notmeet')
|
||||||
iab = sessions.filter(group__parent__acronym = 'iab').exclude(current_status='notmeet')
|
iab = sessions.filter(group__parent__acronym = 'iab').exclude(current_status='notmeet')
|
||||||
|
|
||||||
cache_version = Document.objects.filter(session__meeting__number=meeting.number).aggregate(Max('time'))["time__max"]
|
cache_version = Document.objects.filter(session__meeting__number=meeting.number).aggregate(Max('time'))["time__max"]
|
||||||
|
@ -4115,11 +4021,65 @@ def edit_timeslot_type(request, num, slot_id):
|
||||||
|
|
||||||
else:
|
else:
|
||||||
form = TimeSlotTypeForm(instance=timeslot)
|
form = TimeSlotTypeForm(instance=timeslot)
|
||||||
|
|
||||||
sessions = timeslot.sessions.filter(timeslotassignments__schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None])
|
sessions = timeslot.sessions.filter(timeslotassignments__schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None])
|
||||||
|
|
||||||
return render(request, 'meeting/edit_timeslot_type.html', {'timeslot':timeslot,'form':form,'sessions':sessions})
|
return render(request, 'meeting/edit_timeslot_type.html', {'timeslot':timeslot,'form':form,'sessions':sessions})
|
||||||
|
|
||||||
|
@role_required('Secretariat')
|
||||||
|
def edit_timeslot(request, num, slot_id):
|
||||||
|
timeslot = get_object_or_404(TimeSlot, id=slot_id)
|
||||||
|
meeting = get_object_or_404(Meeting, number=num)
|
||||||
|
if timeslot.meeting != meeting:
|
||||||
|
raise Http404()
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = TimeSlotEditForm(instance=timeslot, data=request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
return HttpResponseRedirect(reverse('ietf.meeting.views.edit_timeslots', kwargs={'num': num}))
|
||||||
|
else:
|
||||||
|
form = TimeSlotEditForm(instance=timeslot)
|
||||||
|
|
||||||
|
sessions = timeslot.sessions.filter(
|
||||||
|
timeslotassignments__schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None])
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
'meeting/edit_timeslot.html',
|
||||||
|
{'timeslot': timeslot, 'form': form, 'sessions': sessions},
|
||||||
|
status=400 if form.errors else 200,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@role_required('Secretariat')
|
||||||
|
def create_timeslot(request, num):
|
||||||
|
meeting = get_object_or_404(Meeting, number=num)
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = TimeSlotCreateForm(meeting, data=request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
bulk_create_timeslots(
|
||||||
|
meeting,
|
||||||
|
[datetime.datetime.combine(day, form.cleaned_data['time'])
|
||||||
|
for day in form.cleaned_data.get('days', [])],
|
||||||
|
form.cleaned_data['locations'],
|
||||||
|
dict(
|
||||||
|
name=form.cleaned_data['name'],
|
||||||
|
type=form.cleaned_data['type'],
|
||||||
|
duration=form.cleaned_data['duration'],
|
||||||
|
show_location=form.cleaned_data['show_location'],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return HttpResponseRedirect(reverse('ietf.meeting.views.edit_timeslots',kwargs={'num':num}))
|
||||||
|
else:
|
||||||
|
form = TimeSlotCreateForm(meeting)
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
'meeting/create_timeslot.html',
|
||||||
|
dict(meeting=meeting, form=form),
|
||||||
|
status=400 if form.errors else 200,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@role_required('Secretariat')
|
@role_required('Secretariat')
|
||||||
def request_minutes(request, num=None):
|
def request_minutes(request, num=None):
|
||||||
|
|
|
@ -11,7 +11,8 @@ from ietf.name.models import (
|
||||||
ReviewRequestStateName, ReviewResultName, ReviewTypeName, RoleName, RoomResourceName,
|
ReviewRequestStateName, ReviewResultName, ReviewTypeName, RoleName, RoomResourceName,
|
||||||
SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName, TopicAudienceName,
|
SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName, TopicAudienceName,
|
||||||
DocUrlTagName, ReviewAssignmentStateName, ReviewerQueuePolicyName, TimerangeName,
|
DocUrlTagName, ReviewAssignmentStateName, ReviewerQueuePolicyName, TimerangeName,
|
||||||
ExtResourceName, ExtResourceTypeName, SlideSubmissionStatusName, ProceedingsMaterialTypeName)
|
ExtResourceName, ExtResourceTypeName, SlideSubmissionStatusName, ProceedingsMaterialTypeName,
|
||||||
|
AgendaFilterTypeName, SessionPurposeName )
|
||||||
|
|
||||||
|
|
||||||
from ietf.stats.models import CountryAlias
|
from ietf.stats.models import CountryAlias
|
||||||
|
@ -56,6 +57,7 @@ class ProceedingsMaterialTypeNameAdmin(NameAdmin):
|
||||||
list_display = ["slug", "name", "desc", "used", "order",]
|
list_display = ["slug", "name", "desc", "used", "order",]
|
||||||
admin.site.register(ProceedingsMaterialTypeName, ProceedingsMaterialTypeNameAdmin)
|
admin.site.register(ProceedingsMaterialTypeName, ProceedingsMaterialTypeNameAdmin)
|
||||||
|
|
||||||
|
admin.site.register(AgendaFilterTypeName, NameAdmin)
|
||||||
admin.site.register(AgendaTypeName, NameAdmin)
|
admin.site.register(AgendaTypeName, NameAdmin)
|
||||||
admin.site.register(BallotPositionName, NameAdmin)
|
admin.site.register(BallotPositionName, NameAdmin)
|
||||||
admin.site.register(ConstraintName, NameAdmin)
|
admin.site.register(ConstraintName, NameAdmin)
|
||||||
|
@ -94,3 +96,4 @@ admin.site.register(TopicAudienceName, NameAdmin)
|
||||||
admin.site.register(DocUrlTagName, NameAdmin)
|
admin.site.register(DocUrlTagName, NameAdmin)
|
||||||
admin.site.register(ExtResourceTypeName, NameAdmin)
|
admin.site.register(ExtResourceTypeName, NameAdmin)
|
||||||
admin.site.register(SlideSubmissionStatusName, NameAdmin)
|
admin.site.register(SlideSubmissionStatusName, NameAdmin)
|
||||||
|
admin.site.register(SessionPurposeName, NameAdmin)
|
|
@ -2592,6 +2592,7 @@
|
||||||
"about_page": "ietf.group.views.group_about",
|
"about_page": "ietf.group.views.group_about",
|
||||||
"acts_like_wg": false,
|
"acts_like_wg": false,
|
||||||
"admin_roles": "[\n \"chair\"\n]",
|
"admin_roles": "[\n \"chair\"\n]",
|
||||||
|
"agenda_filter_type": "special",
|
||||||
"agenda_type": "ietf",
|
"agenda_type": "ietf",
|
||||||
"create_wiki": true,
|
"create_wiki": true,
|
||||||
"custom_group_roles": false,
|
"custom_group_roles": false,
|
||||||
|
@ -2629,6 +2630,7 @@
|
||||||
"about_page": "ietf.group.views.group_about",
|
"about_page": "ietf.group.views.group_about",
|
||||||
"acts_like_wg": false,
|
"acts_like_wg": false,
|
||||||
"admin_roles": "[\n \"chair\"\n]",
|
"admin_roles": "[\n \"chair\"\n]",
|
||||||
|
"agenda_filter_type": "none",
|
||||||
"agenda_type": "ietf",
|
"agenda_type": "ietf",
|
||||||
"create_wiki": false,
|
"create_wiki": false,
|
||||||
"custom_group_roles": false,
|
"custom_group_roles": false,
|
||||||
|
@ -2664,6 +2666,7 @@
|
||||||
"about_page": "ietf.group.views.group_about",
|
"about_page": "ietf.group.views.group_about",
|
||||||
"acts_like_wg": true,
|
"acts_like_wg": true,
|
||||||
"admin_roles": "[\n \"chair\"\n]",
|
"admin_roles": "[\n \"chair\"\n]",
|
||||||
|
"agenda_filter_type": "normal",
|
||||||
"agenda_type": "ietf",
|
"agenda_type": "ietf",
|
||||||
"create_wiki": true,
|
"create_wiki": true,
|
||||||
"custom_group_roles": true,
|
"custom_group_roles": true,
|
||||||
|
@ -2702,6 +2705,7 @@
|
||||||
"about_page": "ietf.group.views.group_about",
|
"about_page": "ietf.group.views.group_about",
|
||||||
"acts_like_wg": false,
|
"acts_like_wg": false,
|
||||||
"admin_roles": "[\n \"ad\"\n]",
|
"admin_roles": "[\n \"ad\"\n]",
|
||||||
|
"agenda_filter_type": "heading",
|
||||||
"agenda_type": "ietf",
|
"agenda_type": "ietf",
|
||||||
"create_wiki": true,
|
"create_wiki": true,
|
||||||
"custom_group_roles": true,
|
"custom_group_roles": true,
|
||||||
|
@ -2739,6 +2743,7 @@
|
||||||
"about_page": "ietf.group.views.group_about",
|
"about_page": "ietf.group.views.group_about",
|
||||||
"acts_like_wg": false,
|
"acts_like_wg": false,
|
||||||
"admin_roles": "[\n \"chair\",\n \"secr\"\n]",
|
"admin_roles": "[\n \"chair\",\n \"secr\"\n]",
|
||||||
|
"agenda_filter_type": "none",
|
||||||
"agenda_type": "ad",
|
"agenda_type": "ad",
|
||||||
"create_wiki": true,
|
"create_wiki": true,
|
||||||
"custom_group_roles": true,
|
"custom_group_roles": true,
|
||||||
|
@ -2776,6 +2781,7 @@
|
||||||
"about_page": "ietf.group.views.group_about",
|
"about_page": "ietf.group.views.group_about",
|
||||||
"acts_like_wg": false,
|
"acts_like_wg": false,
|
||||||
"admin_roles": "[\n \"chair\"\n]",
|
"admin_roles": "[\n \"chair\"\n]",
|
||||||
|
"agenda_filter_type": "normal",
|
||||||
"agenda_type": "ietf",
|
"agenda_type": "ietf",
|
||||||
"create_wiki": false,
|
"create_wiki": false,
|
||||||
"custom_group_roles": true,
|
"custom_group_roles": true,
|
||||||
|
@ -2850,6 +2856,7 @@
|
||||||
"about_page": "ietf.group.views.group_about",
|
"about_page": "ietf.group.views.group_about",
|
||||||
"acts_like_wg": false,
|
"acts_like_wg": false,
|
||||||
"admin_roles": "[\n \"chair\"\n]",
|
"admin_roles": "[\n \"chair\"\n]",
|
||||||
|
"agenda_filter_type": "none",
|
||||||
"agenda_type": "ietf",
|
"agenda_type": "ietf",
|
||||||
"create_wiki": false,
|
"create_wiki": false,
|
||||||
"custom_group_roles": false,
|
"custom_group_roles": false,
|
||||||
|
@ -2885,6 +2892,7 @@
|
||||||
"about_page": "ietf.group.views.group_about",
|
"about_page": "ietf.group.views.group_about",
|
||||||
"acts_like_wg": false,
|
"acts_like_wg": false,
|
||||||
"admin_roles": "[\n \"chair\"\n]",
|
"admin_roles": "[\n \"chair\"\n]",
|
||||||
|
"agenda_filter_type": "none",
|
||||||
"agenda_type": "ad",
|
"agenda_type": "ad",
|
||||||
"create_wiki": false,
|
"create_wiki": false,
|
||||||
"custom_group_roles": true,
|
"custom_group_roles": true,
|
||||||
|
@ -2920,6 +2928,7 @@
|
||||||
"about_page": "ietf.group.views.group_about",
|
"about_page": "ietf.group.views.group_about",
|
||||||
"acts_like_wg": false,
|
"acts_like_wg": false,
|
||||||
"admin_roles": "[\n \"chair\",\n \"lead\"\n]",
|
"admin_roles": "[\n \"chair\",\n \"lead\"\n]",
|
||||||
|
"agenda_filter_type": "heading",
|
||||||
"agenda_type": "ietf",
|
"agenda_type": "ietf",
|
||||||
"create_wiki": false,
|
"create_wiki": false,
|
||||||
"custom_group_roles": true,
|
"custom_group_roles": true,
|
||||||
|
@ -2957,6 +2966,7 @@
|
||||||
"about_page": "ietf.group.views.group_about",
|
"about_page": "ietf.group.views.group_about",
|
||||||
"acts_like_wg": false,
|
"acts_like_wg": false,
|
||||||
"admin_roles": "[\n \"chair\"\n]",
|
"admin_roles": "[\n \"chair\"\n]",
|
||||||
|
"agenda_filter_type": "none",
|
||||||
"agenda_type": null,
|
"agenda_type": null,
|
||||||
"create_wiki": false,
|
"create_wiki": false,
|
||||||
"custom_group_roles": true,
|
"custom_group_roles": true,
|
||||||
|
@ -2994,6 +3004,7 @@
|
||||||
"about_page": "ietf.group.views.group_about",
|
"about_page": "ietf.group.views.group_about",
|
||||||
"acts_like_wg": false,
|
"acts_like_wg": false,
|
||||||
"admin_roles": "[\n \"chair\"\n]",
|
"admin_roles": "[\n \"chair\"\n]",
|
||||||
|
"agenda_filter_type": "heading",
|
||||||
"agenda_type": "ietf",
|
"agenda_type": "ietf",
|
||||||
"create_wiki": false,
|
"create_wiki": false,
|
||||||
"custom_group_roles": true,
|
"custom_group_roles": true,
|
||||||
|
@ -3031,6 +3042,7 @@
|
||||||
"about_page": "ietf.group.views.group_about",
|
"about_page": "ietf.group.views.group_about",
|
||||||
"acts_like_wg": false,
|
"acts_like_wg": false,
|
||||||
"admin_roles": "[\n \"chair\",\n \"lead\"\n]",
|
"admin_roles": "[\n \"chair\",\n \"lead\"\n]",
|
||||||
|
"agenda_filter_type": "none",
|
||||||
"agenda_type": "ad",
|
"agenda_type": "ad",
|
||||||
"create_wiki": false,
|
"create_wiki": false,
|
||||||
"custom_group_roles": true,
|
"custom_group_roles": true,
|
||||||
|
@ -3066,6 +3078,7 @@
|
||||||
"about_page": "ietf.group.views.group_about",
|
"about_page": "ietf.group.views.group_about",
|
||||||
"acts_like_wg": false,
|
"acts_like_wg": false,
|
||||||
"admin_roles": "[\n \"chair\"\n]",
|
"admin_roles": "[\n \"chair\"\n]",
|
||||||
|
"agenda_filter_type": "none",
|
||||||
"agenda_type": null,
|
"agenda_type": null,
|
||||||
"create_wiki": false,
|
"create_wiki": false,
|
||||||
"custom_group_roles": true,
|
"custom_group_roles": true,
|
||||||
|
@ -3103,6 +3116,7 @@
|
||||||
"about_page": "ietf.group.views.group_about",
|
"about_page": "ietf.group.views.group_about",
|
||||||
"acts_like_wg": false,
|
"acts_like_wg": false,
|
||||||
"admin_roles": "[\n \"chair\",\n \"advisor\"\n]",
|
"admin_roles": "[\n \"chair\",\n \"advisor\"\n]",
|
||||||
|
"agenda_filter_type": "none",
|
||||||
"agenda_type": "side",
|
"agenda_type": "side",
|
||||||
"create_wiki": true,
|
"create_wiki": true,
|
||||||
"custom_group_roles": true,
|
"custom_group_roles": true,
|
||||||
|
@ -3140,6 +3154,7 @@
|
||||||
"about_page": "ietf.group.views.group_about",
|
"about_page": "ietf.group.views.group_about",
|
||||||
"acts_like_wg": false,
|
"acts_like_wg": false,
|
||||||
"admin_roles": "[\n \"lead\"\n]",
|
"admin_roles": "[\n \"lead\"\n]",
|
||||||
|
"agenda_filter_type": "normal",
|
||||||
"agenda_type": "ad",
|
"agenda_type": "ad",
|
||||||
"create_wiki": false,
|
"create_wiki": false,
|
||||||
"custom_group_roles": true,
|
"custom_group_roles": true,
|
||||||
|
@ -3177,6 +3192,7 @@
|
||||||
"about_page": "ietf.group.views.group_about",
|
"about_page": "ietf.group.views.group_about",
|
||||||
"acts_like_wg": true,
|
"acts_like_wg": true,
|
||||||
"admin_roles": "[\n \"chair\"\n]",
|
"admin_roles": "[\n \"chair\"\n]",
|
||||||
|
"agenda_filter_type": "normal",
|
||||||
"agenda_type": "ietf",
|
"agenda_type": "ietf",
|
||||||
"create_wiki": true,
|
"create_wiki": true,
|
||||||
"custom_group_roles": true,
|
"custom_group_roles": true,
|
||||||
|
@ -3214,6 +3230,7 @@
|
||||||
"about_page": "ietf.group.views.group_about",
|
"about_page": "ietf.group.views.group_about",
|
||||||
"acts_like_wg": false,
|
"acts_like_wg": false,
|
||||||
"admin_roles": "[\n \"chair\",\n \"secr\"\n]",
|
"admin_roles": "[\n \"chair\",\n \"secr\"\n]",
|
||||||
|
"agenda_filter_type": "normal",
|
||||||
"agenda_type": "ietf",
|
"agenda_type": "ietf",
|
||||||
"create_wiki": true,
|
"create_wiki": true,
|
||||||
"custom_group_roles": true,
|
"custom_group_roles": true,
|
||||||
|
@ -3251,6 +3268,7 @@
|
||||||
"about_page": "ietf.group.views.group_about",
|
"about_page": "ietf.group.views.group_about",
|
||||||
"acts_like_wg": false,
|
"acts_like_wg": false,
|
||||||
"admin_roles": "[\n \"chair\"\n]",
|
"admin_roles": "[\n \"chair\"\n]",
|
||||||
|
"agenda_filter_type": "none",
|
||||||
"agenda_type": "side",
|
"agenda_type": "side",
|
||||||
"create_wiki": false,
|
"create_wiki": false,
|
||||||
"custom_group_roles": true,
|
"custom_group_roles": true,
|
||||||
|
@ -3286,6 +3304,7 @@
|
||||||
"about_page": "ietf.group.views.group_about",
|
"about_page": "ietf.group.views.group_about",
|
||||||
"acts_like_wg": true,
|
"acts_like_wg": true,
|
||||||
"admin_roles": "[\n \"chair\"\n]",
|
"admin_roles": "[\n \"chair\"\n]",
|
||||||
|
"agenda_filter_type": "normal",
|
||||||
"agenda_type": "ietf",
|
"agenda_type": "ietf",
|
||||||
"create_wiki": true,
|
"create_wiki": true,
|
||||||
"custom_group_roles": false,
|
"custom_group_roles": false,
|
||||||
|
@ -3323,6 +3342,7 @@
|
||||||
"about_page": "ietf.group.views.group_about",
|
"about_page": "ietf.group.views.group_about",
|
||||||
"acts_like_wg": false,
|
"acts_like_wg": false,
|
||||||
"admin_roles": "[\n \"chair\"\n]",
|
"admin_roles": "[\n \"chair\"\n]",
|
||||||
|
"agenda_filter_type": "none",
|
||||||
"agenda_type": null,
|
"agenda_type": null,
|
||||||
"create_wiki": false,
|
"create_wiki": false,
|
||||||
"custom_group_roles": true,
|
"custom_group_roles": true,
|
||||||
|
@ -3361,6 +3381,7 @@
|
||||||
"about_page": "ietf.group.views.group_about",
|
"about_page": "ietf.group.views.group_about",
|
||||||
"acts_like_wg": false,
|
"acts_like_wg": false,
|
||||||
"admin_roles": "[\n \"chair\"\n]",
|
"admin_roles": "[\n \"chair\"\n]",
|
||||||
|
"agenda_filter_type": "special",
|
||||||
"agenda_type": "ietf",
|
"agenda_type": "ietf",
|
||||||
"create_wiki": true,
|
"create_wiki": true,
|
||||||
"custom_group_roles": true,
|
"custom_group_roles": true,
|
||||||
|
@ -3398,6 +3419,7 @@
|
||||||
"about_page": "ietf.group.views.group_about",
|
"about_page": "ietf.group.views.group_about",
|
||||||
"acts_like_wg": true,
|
"acts_like_wg": true,
|
||||||
"admin_roles": "[\n \"chair\"\n]",
|
"admin_roles": "[\n \"chair\"\n]",
|
||||||
|
"agenda_filter_type": "normal",
|
||||||
"agenda_type": "ietf",
|
"agenda_type": "ietf",
|
||||||
"create_wiki": true,
|
"create_wiki": true,
|
||||||
"custom_group_roles": false,
|
"custom_group_roles": false,
|
||||||
|
@ -6095,6 +6117,46 @@
|
||||||
"model": "meeting.businessconstraint",
|
"model": "meeting.businessconstraint",
|
||||||
"pk": "sessions_out_of_order"
|
"pk": "sessions_out_of_order"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"desc": "Column heading button",
|
||||||
|
"name": "Heading",
|
||||||
|
"order": 2,
|
||||||
|
"used": true
|
||||||
|
},
|
||||||
|
"model": "name.agendafiltertypename",
|
||||||
|
"pk": "heading"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"desc": "Not used except for a timeslot-type column (e.g., officehours)",
|
||||||
|
"name": "None",
|
||||||
|
"order": 0,
|
||||||
|
"used": true
|
||||||
|
},
|
||||||
|
"model": "name.agendafiltertypename",
|
||||||
|
"pk": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"desc": "Non-heading filter button",
|
||||||
|
"name": "Normal",
|
||||||
|
"order": 1,
|
||||||
|
"used": true
|
||||||
|
},
|
||||||
|
"model": "name.agendafiltertypename",
|
||||||
|
"pk": "normal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"desc": "Button in the catch-all column",
|
||||||
|
"name": "Special",
|
||||||
|
"order": 3,
|
||||||
|
"used": true
|
||||||
|
},
|
||||||
|
"model": "name.agendafiltertypename",
|
||||||
|
"pk": "special"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fields": {
|
"fields": {
|
||||||
"desc": "",
|
"desc": "",
|
||||||
|
@ -13129,6 +13191,17 @@
|
||||||
"model": "name.timeslottypename",
|
"model": "name.timeslottypename",
|
||||||
"pk": "offagenda"
|
"pk": "offagenda"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"desc": "Office hours timeslot",
|
||||||
|
"name": "Office Hours",
|
||||||
|
"order": 0,
|
||||||
|
"private": false,
|
||||||
|
"used": true
|
||||||
|
},
|
||||||
|
"model": "name.timeslottypename",
|
||||||
|
"pk": "officehours"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fields": {
|
"fields": {
|
||||||
"desc": "",
|
"desc": "",
|
||||||
|
|
27
ietf/name/migrations/0032_agendafiltertypename.py
Normal file
27
ietf/name/migrations/0032_agendafiltertypename.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# Generated by Django 2.2.19 on 2021-04-02 12:03
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('name', '0031_add_procmaterials'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='AgendaFilterTypeName',
|
||||||
|
fields=[
|
||||||
|
('slug', models.CharField(max_length=32, primary_key=True, serialize=False)),
|
||||||
|
('name', models.CharField(max_length=255)),
|
||||||
|
('desc', models.TextField(blank=True)),
|
||||||
|
('used', models.BooleanField(default=True)),
|
||||||
|
('order', models.IntegerField(default=0)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['order', 'name'],
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
33
ietf/name/migrations/0033_populate_agendafiltertypename.py
Normal file
33
ietf/name/migrations/0033_populate_agendafiltertypename.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# Generated by Django 2.2.20 on 2021-04-20 13:56
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def forward(apps, schema_editor):
|
||||||
|
AgendaFilterTypeName = apps.get_model('name', 'AgendaFilterTypeName')
|
||||||
|
names = (
|
||||||
|
('none', 'None', 'Not used except for a timeslot-type column (e.g., officehours)'),
|
||||||
|
('normal', 'Normal', 'Non-heading filter button'),
|
||||||
|
('heading', 'Heading', 'Column heading button'),
|
||||||
|
('special', 'Special', 'Button in the catch-all column'),
|
||||||
|
)
|
||||||
|
for order, (slug, name, desc) in enumerate(names):
|
||||||
|
AgendaFilterTypeName.objects.get_or_create(
|
||||||
|
slug=slug,
|
||||||
|
defaults=dict(name=name, desc=desc, order=order, used=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def reverse(apps, schema_editor):
|
||||||
|
pass # nothing to do, model about to be destroyed anyway
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('name', '0032_agendafiltertypename'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(forward, reverse),
|
||||||
|
]
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Generated by Django 2.2.19 on 2021-03-29 08:28
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def forward(apps, schema_editor):
|
||||||
|
TimeSlotTypeName = apps.get_model('name', 'TimeSlotTypeName')
|
||||||
|
|
||||||
|
TimeSlotTypeName.objects.get_or_create(
|
||||||
|
slug='officehours',
|
||||||
|
defaults=dict(
|
||||||
|
name='Office Hours',
|
||||||
|
desc='Office hours timeslot',
|
||||||
|
used=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def reverse(apps, schema_editor):
|
||||||
|
pass # don't remove the name when migrating
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('name', '0033_populate_agendafiltertypename'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(forward, reverse),
|
||||||
|
]
|
32
ietf/name/migrations/0035_sessionpurposename.py
Normal file
32
ietf/name/migrations/0035_sessionpurposename.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# Copyright The IETF Trust 2021 All Rights Reserved
|
||||||
|
|
||||||
|
# Generated by Django 2.2.24 on 2021-09-16 09:42
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import ietf.name.models
|
||||||
|
import jsonfield
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('name', '0034_add_officehours_timeslottypename'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SessionPurposeName',
|
||||||
|
fields=[
|
||||||
|
('slug', models.CharField(max_length=32, primary_key=True, serialize=False)),
|
||||||
|
('name', models.CharField(max_length=255)),
|
||||||
|
('desc', models.TextField(blank=True)),
|
||||||
|
('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')])),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['order', 'name'],
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
48
ietf/name/migrations/0036_populate_sessionpurposename.py
Normal file
48
ietf/name/migrations/0036_populate_sessionpurposename.py
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
# Copyright The IETF Trust 2021 All Rights Reserved
|
||||||
|
|
||||||
|
# Generated by Django 2.2.24 on 2021-09-16 09:42
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
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((
|
||||||
|
('session', 'Session', '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', ['other']),
|
||||||
|
('presentation', 'Presentation', 'Presentation session', ['other', 'regular'])
|
||||||
|
)):
|
||||||
|
# verify that we're not about to use an invalid purpose
|
||||||
|
for ts_type in tstypes:
|
||||||
|
TimeSlotTypeName.objects.get(pk=ts_type) # throws an exception unless exists
|
||||||
|
|
||||||
|
SessionPurposeName.objects.create(
|
||||||
|
slug=slug,
|
||||||
|
name=name,
|
||||||
|
desc=desc,
|
||||||
|
used=True,
|
||||||
|
order=order,
|
||||||
|
timeslot_types = tstypes
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def reverse(apps, schema_editor):
|
||||||
|
SessionPurposeName = apps.get_model('name', 'SessionPurposeName')
|
||||||
|
SessionPurposeName.objects.all().delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('name', '0035_sessionpurposename'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(forward, reverse)
|
||||||
|
]
|
|
@ -1,10 +1,13 @@
|
||||||
# Copyright The IETF Trust 2010-2020, All Rights Reserved
|
# Copyright The IETF Trust 2010-2020, All Rights Reserved
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import jsonfield
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
from ietf.utils.models import ForeignKey
|
from ietf.utils.models import ForeignKey
|
||||||
|
from ietf.utils.validators import JSONForeignKeyListValidator
|
||||||
|
|
||||||
|
|
||||||
class NameModel(models.Model):
|
class NameModel(models.Model):
|
||||||
slug = models.CharField(max_length=32, primary_key=True)
|
slug = models.CharField(max_length=32, primary_key=True)
|
||||||
|
@ -64,8 +67,17 @@ class ProceedingsMaterialTypeName(NameModel):
|
||||||
"""social_event, host_speaker_series, supporters, wiki, additional_information"""
|
"""social_event, host_speaker_series, supporters, wiki, additional_information"""
|
||||||
class AgendaTypeName(NameModel):
|
class AgendaTypeName(NameModel):
|
||||||
"""ietf, ad, side, workshop, ..."""
|
"""ietf, ad, side, workshop, ..."""
|
||||||
|
class AgendaFilterTypeName(NameModel):
|
||||||
|
"""none, normal, heading, special"""
|
||||||
class SessionStatusName(NameModel):
|
class SessionStatusName(NameModel):
|
||||||
"""Waiting for Approval, Approved, Waiting for Scheduling, Scheduled, Cancelled, Disapproved"""
|
"""Waiting for Approval, Approved, Waiting for Scheduling, Scheduled, Cancelled, Disapproved"""
|
||||||
|
class SessionPurposeName(NameModel):
|
||||||
|
"""Regular, Tutorial, Office Hours, Coding, Social, Admin"""
|
||||||
|
timeslot_types = jsonfield.JSONField(
|
||||||
|
max_length=256, blank=False, default=[],
|
||||||
|
help_text='Allowed TimeSlotTypeNames',
|
||||||
|
validators=[JSONForeignKeyListValidator('name.TimeSlotTypeName')],
|
||||||
|
)
|
||||||
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")
|
private = models.BooleanField(default=False, help_text="Whether sessions of this type should be kept off the public agenda")
|
||||||
|
|
|
@ -8,7 +8,7 @@ from tastypie.cache import SimpleCache
|
||||||
|
|
||||||
from ietf import api
|
from ietf import api
|
||||||
|
|
||||||
from ietf.name.models import ( AgendaTypeName, BallotPositionName, ConstraintName,
|
from ietf.name.models import ( AgendaFilterTypeName, AgendaTypeName, BallotPositionName, ConstraintName,
|
||||||
ContinentName, CountryName, DBTemplateTypeName, DocRelationshipName, DocReminderTypeName,
|
ContinentName, CountryName, DBTemplateTypeName, DocRelationshipName, DocReminderTypeName,
|
||||||
DocTagName, DocTypeName, DocUrlTagName, DraftSubmissionStateName, FeedbackTypeName,
|
DocTagName, DocTypeName, DocUrlTagName, DraftSubmissionStateName, FeedbackTypeName,
|
||||||
FormalLanguageName, GroupMilestoneStateName, GroupStateName, GroupTypeName,
|
FormalLanguageName, GroupMilestoneStateName, GroupStateName, GroupTypeName,
|
||||||
|
@ -684,3 +684,20 @@ class ProceedingsMaterialTypeNameResource(ModelResource):
|
||||||
"order": ALL,
|
"order": ALL,
|
||||||
}
|
}
|
||||||
api.name.register(ProceedingsMaterialTypeNameResource())
|
api.name.register(ProceedingsMaterialTypeNameResource())
|
||||||
|
|
||||||
|
|
||||||
|
class AgendaFilterTypeNameResource(ModelResource):
|
||||||
|
class Meta:
|
||||||
|
queryset = AgendaFilterTypeName.objects.all()
|
||||||
|
serializer = api.Serializer()
|
||||||
|
cache = SimpleCache()
|
||||||
|
#resource_name = 'agendafiltertypename'
|
||||||
|
ordering = ['slug', ]
|
||||||
|
filtering = {
|
||||||
|
"slug": ALL,
|
||||||
|
"name": ALL,
|
||||||
|
"desc": ALL,
|
||||||
|
"used": ALL,
|
||||||
|
"order": ALL,
|
||||||
|
}
|
||||||
|
api.name.register(AgendaFilterTypeNameResource())
|
||||||
|
|
|
@ -8,8 +8,9 @@ from django.db.models import Q
|
||||||
import debug # pyflakes:ignore
|
import debug # pyflakes:ignore
|
||||||
|
|
||||||
from ietf.group.models import Group
|
from ietf.group.models import Group
|
||||||
|
from ietf.meeting.fields import SessionPurposeAndTypeField
|
||||||
from ietf.meeting.models import Meeting, Room, TimeSlot, Session, SchedTimeSessAssignment
|
from ietf.meeting.models import Meeting, Room, TimeSlot, Session, SchedTimeSessAssignment
|
||||||
from ietf.name.models import TimeSlotTypeName
|
from ietf.name.models import TimeSlotTypeName, SessionPurposeName
|
||||||
import ietf.utils.fields
|
import ietf.utils.fields
|
||||||
|
|
||||||
|
|
||||||
|
@ -130,6 +131,13 @@ class MeetingRoomForm(forms.ModelForm):
|
||||||
model = Room
|
model = Room
|
||||||
exclude = ['resources']
|
exclude = ['resources']
|
||||||
|
|
||||||
|
class MeetingRoomOptionsForm(forms.Form):
|
||||||
|
copy_timeslots = forms.BooleanField(
|
||||||
|
required=False,
|
||||||
|
initial=False,
|
||||||
|
label='Duplicate timeslots from previous meeting for new rooms?',
|
||||||
|
)
|
||||||
|
|
||||||
class TimeSlotForm(forms.Form):
|
class TimeSlotForm(forms.Form):
|
||||||
day = forms.ChoiceField()
|
day = forms.ChoiceField()
|
||||||
time = forms.TimeField()
|
time = forms.TimeField()
|
||||||
|
@ -163,7 +171,10 @@ class TimeSlotForm(forms.Form):
|
||||||
|
|
||||||
class MiscSessionForm(TimeSlotForm):
|
class MiscSessionForm(TimeSlotForm):
|
||||||
short = forms.CharField(max_length=32,label='Short Name',help_text='Enter an abbreviated session name (used for material file names)',required=False)
|
short = forms.CharField(max_length=32,label='Short Name',help_text='Enter an abbreviated session name (used for material file names)',required=False)
|
||||||
type = forms.ModelChoiceField(queryset=TimeSlotTypeName.objects.filter(used=True).exclude(slug__in=('regular',)),empty_label=None)
|
purpose = SessionPurposeAndTypeField(
|
||||||
|
purpose_queryset=SessionPurposeName.objects.none(),
|
||||||
|
type_queryset=TimeSlotTypeName.objects.none(),
|
||||||
|
)
|
||||||
group = forms.ModelChoiceField(
|
group = forms.ModelChoiceField(
|
||||||
queryset=Group.objects.filter(
|
queryset=Group.objects.filter(
|
||||||
Q(type__in=['ietf','team','area'],state='active')|
|
Q(type__in=['ietf','team','area'],state='active')|
|
||||||
|
@ -187,8 +198,13 @@ class MiscSessionForm(TimeSlotForm):
|
||||||
self.meeting = kwargs.pop('meeting')
|
self.meeting = kwargs.pop('meeting')
|
||||||
if 'session' in kwargs:
|
if 'session' in kwargs:
|
||||||
self.session = kwargs.pop('session')
|
self.session = kwargs.pop('session')
|
||||||
|
initial = kwargs.setdefault('initial', dict())
|
||||||
|
initial['purpose'] = (initial.pop('purpose', ''), initial.pop('type', ''))
|
||||||
super(MiscSessionForm, self).__init__(*args,**kwargs)
|
super(MiscSessionForm, self).__init__(*args,**kwargs)
|
||||||
self.fields['location'].queryset = Room.objects.filter(meeting=self.meeting)
|
self.fields['location'].queryset = Room.objects.filter(meeting=self.meeting)
|
||||||
|
self.fields['purpose'].purpose_queryset = SessionPurposeName.objects.filter(
|
||||||
|
used=True).exclude(slug='session').order_by('name')
|
||||||
|
self.fields['purpose'].type_queryset = TimeSlotTypeName.objects.filter(used=True)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super(MiscSessionForm, self).clean()
|
super(MiscSessionForm, self).clean()
|
||||||
|
@ -196,13 +212,15 @@ class MiscSessionForm(TimeSlotForm):
|
||||||
return
|
return
|
||||||
cleaned_data = self.cleaned_data
|
cleaned_data = self.cleaned_data
|
||||||
group = cleaned_data['group']
|
group = cleaned_data['group']
|
||||||
type = cleaned_data['type']
|
type = cleaned_data['purpose'].type
|
||||||
short = cleaned_data['short']
|
short = cleaned_data['short']
|
||||||
if type.slug in ('other','plenary','lead') and not group:
|
if type.slug in ('other','plenary','lead') and not group:
|
||||||
raise forms.ValidationError('ERROR: a group selection is required')
|
raise forms.ValidationError('ERROR: a group selection is required')
|
||||||
if type.slug in ('other','plenary','lead') and not short:
|
if type.slug in ('other','plenary','lead') and not short:
|
||||||
raise forms.ValidationError('ERROR: a short name is required')
|
raise forms.ValidationError('ERROR: a short name is required')
|
||||||
|
|
||||||
|
cleaned_data['purpose'] = cleaned_data['purpose'].purpose
|
||||||
|
cleaned_data['type'] = type
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
def clean_group(self):
|
def clean_group(self):
|
||||||
|
|
|
@ -26,7 +26,7 @@ from ietf.group.models import Group, GroupEvent
|
||||||
from ietf.secr.meetings.blue_sheets import create_blue_sheets
|
from ietf.secr.meetings.blue_sheets import create_blue_sheets
|
||||||
from ietf.secr.meetings.forms import ( BaseMeetingRoomFormSet, MeetingModelForm, MeetingSelectForm,
|
from ietf.secr.meetings.forms import ( BaseMeetingRoomFormSet, MeetingModelForm, MeetingSelectForm,
|
||||||
MeetingRoomForm, MiscSessionForm, TimeSlotForm, RegularSessionEditForm,
|
MeetingRoomForm, MiscSessionForm, TimeSlotForm, RegularSessionEditForm,
|
||||||
UploadBlueSheetForm )
|
UploadBlueSheetForm, MeetingRoomOptionsForm )
|
||||||
from ietf.secr.proceedings.utils import handle_upload_file
|
from ietf.secr.proceedings.utils import handle_upload_file
|
||||||
from ietf.secr.sreq.views import get_initial_session
|
from ietf.secr.sreq.views import get_initial_session
|
||||||
from ietf.secr.utils.meeting import get_session, get_timeslot
|
from ietf.secr.utils.meeting import get_session, get_timeslot
|
||||||
|
@ -406,9 +406,11 @@ def misc_sessions(request, meeting_id, schedule_name):
|
||||||
name = form.cleaned_data['name']
|
name = form.cleaned_data['name']
|
||||||
short = form.cleaned_data['short']
|
short = form.cleaned_data['short']
|
||||||
type = form.cleaned_data['type']
|
type = form.cleaned_data['type']
|
||||||
|
purpose = form.cleaned_data['purpose']
|
||||||
group = form.cleaned_data['group']
|
group = form.cleaned_data['group']
|
||||||
duration = form.cleaned_data['duration']
|
duration = form.cleaned_data['duration']
|
||||||
location = form.cleaned_data['location']
|
location = form.cleaned_data['location']
|
||||||
|
remote_instructions = form.cleaned_data['remote_instructions']
|
||||||
|
|
||||||
# create TimeSlot object
|
# create TimeSlot object
|
||||||
timeslot = TimeSlot.objects.create(type=type,
|
timeslot = TimeSlot.objects.create(type=type,
|
||||||
|
@ -427,7 +429,9 @@ def misc_sessions(request, meeting_id, schedule_name):
|
||||||
name=name,
|
name=name,
|
||||||
short=short,
|
short=short,
|
||||||
group=group,
|
group=group,
|
||||||
type=type)
|
type=type,
|
||||||
|
purpose=purpose,
|
||||||
|
remote_instructions=remote_instructions)
|
||||||
|
|
||||||
SchedulingEvent.objects.create(
|
SchedulingEvent.objects.create(
|
||||||
session=session,
|
session=session,
|
||||||
|
@ -537,6 +541,7 @@ def misc_session_edit(request, meeting_id, schedule_name, slot_id):
|
||||||
name = form.cleaned_data['name']
|
name = form.cleaned_data['name']
|
||||||
short = form.cleaned_data['short']
|
short = form.cleaned_data['short']
|
||||||
duration = form.cleaned_data['duration']
|
duration = form.cleaned_data['duration']
|
||||||
|
session_purpose = form.cleaned_data['purpose']
|
||||||
slot_type = form.cleaned_data['type']
|
slot_type = form.cleaned_data['type']
|
||||||
show_location = form.cleaned_data['show_location']
|
show_location = form.cleaned_data['show_location']
|
||||||
remote_instructions = form.cleaned_data['remote_instructions']
|
remote_instructions = form.cleaned_data['remote_instructions']
|
||||||
|
@ -553,6 +558,8 @@ def misc_session_edit(request, meeting_id, schedule_name, slot_id):
|
||||||
session.name = name
|
session.name = name
|
||||||
session.short = short
|
session.short = short
|
||||||
session.remote_instructions = remote_instructions
|
session.remote_instructions = remote_instructions
|
||||||
|
session.purpose = session_purpose
|
||||||
|
session.type = slot_type
|
||||||
session.save()
|
session.save()
|
||||||
|
|
||||||
messages.success(request, 'Location saved')
|
messages.success(request, 'Location saved')
|
||||||
|
@ -570,7 +577,8 @@ def misc_session_edit(request, meeting_id, schedule_name, slot_id):
|
||||||
'time':slot.time.strftime('%H:%M'),
|
'time':slot.time.strftime('%H:%M'),
|
||||||
'duration':duration_string(slot.duration),
|
'duration':duration_string(slot.duration),
|
||||||
'show_location':slot.show_location,
|
'show_location':slot.show_location,
|
||||||
'type':slot.type,
|
'purpose': session.purpose,
|
||||||
|
'type': session.type,
|
||||||
'remote_instructions': session.remote_instructions,
|
'remote_instructions': session.remote_instructions,
|
||||||
}
|
}
|
||||||
form = MiscSessionForm(initial=initial, meeting=meeting, session=session)
|
form = MiscSessionForm(initial=initial, meeting=meeting, session=session)
|
||||||
|
@ -637,29 +645,34 @@ def rooms(request, meeting_id, schedule_name):
|
||||||
return redirect('ietf.secr.meetings.views.main', meeting_id=meeting_id,schedule_name=schedule_name)
|
return redirect('ietf.secr.meetings.views.main', meeting_id=meeting_id,schedule_name=schedule_name)
|
||||||
|
|
||||||
formset = RoomFormset(request.POST, instance=meeting, prefix='room')
|
formset = RoomFormset(request.POST, instance=meeting, prefix='room')
|
||||||
if formset.is_valid():
|
options_form = MeetingRoomOptionsForm(request.POST)
|
||||||
|
if formset.is_valid() and options_form.is_valid():
|
||||||
formset.save()
|
formset.save()
|
||||||
|
|
||||||
# if we are creating rooms for the first time create full set of timeslots
|
# only create timeslots on request
|
||||||
if first_time:
|
if options_form.cleaned_data['copy_timeslots']:
|
||||||
build_timeslots(meeting)
|
# if we are creating rooms for the first time create full set of timeslots
|
||||||
|
if first_time:
|
||||||
|
build_timeslots(meeting)
|
||||||
|
|
||||||
# otherwise if we're modifying rooms
|
# otherwise if we're modifying rooms
|
||||||
else:
|
else:
|
||||||
# add timeslots for new rooms, deleting rooms automatically deletes timeslots
|
# add timeslots for new rooms, deleting rooms automatically deletes timeslots
|
||||||
for form in formset.forms[formset.initial_form_count():]:
|
for form in formset.forms[formset.initial_form_count():]:
|
||||||
if form.instance.pk:
|
if form.instance.pk:
|
||||||
build_timeslots(meeting,room=form.instance)
|
build_timeslots(meeting,room=form.instance)
|
||||||
|
|
||||||
messages.success(request, 'Meeting Rooms changed successfully')
|
messages.success(request, 'Meeting Rooms changed successfully')
|
||||||
return redirect('ietf.secr.meetings.views.rooms', meeting_id=meeting_id, schedule_name=schedule_name)
|
return redirect('ietf.secr.meetings.views.rooms', meeting_id=meeting_id, schedule_name=schedule_name)
|
||||||
else:
|
else:
|
||||||
formset = RoomFormset(instance=meeting, prefix='room')
|
formset = RoomFormset(instance=meeting, prefix='room')
|
||||||
|
options_form = MeetingRoomOptionsForm()
|
||||||
|
|
||||||
return render(request, 'meetings/rooms.html', {
|
return render(request, 'meetings/rooms.html', {
|
||||||
'meeting': meeting,
|
'meeting': meeting,
|
||||||
'schedule': schedule,
|
'schedule': schedule,
|
||||||
'formset': formset,
|
'formset': formset,
|
||||||
|
'options_form': options_form,
|
||||||
'selected': 'rooms'}
|
'selected': 'rooms'}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import debug # pyflakes:ignore
|
||||||
|
|
||||||
from ietf.name.models import TimerangeName, ConstraintName
|
from ietf.name.models import TimerangeName, ConstraintName
|
||||||
from ietf.group.models import Group
|
from ietf.group.models import Group
|
||||||
|
from ietf.meeting.forms import SessionDetailsFormSet
|
||||||
from ietf.meeting.models import ResourceAssociation, Constraint
|
from ietf.meeting.models import ResourceAssociation, Constraint
|
||||||
from ietf.person.fields import SearchablePersonsField
|
from ietf.person.fields import SearchablePersonsField
|
||||||
from ietf.utils.html import clean_text_field
|
from ietf.utils.html import clean_text_field
|
||||||
|
@ -61,13 +62,11 @@ class NameModelMultipleChoiceField(forms.ModelMultipleChoiceField):
|
||||||
def label_from_instance(self, name):
|
def label_from_instance(self, name):
|
||||||
return name.desc
|
return name.desc
|
||||||
|
|
||||||
|
|
||||||
class SessionForm(forms.Form):
|
class SessionForm(forms.Form):
|
||||||
num_session = forms.ChoiceField(choices=NUM_SESSION_CHOICES)
|
num_session = forms.ChoiceField(choices=NUM_SESSION_CHOICES)
|
||||||
length_session1 = forms.ChoiceField(choices=LENGTH_SESSION_CHOICES)
|
# session fields are added in __init__()
|
||||||
length_session2 = forms.ChoiceField(choices=LENGTH_SESSION_CHOICES,required=False)
|
|
||||||
session_time_relation = forms.ChoiceField(choices=SESSION_TIME_RELATION_CHOICES, required=False)
|
session_time_relation = forms.ChoiceField(choices=SESSION_TIME_RELATION_CHOICES, required=False)
|
||||||
length_session3 = forms.ChoiceField(choices=LENGTH_SESSION_CHOICES,required=False)
|
|
||||||
attendees = forms.IntegerField()
|
attendees = forms.IntegerField()
|
||||||
# FIXME: it would cleaner to have these be
|
# FIXME: it would cleaner to have these be
|
||||||
# ModelMultipleChoiceField, and just customize the widgetry, that
|
# ModelMultipleChoiceField, and just customize the widgetry, that
|
||||||
|
@ -84,19 +83,16 @@ class SessionForm(forms.Form):
|
||||||
queryset=TimerangeName.objects.all())
|
queryset=TimerangeName.objects.all())
|
||||||
adjacent_with_wg = forms.ChoiceField(required=False)
|
adjacent_with_wg = forms.ChoiceField(required=False)
|
||||||
|
|
||||||
def __init__(self, group, meeting, *args, **kwargs):
|
def __init__(self, group, meeting, data=None, *args, **kwargs):
|
||||||
if 'hidden' in kwargs:
|
if 'hidden' in kwargs:
|
||||||
self.hidden = kwargs.pop('hidden')
|
self.hidden = kwargs.pop('hidden')
|
||||||
else:
|
else:
|
||||||
self.hidden = False
|
self.hidden = False
|
||||||
|
|
||||||
self.group = group
|
self.group = group
|
||||||
|
self.session_forms = SessionDetailsFormSet(group=self.group, meeting=meeting, data=data)
|
||||||
|
super(SessionForm, self).__init__(data=data, *args, **kwargs)
|
||||||
|
|
||||||
super(SessionForm, self).__init__(*args, **kwargs)
|
|
||||||
self.fields['num_session'].widget.attrs['onChange'] = "ietf_sessions.stat_ls(this.selectedIndex);"
|
|
||||||
self.fields['length_session1'].widget.attrs['onClick'] = "if (ietf_sessions.check_num_session(1)) this.disabled=true;"
|
|
||||||
self.fields['length_session2'].widget.attrs['onClick'] = "if (ietf_sessions.check_num_session(2)) this.disabled=true;"
|
|
||||||
self.fields['length_session3'].widget.attrs['onClick'] = "if (ietf_sessions.check_third_session()) { this.disabled=true;}"
|
|
||||||
self.fields['comments'].widget = forms.Textarea(attrs={'rows':'3','cols':'65'})
|
self.fields['comments'].widget = forms.Textarea(attrs={'rows':'3','cols':'65'})
|
||||||
|
|
||||||
other_groups = list(allowed_conflicting_groups().exclude(pk=group.pk).values_list('acronym', 'acronym').order_by('acronym'))
|
other_groups = list(allowed_conflicting_groups().exclude(pk=group.pk).values_list('acronym', 'acronym').order_by('acronym'))
|
||||||
|
@ -148,15 +144,8 @@ class SessionForm(forms.Form):
|
||||||
)
|
)
|
||||||
|
|
||||||
self.fields['joint_with_groups_selector'].widget.attrs['onChange'] = "document.form_post.joint_with_groups.value=document.form_post.joint_with_groups.value + ' ' + this.options[this.selectedIndex].value; return 1;"
|
self.fields['joint_with_groups_selector'].widget.attrs['onChange'] = "document.form_post.joint_with_groups.value=document.form_post.joint_with_groups.value + ' ' + this.options[this.selectedIndex].value; return 1;"
|
||||||
self.fields['third_session'].widget.attrs['onClick'] = "if (document.form_post.num_session.selectedIndex < 2) { alert('Cannot use this field - Number of Session is not set to 2'); return false; } else { if (this.checked==true) { document.form_post.length_session3.disabled=false; } else { document.form_post.length_session3.value=0;document.form_post.length_session3.disabled=true; } }"
|
|
||||||
self.fields["resources"].choices = [(x.pk,x.desc) for x in ResourceAssociation.objects.filter(name__used=True).order_by('name__order') ]
|
self.fields["resources"].choices = [(x.pk,x.desc) for x in ResourceAssociation.objects.filter(name__used=True).order_by('name__order') ]
|
||||||
|
|
||||||
# check third_session checkbox if instance and length_session3
|
|
||||||
# assert False, (self.instance, self.fields['length_session3'].initial)
|
|
||||||
if self.initial and 'length_session3' in self.initial:
|
|
||||||
if self.initial['length_session3'] != '0' and self.initial['length_session3'] != None:
|
|
||||||
self.fields['third_session'].initial = True
|
|
||||||
|
|
||||||
if self.hidden:
|
if self.hidden:
|
||||||
for key in list(self.fields.keys()):
|
for key in list(self.fields.keys()):
|
||||||
self.fields[key].widget = forms.HiddenInput()
|
self.fields[key].widget = forms.HiddenInput()
|
||||||
|
@ -235,8 +224,13 @@ class SessionForm(forms.Form):
|
||||||
def clean_comments(self):
|
def clean_comments(self):
|
||||||
return clean_text_field(self.cleaned_data['comments'])
|
return clean_text_field(self.cleaned_data['comments'])
|
||||||
|
|
||||||
|
def is_valid(self):
|
||||||
|
return super().is_valid() and self.session_forms.is_valid()
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super(SessionForm, self).clean()
|
super(SessionForm, self).clean()
|
||||||
|
self.session_forms.clean()
|
||||||
|
|
||||||
data = self.cleaned_data
|
data = self.cleaned_data
|
||||||
|
|
||||||
# Validate the individual conflict fields
|
# Validate the individual conflict fields
|
||||||
|
@ -254,44 +248,45 @@ class SessionForm(forms.Form):
|
||||||
for error in self._validate_duplicate_conflicts(data):
|
for error in self._validate_duplicate_conflicts(data):
|
||||||
self.add_error(None, error)
|
self.add_error(None, error)
|
||||||
|
|
||||||
# verify session_length and num_session correspond
|
# Verify expected number of session entries are present
|
||||||
|
num_sessions_with_data = len(self.session_forms.forms_to_keep)
|
||||||
|
num_sessions_expected = -1
|
||||||
|
try:
|
||||||
|
num_sessions_expected = int(data.get('num_session', ''))
|
||||||
|
except ValueError:
|
||||||
|
self.add_error('num_session', 'Invalid value for number of sessions')
|
||||||
|
if len(self.session_forms.errors) == 0 and num_sessions_with_data < num_sessions_expected:
|
||||||
|
self.add_error('num_session', 'Must provide data for all sessions')
|
||||||
|
|
||||||
# if default (empty) option is selected, cleaned_data won't include num_session key
|
# if default (empty) option is selected, cleaned_data won't include num_session key
|
||||||
if data.get('num_session','') == '2':
|
if num_sessions_expected != 2 and num_sessions_expected is not None:
|
||||||
if not data['length_session2']:
|
|
||||||
self.add_error('length_session2', forms.ValidationError('You must enter a length for all sessions'))
|
|
||||||
else:
|
|
||||||
if data.get('session_time_relation'):
|
if data.get('session_time_relation'):
|
||||||
self.add_error(
|
self.add_error(
|
||||||
'session_time_relation',
|
'session_time_relation',
|
||||||
forms.ValidationError('Time between sessions can only be used when two sessions are requested.')
|
forms.ValidationError('Time between sessions can only be used when two sessions are requested.')
|
||||||
)
|
)
|
||||||
if data.get('joint_for_session') == '2':
|
|
||||||
|
joint_session = data.get('joint_for_session', '')
|
||||||
|
if joint_session != '':
|
||||||
|
joint_session = int(joint_session)
|
||||||
|
if joint_session > num_sessions_with_data:
|
||||||
self.add_error(
|
self.add_error(
|
||||||
'joint_for_session',
|
'joint_for_session',
|
||||||
forms.ValidationError(
|
forms.ValidationError(
|
||||||
'The second session can not be the joint session, because you have not requested a second session.'
|
f'Session {joint_session} can not be the joint session, the session has not been requested.'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if data.get('third_session', False):
|
|
||||||
if not data.get('length_session3',None):
|
|
||||||
self.add_error('length_session3', forms.ValidationError('You must enter a length for all sessions'))
|
|
||||||
elif data.get('joint_for_session') == '3':
|
|
||||||
self.add_error(
|
|
||||||
'joint_for_session',
|
|
||||||
forms.ValidationError(
|
|
||||||
'The third session can not be the joint session, because you have not requested a third session.'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media(self):
|
||||||
|
# get media for our formset
|
||||||
|
return super().media + self.session_forms.media
|
||||||
|
|
||||||
|
|
||||||
class VirtualSessionForm(SessionForm):
|
class VirtualSessionForm(SessionForm):
|
||||||
'''A SessionForm customized for special virtual meeting requirements'''
|
'''A SessionForm customized for special virtual meeting requirements'''
|
||||||
length_session1 = forms.ChoiceField(choices=VIRTUAL_LENGTH_SESSION_CHOICES)
|
|
||||||
length_session2 = forms.ChoiceField(choices=VIRTUAL_LENGTH_SESSION_CHOICES,required=False)
|
|
||||||
length_session3 = forms.ChoiceField(choices=VIRTUAL_LENGTH_SESSION_CHOICES,required=False)
|
|
||||||
attendees = forms.IntegerField(required=False)
|
attendees = forms.IntegerField(required=False)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -25,15 +25,17 @@ def display_duration(value):
|
||||||
"""
|
"""
|
||||||
Maps a session requested duration from select index to
|
Maps a session requested duration from select index to
|
||||||
label."""
|
label."""
|
||||||
map = {'0':'None',
|
value = int(value)
|
||||||
'1800':'30 Minutes',
|
map = {0: 'None',
|
||||||
'3000':'50 Minutes',
|
1800: '30 Minutes',
|
||||||
'3600':'1 Hour',
|
3600: '1 Hour',
|
||||||
'5400':'1.5 Hours',
|
5400: '1.5 Hours',
|
||||||
'6000':'100 Minutes',
|
7200: '2 Hours',
|
||||||
'7200':'2 Hours',
|
9000: '2.5 Hours'}
|
||||||
'9000':'2.5 Hours'}
|
if value in map:
|
||||||
return map[value]
|
return map[value]
|
||||||
|
else:
|
||||||
|
return "%d Hours %d Minutes %d Seconds"%(value//3600,(value%3600)//60,value%60)
|
||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
def get_published_date(doc):
|
def get_published_date(doc):
|
||||||
|
|
|
@ -62,15 +62,6 @@ def get_initial_session(sessions, prune_conflicts=False):
|
||||||
|
|
||||||
# even if there are three sessions requested, the old form has 2 in this field
|
# even if there are three sessions requested, the old form has 2 in this field
|
||||||
initial['num_session'] = min(sessions.count(), 2)
|
initial['num_session'] = min(sessions.count(), 2)
|
||||||
|
|
||||||
# accessing these foreign key fields throw errors if they are unset so we
|
|
||||||
# need to catch these
|
|
||||||
initial['length_session1'] = str(sessions[0].requested_duration.seconds)
|
|
||||||
try:
|
|
||||||
initial['length_session2'] = str(sessions[1].requested_duration.seconds)
|
|
||||||
initial['length_session3'] = str(sessions[2].requested_duration.seconds)
|
|
||||||
except IndexError:
|
|
||||||
pass
|
|
||||||
initial['attendees'] = sessions[0].attendees
|
initial['attendees'] = sessions[0].attendees
|
||||||
|
|
||||||
def valid_conflict(conflict):
|
def valid_conflict(conflict):
|
||||||
|
@ -281,7 +272,7 @@ def confirm(request, acronym):
|
||||||
|
|
||||||
form = FormClass(group, meeting, request.POST, hidden=True)
|
form = FormClass(group, meeting, request.POST, hidden=True)
|
||||||
form.is_valid()
|
form.is_valid()
|
||||||
|
|
||||||
login = request.user.person
|
login = request.user.person
|
||||||
|
|
||||||
# check if request already exists for this group
|
# check if request already exists for this group
|
||||||
|
@ -316,38 +307,36 @@ def confirm(request, acronym):
|
||||||
if request.method == 'POST' and button_text == 'Submit':
|
if request.method == 'POST' and button_text == 'Submit':
|
||||||
# delete any existing session records with status = canceled or notmeet
|
# delete any existing session records with status = canceled or notmeet
|
||||||
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)
|
||||||
# create new session records
|
# Create new session records
|
||||||
count = 0
|
# Should really use sess_form.save(), but needs data from the main form as well. Need to sort that out properly.
|
||||||
# lenth_session2 and length_session3 fields might be disabled by javascript and so
|
for count, sess_form in enumerate(form.session_forms[:num_sessions]):
|
||||||
# wouldn't appear in form data
|
slug = 'apprw' if count == 3 else 'schedw'
|
||||||
for duration in (form.data.get('length_session1',None),form.data.get('length_session2',None),form.data.get('length_session3',None)):
|
new_session = Session.objects.create(
|
||||||
count += 1
|
meeting=meeting,
|
||||||
if duration:
|
group=group,
|
||||||
slug = 'apprw' if count == 3 else 'schedw'
|
attendees=form.cleaned_data['attendees'],
|
||||||
new_session = Session.objects.create(
|
requested_duration=sess_form.cleaned_data['requested_duration'],
|
||||||
meeting=meeting,
|
name=sess_form.cleaned_data['name'],
|
||||||
group=group,
|
comments=form.cleaned_data['comments'],
|
||||||
attendees=form.cleaned_data['attendees'],
|
purpose=sess_form.cleaned_data['purpose'],
|
||||||
requested_duration=datetime.timedelta(0,int(duration)),
|
type=sess_form.cleaned_data['type'],
|
||||||
comments=form.cleaned_data['comments'],
|
)
|
||||||
type_id='regular',
|
SchedulingEvent.objects.create(
|
||||||
)
|
session=new_session,
|
||||||
SchedulingEvent.objects.create(
|
status=SessionStatusName.objects.get(slug=slug),
|
||||||
session=new_session,
|
by=login,
|
||||||
status=SessionStatusName.objects.get(slug=slug),
|
)
|
||||||
by=login,
|
if 'resources' in form.data:
|
||||||
)
|
new_session.resources.set(session_data['resources'])
|
||||||
if 'resources' in form.data:
|
jfs = form.data.get('joint_for_session', '-1')
|
||||||
new_session.resources.set(session_data['resources'])
|
if not jfs: # jfs might be ''
|
||||||
jfs = form.data.get('joint_for_session', '-1')
|
jfs = '-1'
|
||||||
if not jfs: # jfs might be ''
|
if int(jfs) == count:
|
||||||
jfs = '-1'
|
groups_split = form.cleaned_data.get('joint_with_groups').replace(',',' ').split()
|
||||||
if int(jfs) == count:
|
joint = Group.objects.filter(acronym__in=groups_split)
|
||||||
groups_split = form.cleaned_data.get('joint_with_groups').replace(',',' ').split()
|
new_session.joint_with_groups.set(joint)
|
||||||
joint = Group.objects.filter(acronym__in=groups_split)
|
session_changed(new_session)
|
||||||
new_session.joint_with_groups.set(joint)
|
|
||||||
session_changed(new_session)
|
|
||||||
|
|
||||||
# write constraint records
|
# write constraint records
|
||||||
for conflictname, cfield_id in form.wg_constraint_field_ids():
|
for conflictname, cfield_id in form.wg_constraint_field_ids():
|
||||||
|
@ -418,7 +407,11 @@ 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)
|
||||||
sessions = add_event_info_to_session_qs(Session.objects.filter(group=group, meeting=meeting)).filter(Q(current_status__isnull=True) | ~Q(current_status__in=['canceled', 'notmeet'])).order_by('id')
|
sessions = add_event_info_to_session_qs(
|
||||||
|
Session.objects.filter(group=group, meeting=meeting)
|
||||||
|
).filter(
|
||||||
|
Q(current_status__isnull=True) | ~Q(current_status__in=['canceled', 'notmeet', 'deleted'])
|
||||||
|
).order_by('id')
|
||||||
sessions_count = sessions.count()
|
sessions_count = sessions.count()
|
||||||
initial = get_initial_session(sessions)
|
initial = get_initial_session(sessions)
|
||||||
FormClass = get_session_form_class()
|
FormClass = get_session_form_class()
|
||||||
|
@ -449,67 +442,68 @@ def edit(request, acronym, num=None):
|
||||||
form = FormClass(group, meeting, request.POST, initial=initial)
|
form = FormClass(group, meeting, request.POST, initial=initial)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
if form.has_changed():
|
if form.has_changed():
|
||||||
|
form.session_forms.save() # todo maintain event creation in commented-out old code!!
|
||||||
# might be cleaner to simply delete and rewrite all records (but maintain submitter?)
|
# might be cleaner to simply delete and rewrite all records (but maintain submitter?)
|
||||||
# adjust duration or add sessions
|
# adjust duration or add sessions
|
||||||
# session 1
|
# session 1
|
||||||
if 'length_session1' in form.changed_data:
|
# if 'length_session1' in form.changed_data:
|
||||||
session = sessions[0]
|
# session = sessions[0]
|
||||||
session.requested_duration = datetime.timedelta(0,int(form.cleaned_data['length_session1']))
|
# session.requested_duration = datetime.timedelta(0,int(form.cleaned_data['length_session1']))
|
||||||
session.save()
|
# session.save()
|
||||||
session_changed(session)
|
# session_changed(session)
|
||||||
|
#
|
||||||
# session 2
|
# # session 2
|
||||||
if 'length_session2' in form.changed_data:
|
# if 'length_session2' in form.changed_data:
|
||||||
length_session2 = form.cleaned_data['length_session2']
|
# length_session2 = form.cleaned_data['length_session2']
|
||||||
if length_session2 == '':
|
# if length_session2 == '':
|
||||||
sessions[1].delete()
|
# sessions[1].delete()
|
||||||
elif sessions_count < 2:
|
# elif sessions_count < 2:
|
||||||
duration = datetime.timedelta(0,int(form.cleaned_data['length_session2']))
|
# duration = datetime.timedelta(0,int(form.cleaned_data['length_session2']))
|
||||||
new_session = Session.objects.create(
|
# new_session = Session.objects.create(
|
||||||
meeting=meeting,
|
# meeting=meeting,
|
||||||
group=group,
|
# group=group,
|
||||||
attendees=form.cleaned_data['attendees'],
|
# attendees=form.cleaned_data['attendees'],
|
||||||
requested_duration=duration,
|
# requested_duration=duration,
|
||||||
comments=form.cleaned_data['comments'],
|
# comments=form.cleaned_data['comments'],
|
||||||
type_id='regular',
|
# type_id='regular',
|
||||||
)
|
# )
|
||||||
SchedulingEvent.objects.create(
|
# SchedulingEvent.objects.create(
|
||||||
session=new_session,
|
# session=new_session,
|
||||||
status=SessionStatusName.objects.get(slug='schedw'),
|
# status=SessionStatusName.objects.get(slug='schedw'),
|
||||||
by=request.user.person,
|
# by=request.user.person,
|
||||||
)
|
# )
|
||||||
else:
|
# else:
|
||||||
duration = datetime.timedelta(0,int(form.cleaned_data['length_session2']))
|
# duration = datetime.timedelta(0,int(form.cleaned_data['length_session2']))
|
||||||
session = sessions[1]
|
# session = sessions[1]
|
||||||
session.requested_duration = duration
|
# session.requested_duration = duration
|
||||||
session.save()
|
# session.save()
|
||||||
|
#
|
||||||
# session 3
|
# # session 3
|
||||||
if 'length_session3' in form.changed_data:
|
# if 'length_session3' in form.changed_data:
|
||||||
length_session3 = form.cleaned_data['length_session3']
|
# length_session3 = form.cleaned_data['length_session3']
|
||||||
if length_session3 == '':
|
# if length_session3 == '':
|
||||||
sessions[2].delete()
|
# sessions[2].delete()
|
||||||
elif sessions_count < 3:
|
# elif sessions_count < 3:
|
||||||
duration = datetime.timedelta(0,int(form.cleaned_data['length_session3']))
|
# duration = datetime.timedelta(0,int(form.cleaned_data['length_session3']))
|
||||||
new_session = Session.objects.create(
|
# new_session = Session.objects.create(
|
||||||
meeting=meeting,
|
# meeting=meeting,
|
||||||
group=group,
|
# group=group,
|
||||||
attendees=form.cleaned_data['attendees'],
|
# attendees=form.cleaned_data['attendees'],
|
||||||
requested_duration=duration,
|
# requested_duration=duration,
|
||||||
comments=form.cleaned_data['comments'],
|
# comments=form.cleaned_data['comments'],
|
||||||
type_id='regular',
|
# type_id='regular',
|
||||||
)
|
# )
|
||||||
SchedulingEvent.objects.create(
|
# SchedulingEvent.objects.create(
|
||||||
session=new_session,
|
# session=new_session,
|
||||||
status=SessionStatusName.objects.get(slug='apprw'),
|
# status=SessionStatusName.objects.get(slug='apprw'),
|
||||||
by=request.user.person,
|
# by=request.user.person,
|
||||||
)
|
# )
|
||||||
else:
|
# else:
|
||||||
duration = datetime.timedelta(0,int(form.cleaned_data['length_session3']))
|
# duration = datetime.timedelta(0,int(form.cleaned_data['length_session3']))
|
||||||
session = sessions[2]
|
# session = sessions[2]
|
||||||
session.requested_duration = duration
|
# session.requested_duration = duration
|
||||||
session.save()
|
# session.save()
|
||||||
session_changed(session)
|
# session_changed(session)
|
||||||
|
|
||||||
# New sessions may have been created, refresh the sessions list
|
# New sessions may have been created, refresh the sessions list
|
||||||
sessions = add_event_info_to_session_qs(
|
sessions = add_event_info_to_session_qs(
|
||||||
|
|
83
ietf/secr/static/secr/js/session_purpose_and_type_widget.js
Normal file
83
ietf/secr/static/secr/js/session_purpose_and_type_widget.js
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
/* Copyright The IETF Trust 2021, All Rights Reserved
|
||||||
|
*
|
||||||
|
* JS support for the SessionPurposeAndTypeWidget
|
||||||
|
* */
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/* Find elements that are parts of the session details widgets. This is an
|
||||||
|
* HTMLCollection that will update if the DOM changes, so ok to evaluate immediately. */
|
||||||
|
const widget_elements = document.getElementsByClassName('session_purpose_widget');
|
||||||
|
|
||||||
|
/* Find the id prefix for each widget. Individual elements have a _<number> suffix. */
|
||||||
|
function get_widget_ids(elements) {
|
||||||
|
const ids = new Set();
|
||||||
|
for (let ii=0; ii < elements.length; ii++) {
|
||||||
|
const parts = elements[ii].id.split('_');
|
||||||
|
parts.pop();
|
||||||
|
ids.add(parts.join('_'));
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Set the 'type' element to a type valid for the currently selected purpose, if possible */
|
||||||
|
function set_valid_type(type_elt, purpose, allowed_types) {
|
||||||
|
const valid_types = allowed_types[purpose] || [];
|
||||||
|
if (valid_types.indexOf(type_elt.value) === -1) {
|
||||||
|
type_elt.value = (valid_types.length > 0) ? valid_types[0] : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide any type options not allowed for the selected purpose */
|
||||||
|
function update_type_option_visibility(type_option_elts, purpose, allowed_types) {
|
||||||
|
const valid_types = allowed_types[purpose] || [];
|
||||||
|
for (const elt of type_option_elts) {
|
||||||
|
if (valid_types.indexOf(elt.value) === -1) {
|
||||||
|
elt.setAttribute('hidden', 'hidden');
|
||||||
|
} else {
|
||||||
|
elt.removeAttribute('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Update visibility of 'type' select so it is only shown when multiple options are available */
|
||||||
|
function update_widget_visibility(elt, purpose, allowed_types) {
|
||||||
|
const valid_types = allowed_types[purpose] || [];
|
||||||
|
if (valid_types.length > 1) {
|
||||||
|
elt.removeAttribute('hidden'); // make visible
|
||||||
|
} else {
|
||||||
|
elt.setAttribute('hidden', 'hidden'); // make invisible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Update the 'type' select to reflect a change in the selected purpose */
|
||||||
|
function update_type_element(type_elt, purpose, type_options, allowed_types) {
|
||||||
|
update_widget_visibility(type_elt, purpose, allowed_types);
|
||||||
|
update_type_option_visibility(type_options, purpose, allowed_types);
|
||||||
|
set_valid_type(type_elt, purpose, allowed_types);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Factory for event handler with a closure */
|
||||||
|
function purpose_change_handler(type_elt, type_options, allowed_types) {
|
||||||
|
return function(event) {
|
||||||
|
update_type_element(type_elt, event.target.value, type_options, allowed_types);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Initialization */
|
||||||
|
function on_load() {
|
||||||
|
for (const widget_id of get_widget_ids(widget_elements)) {
|
||||||
|
const purpose_elt = document.getElementById(widget_id + '_0');
|
||||||
|
const type_elt = document.getElementById(widget_id + '_1');
|
||||||
|
const type_options = type_elt.getElementsByTagName('option');
|
||||||
|
const allowed_types = JSON.parse(type_elt.dataset.allowedOptions);
|
||||||
|
|
||||||
|
purpose_elt.addEventListener(
|
||||||
|
'change',
|
||||||
|
purpose_change_handler(type_elt, type_options, allowed_types)
|
||||||
|
);
|
||||||
|
update_type_element(type_elt, purpose_elt.value, type_options, allowed_types);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('load', on_load, false);
|
||||||
|
})();
|
|
@ -1,59 +1,52 @@
|
||||||
// Copyright The IETF Trust 2015-2021, All Rights Reserved
|
// Copyright The IETF Trust 2015-2021, All Rights Reserved
|
||||||
|
/* global alert */
|
||||||
var ietf_sessions; // public interface
|
var ietf_sessions; // public interface
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
function stat_ls (val){
|
function get_formset_management_data(prefix) {
|
||||||
if (val == 0) {
|
return {
|
||||||
document.form_post.length_session1.disabled = true;
|
total_forms: document.getElementById('id_' + prefix + '-TOTAL_FORMS').value,
|
||||||
document.form_post.length_session2.disabled = true;
|
};
|
||||||
if (document.form_post.length_session3) { document.form_post.length_session3.disabled = true; }
|
}
|
||||||
document.form_post.session_time_relation.disabled = true;
|
|
||||||
document.form_post.joint_for_session.disabled = true;
|
function update_session_form_visibility(session_num, is_visible) {
|
||||||
document.form_post.length_session1.value = 0;
|
const elt = document.getElementById('session_row_' + session_num);
|
||||||
document.form_post.length_session2.value = 0;
|
if (elt) {
|
||||||
document.form_post.length_session3.value = 0;
|
elt.hidden = !is_visible;
|
||||||
document.form_post.session_time_relation.value = '';
|
elt.querySelector('[name$="DELETE"]').value = is_visible ? '' : 'on';
|
||||||
document.form_post.joint_for_session.value = '';
|
|
||||||
document.form_post.third_session.checked=false;
|
|
||||||
}
|
|
||||||
if (val == 1) {
|
|
||||||
document.form_post.length_session1.disabled = false;
|
|
||||||
document.form_post.length_session2.disabled = true;
|
|
||||||
if (document.form_post.length_session3) { document.form_post.length_session3.disabled = true; }
|
|
||||||
document.form_post.session_time_relation.disabled = true;
|
|
||||||
document.form_post.joint_for_session.disabled = true;
|
|
||||||
document.form_post.length_session2.value = 0;
|
|
||||||
document.form_post.length_session3.value = 0;
|
|
||||||
document.form_post.session_time_relation.value = '';
|
|
||||||
document.form_post.joint_for_session.value = '1';
|
|
||||||
document.form_post.third_session.checked=false;
|
|
||||||
}
|
|
||||||
if (val == 2) {
|
|
||||||
document.form_post.length_session1.disabled = false;
|
|
||||||
document.form_post.length_session2.disabled = false;
|
|
||||||
if (document.form_post.length_session3) { document.form_post.length_session3.disabled = false; }
|
|
||||||
document.form_post.session_time_relation.disabled = false;
|
|
||||||
document.form_post.joint_for_session.disabled = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function check_num_session (val) {
|
function have_additional_session() {
|
||||||
if (document.form_post.num_session.value < val) {
|
const elt = document.getElementById('id_third_session');
|
||||||
alert("Please change the value in the Number of Sessions to use this field");
|
return elt && elt.checked;
|
||||||
document.form_post.num_session.focused = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function check_third_session () {
|
function update_for_num_sessions(val) {
|
||||||
if (document.form_post.third_session.checked == false) {
|
const total_forms = get_formset_management_data('session_set').total_forms;
|
||||||
|
val = Number(val);
|
||||||
return true;
|
if (have_additional_session()) {
|
||||||
|
val++;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i=0; i < total_forms; i++) {
|
||||||
|
update_session_form_visibility(i, i < val);
|
||||||
|
}
|
||||||
|
|
||||||
|
const only_one_session = (val === 1);
|
||||||
|
if (document.form_post.session_time_relation) {
|
||||||
|
document.form_post.session_time_relation.disabled = only_one_session;
|
||||||
|
document.form_post.session_time_relation.closest('tr').hidden = only_one_session;
|
||||||
|
}
|
||||||
|
if (document.form_post.joint_for_session) {
|
||||||
|
document.form_post.joint_for_session.disabled = only_one_session;
|
||||||
|
}
|
||||||
|
const third_session_row = document.getElementById('third_session_row');
|
||||||
|
if (third_session_row) {
|
||||||
|
third_session_row.hidden = val < 2;
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function delete_last_joint_with_groups () {
|
function delete_last_joint_with_groups () {
|
||||||
|
@ -114,7 +107,39 @@ var ietf_sessions; // public interface
|
||||||
delete_last_wg_constraint(slug);
|
delete_last_wg_constraint(slug);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for the change event on the session count select or 'third session' checkbox
|
||||||
|
*/
|
||||||
|
function handle_num_session_change(event) {
|
||||||
|
const num_select_value = Number(event.target.value);
|
||||||
|
if (num_select_value !== 2) {
|
||||||
|
if (document.form_post.third_session) {
|
||||||
|
document.form_post.third_session.checked = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
update_for_num_sessions(num_select_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_third_session_change(event) {
|
||||||
|
const num_select_value = Number(document.getElementById('id_num_session').value);
|
||||||
|
if (num_select_value === 2) {
|
||||||
|
update_for_num_sessions(num_select_value);
|
||||||
|
} else {
|
||||||
|
event.target.checked = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Initialization */
|
||||||
function on_load() {
|
function on_load() {
|
||||||
|
// Attach event handler to session count select
|
||||||
|
const num_session_select = document.getElementById('id_num_session');
|
||||||
|
num_session_select.addEventListener('change', handle_num_session_change);
|
||||||
|
const third_session_input = document.getElementById('id_third_session');
|
||||||
|
if (third_session_input) {
|
||||||
|
third_session_input.addEventListener('change', handle_third_session_change);
|
||||||
|
}
|
||||||
|
update_for_num_sessions(num_session_select.value);
|
||||||
|
|
||||||
// Attach event handlers to constraint selectors
|
// Attach event handlers to constraint selectors
|
||||||
let selectors = document.getElementsByClassName('wg_constraint_selector');
|
let selectors = document.getElementsByClassName('wg_constraint_selector');
|
||||||
for (let index = 0; index < selectors.length; index++) {
|
for (let index = 0; index < selectors.length; index++) {
|
||||||
|
@ -128,9 +153,6 @@ var ietf_sessions; // public interface
|
||||||
|
|
||||||
// expose public interface methods
|
// expose public interface methods
|
||||||
ietf_sessions = {
|
ietf_sessions = {
|
||||||
stat_ls: stat_ls,
|
|
||||||
check_num_session: check_num_session,
|
|
||||||
check_third_session: check_third_session,
|
|
||||||
delete_last_joint_with_groups: delete_last_joint_with_groups,
|
delete_last_joint_with_groups: delete_last_joint_with_groups,
|
||||||
delete_wg_constraint_clicked: delete_wg_constraint_clicked
|
delete_wg_constraint_clicked: delete_wg_constraint_clicked
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,27 @@
|
||||||
<span class="required">*</span> Required Field
|
<span class="required">*</span> Required Field
|
||||||
<form id="session-request-form" action="." method="post" name="form_post">{% csrf_token %}
|
<form id="session-request-form" action="." method="post" name="form_post">{% csrf_token %}
|
||||||
|
{{ form.session_forms.management_form }}
|
||||||
{% if form.non_field_errors %}{{ form.non_field_errors }}{% endif %}
|
{% if form.non_field_errors %}{{ form.non_field_errors }}{% endif %}
|
||||||
<table id="sessions-new-table" cellspacing="1" cellpadding="1" border="0">
|
<table id="sessions-new-table" cellspacing="1" cellpadding="1" border="0">
|
||||||
<col width="150">
|
<col width="150">
|
||||||
<tr class="bg1"><td>Working Group Name:</td><td>{{ group.name }} ({{ group.acronym }})</td></tr>
|
<tr class="bg1"><td>Working Group Name:</td><td>{{ group.name }} ({{ group.acronym }})</td></tr>
|
||||||
<tr class="bg2"><td>Area Name:</td><td>{% if group.parent %}{{ group.parent.name }} ({{ group.parent.acronym }}){% endif %}</td></tr>
|
<tr class="bg2"><td>Area Name:</td><td>{% if group.parent %}{{ group.parent.name }} ({{ group.parent.acronym }}){% endif %}</td></tr>
|
||||||
<tr class="bg1"><td>Number of Sessions:<span class="required">*</span></td><td>{{ form.num_session.errors }}{{ form.num_session }}</td></tr>
|
<tr class="bg1"><td>Number of Sessions:<span class="required">*</span></td><td>{{ form.num_session.errors }}{{ form.num_session }}</td></tr>
|
||||||
<tr class="bg2"><td>Length of Session 1:<span class="required">*</span></td><td>{{ form.length_session1.errors }}{{ form.length_session1 }}</td></tr>
|
<tr class="bg2" id="session_row_0"><td>Session 1:<span class="required">*</span></td><td>{% include 'meeting/session_details_form.html' with form=form.session_forms.0 only %}</td></tr>
|
||||||
<tr class="bg2"><td>Length of Session 2:<span class="required">*</span></td><td>{{ form.length_session2.errors }}{{ form.length_session2 }}</td></tr>
|
<tr class="bg2" id="session_row_1"><td>Session 2:<span class="required">*</span></td><td>{% include 'meeting/session_details_form.html' with form=form.session_forms.1 only %}</td></tr>
|
||||||
{% if not is_virtual %}
|
{% if not is_virtual %}
|
||||||
<tr class="bg2"><td>Time between two sessions:</td><td>{{ form.session_time_relation.errors }}{{ form.session_time_relation }}</td></tr>
|
<tr class="bg2"><td>Time between two sessions:</td><td>{{ form.session_time_relation.errors }}{{ form.session_time_relation }}</td></tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if group.type.slug == "wg" %}
|
{% if group.type.slug == "wg" %}
|
||||||
<tr class="bg2"><td>Additional Session Request:</td><td>{{ form.third_session }} Check this box to request an additional session.<br>
|
<tr class="bg2" id="third_session_row"><td>Additional Session Request:</td><td>{{ form.third_session }} Check this box to request an additional session.<br>
|
||||||
Additional slot may be available after agenda scheduling has closed and with the approval of an Area Director.<br>
|
Additional slot may be available after agenda scheduling has closed and with the approval of an Area Director.<br>
|
||||||
Length of Third Session: {{ form.length_session3.errors }}{{ form.length_session3 }}</td></tr>
|
<div id="session_row_2">
|
||||||
|
Third Session:
|
||||||
|
{% include 'meeting/session_details_form.html' with form=form.session_forms.2 only %}
|
||||||
|
</div>
|
||||||
|
</td></tr>
|
||||||
|
{% else %}{# else group.type.slug != "wg" #}
|
||||||
|
{% include 'meeting/session_details_form.html' with form=form.session_forms.2 hidden=True only %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<tr class="bg1"><td>Number of Attendees:{% if not is_virtual %}<span class="required">*</span>{% endif %}</td><td>{{ form.attendees.errors }}{{ form.attendees }}</td></tr>
|
<tr class="bg1"><td>Number of Attendees:{% if not is_virtual %}<span class="required">*</span>{% endif %}</td><td>{{ form.attendees.errors }}{{ form.attendees }}</td></tr>
|
||||||
<tr class="bg2"><td>People who must be present:</td><td>{{ form.bethere.errors }}{{ form.bethere }}</td></tr>
|
<tr class="bg2"><td>People who must be present:</td><td>{{ form.bethere.errors }}{{ form.bethere }}</td></tr>
|
||||||
|
|
|
@ -4,16 +4,24 @@
|
||||||
<tr class="row1"><td>Working Group Name:</td><td>{{ group.name }} ({{ group.acronym }})</td></tr>
|
<tr class="row1"><td>Working Group Name:</td><td>{{ group.name }} ({{ group.acronym }})</td></tr>
|
||||||
<tr class="row2"><td>Area Name:</td><td>{{ group.parent }}</td></tr>
|
<tr class="row2"><td>Area Name:</td><td>{{ group.parent }}</td></tr>
|
||||||
<tr class="row1"><td>Number of Sessions Requested:</td><td>{% if session.length_session3 %}3{% else %}{{ session.num_session }}{% endif %}</td></tr>
|
<tr class="row1"><td>Number of Sessions Requested:</td><td>{% if session.length_session3 %}3{% else %}{{ session.num_session }}{% endif %}</td></tr>
|
||||||
<tr class="row2"><td>Length of Session 1:</td><td>{{ session.length_session1|display_duration }}</td></tr>
|
{% for sess_form in form.session_forms %}{% if sess_form.cleaned_data and not sess_form.cleaned_data.DELETE %}
|
||||||
{% if session.length_session2 %}
|
<tr class="row2"><td>Session {{ forloop.counter }}:</td><td>
|
||||||
<tr class="row2"><td>Length of Session 2:</td><td>{{ session.length_session2|display_duration }}</td></tr>
|
<dl>
|
||||||
{% if not is_virtual %}
|
<dt>Length</dt><dd>{{ sess_form.cleaned_data.requested_duration.total_seconds|display_duration }}</dd>
|
||||||
|
{% if sess_form.cleaned_data.name %}<dt>Name</dt><dd>{{ sess_form.cleaned_data.name }}</dd>{% endif %}
|
||||||
|
{% if sess_form.cleaned_data.purpose.slug != 'session' %}
|
||||||
|
<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 %}
|
||||||
|
</dd>
|
||||||
|
{% endif %}
|
||||||
|
</dl>
|
||||||
|
</td></tr>
|
||||||
|
{% if forloop.counter == 2 and not is_virtual %}
|
||||||
<tr class="row2"><td>Time between sessions:</td><td>{% if session.session_time_relation_display %}{{ session.session_time_relation_display }}{% else %}No preference{% endif %}</td></tr>
|
<tr class="row2"><td>Time between sessions:</td><td>{% if session.session_time_relation_display %}{{ session.session_time_relation_display }}{% else %}No preference{% endif %}</td></tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}{% endfor %}
|
||||||
{% if session.length_session3 %}
|
|
||||||
<tr class="row2"><td>Length of Session 3:</td><td>{{ session.length_session3|display_duration }}</td></tr>
|
|
||||||
{% endif %}
|
|
||||||
<tr class="row1"><td>Number of Attendees:</td><td>{{ session.attendees }}</td></tr>
|
<tr class="row1"><td>Number of Attendees:</td><td>{{ session.attendees }}</td></tr>
|
||||||
<tr class="row2">
|
<tr class="row2">
|
||||||
<td>Conflicts to Avoid:</td>
|
<td>Conflicts to Avoid:</td>
|
||||||
|
|
|
@ -21,3 +21,11 @@
|
||||||
</div> <!-- module -->
|
</div> <!-- module -->
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extrahead %}
|
||||||
|
{{ block.super }}
|
||||||
|
{{ form.media.js }}
|
||||||
|
{% endblock %}
|
||||||
|
{% block extrastyle %}
|
||||||
|
{{ form.media.css }}
|
||||||
|
{% endblock %}
|
|
@ -1,5 +1,5 @@
|
||||||
{% extends "meetings/base_rooms_times.html" %}
|
{% extends "meetings/base_rooms_times.html" %}
|
||||||
|
{% load agenda_custom_tags %}
|
||||||
{% block subsection %}
|
{% block subsection %}
|
||||||
|
|
||||||
<div class="module">
|
<div class="module">
|
||||||
|
@ -27,12 +27,12 @@
|
||||||
<tr class="{% cycle 'row1' 'row2' %}{% ifchanged assignment.session.type %} break{% endifchanged %}{% if assignment.current_session_status == "canceled" %} cancelled{% endif %}">
|
<tr class="{% cycle 'row1' 'row2' %}{% ifchanged assignment.session.type %} break{% endifchanged %}{% if assignment.current_session_status == "canceled" %} cancelled{% endif %}">
|
||||||
<td>{{ assignment.timeslot.time|date:"D" }}</td>
|
<td>{{ assignment.timeslot.time|date:"D" }}</td>
|
||||||
<td>{{ assignment.timeslot.time|date:"H:i" }}-{{ assignment.timeslot.end_time|date:"H:i" }}</td>
|
<td>{{ assignment.timeslot.time|date:"H:i" }}-{{ assignment.timeslot.end_time|date:"H:i" }}</td>
|
||||||
<td>{{ assignment.timeslot.name }}</td>
|
<td>{% assignment_display_name assignment %}</td>
|
||||||
<td>{{ assignment.session.short }}</td>
|
<td>{{ assignment.session.short }}</td>
|
||||||
<td>{{ assignment.session.group.acronym }}</td>
|
<td>{{ assignment.session.group.acronym }}</td>
|
||||||
<td>{{ assignment.timeslot.location }}</td>
|
<td>{{ assignment.timeslot.location }}</td>
|
||||||
<td>{{ assignment.timeslot.show_location }}</td>
|
<td>{{ assignment.timeslot.show_location }}</td>
|
||||||
<td>{{ assignment.timeslot.type }}</td>
|
<td>{% with purpose=assignment.session.purpose %}{{ purpose }}{% if purpose.timeslot_types|length > 1 %} ({{ assignment.slot_type }}){% endif %}{% endwith %}</td>
|
||||||
{% if assignment.schedule_id == schedule.pk %}
|
{% if assignment.schedule_id == schedule.pk %}
|
||||||
<td><a href="{% url "ietf.secr.meetings.views.misc_session_edit" meeting_id=meeting.number schedule_name=schedule.name slot_id=assignment.timeslot.id %}">Edit</a></td>
|
<td><a href="{% url "ietf.secr.meetings.views.misc_session_edit" meeting_id=meeting.number schedule_name=schedule.name slot_id=assignment.timeslot.id %}">Edit</a></td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -49,7 +49,7 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% else %}
|
{% else %}
|
||||||
<h3>No timeslots exist for this meeting. First add the rooms and then the app will create timeslots based on the schedule from the last meeting.</h3>
|
<h3>No timeslots exist for this meeting. Add rooms with the "duplicate timeslots" option enabled to copy timeslots from the last meeting.</h3>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<br /><hr />
|
<br /><hr />
|
||||||
|
|
||||||
|
@ -74,3 +74,11 @@
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extrahead %}
|
||||||
|
{{ block.super }}
|
||||||
|
{{ form.media.js }}
|
||||||
|
{% endblock %}
|
||||||
|
{% block extrastyle %}
|
||||||
|
{{ form.media.css }}
|
||||||
|
{% endblock %}
|
|
@ -11,7 +11,8 @@
|
||||||
<form id="meetings-meta-rooms" action="" method="post">{% csrf_token %}
|
<form id="meetings-meta-rooms" action="" method="post">{% csrf_token %}
|
||||||
{{ formset.management_form }}
|
{{ formset.management_form }}
|
||||||
{{ formset.non_form_errors }}
|
{{ formset.non_form_errors }}
|
||||||
|
{% if options_form %}{{ options_form.errors }}{% endif %}
|
||||||
|
|
||||||
<table id="id_rooms_table" class="full-width">
|
<table id="id_rooms_table" class="full-width">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -43,9 +44,10 @@
|
||||||
</div> <!-- iniline-related -->
|
</div> <!-- iniline-related -->
|
||||||
</div> <!-- inline-group -->
|
</div> <!-- inline-group -->
|
||||||
|
|
||||||
{% include "includes/buttons_save.html" %}
|
{% if options_form %}{{ options_form }}{% endif %}
|
||||||
|
{% include "includes/buttons_save.html" %}
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</div> <!-- module -->
|
</div> <!-- module -->
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% else %}
|
{% else %}
|
||||||
<h3>No timeslots exist for this meeting. First add the rooms and then the app will create timeslots based on the schedule from the last meeting.</h3>
|
<h3>No timeslots exist for this meeting. Add rooms with the "duplicate timeslots" option enabled to copy timeslots from the last meeting.</h3>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<br /><hr />
|
<br /><hr />
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,18 @@
|
||||||
|
|
||||||
{% block title %}Sessions - Confirm{% endblock %}
|
{% block title %}Sessions - Confirm{% endblock %}
|
||||||
|
|
||||||
|
{% block extrastyle %}
|
||||||
|
<style>
|
||||||
|
dl {width: 100%;}
|
||||||
|
dt {float: left; width: 15%; margin: 0.1em 0 0.1em 0; }
|
||||||
|
dt::after {content: ":";}
|
||||||
|
dd {float: left; width: 85%; margin: 0.1em 0 0.1em 0;}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block extrahead %}{{ block.super }}
|
{% block extrahead %}{{ block.super }}
|
||||||
<script type="text/javascript" src="{% static 'secr/js/utils.js' %}"></script>
|
<script type="text/javascript" src="{% static 'secr/js/utils.js' %}"></script>
|
||||||
|
{{ form.media }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block breadcrumbs %}{{ block.super }}
|
{% block breadcrumbs %}{{ block.super }}
|
||||||
|
@ -20,7 +30,7 @@
|
||||||
|
|
||||||
{% include "includes/sessions_request_view.html" %}
|
{% include "includes/sessions_request_view.html" %}
|
||||||
|
|
||||||
{% if session.length_session3 %}
|
{% if form.session_forms.forms_to_keep|length > 2 %}
|
||||||
<br>
|
<br>
|
||||||
<span class="alert"><p><b>Note: Your request for a third session must be approved by an area director before
|
<span class="alert"><p><b>Note: Your request for a third session must be approved by an area director before
|
||||||
being submitted to agenda@ietf.org. Click "Submit" below to email an approval
|
being submitted to agenda@ietf.org. Click "Submit" below to email an approval
|
||||||
|
@ -30,6 +40,8 @@
|
||||||
|
|
||||||
<form action="{% url "ietf.secr.sreq.views.confirm" acronym=group.acronym %}" method="post">{% csrf_token %}
|
<form action="{% url "ietf.secr.sreq.views.confirm" acronym=group.acronym %}" method="post">{% csrf_token %}
|
||||||
{{ form }}
|
{{ form }}
|
||||||
|
{{ form.session_forms.management_form }}
|
||||||
|
{% for sf in form.session_forms %}{% include 'meeting/session_details_form.html' with form=sf hidden=True only %}{% endfor %}
|
||||||
{% include "includes/buttons_submit_cancel.html" %}
|
{% include "includes/buttons_submit_cancel.html" %}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
|
@ -959,8 +959,11 @@ INTERNET_DRAFT_DAYS_TO_EXPIRE = 185
|
||||||
|
|
||||||
FLOORPLAN_MEDIA_DIR = 'floor'
|
FLOORPLAN_MEDIA_DIR = 'floor'
|
||||||
FLOORPLAN_DIR = os.path.join(MEDIA_ROOT, FLOORPLAN_MEDIA_DIR)
|
FLOORPLAN_DIR = os.path.join(MEDIA_ROOT, FLOORPLAN_MEDIA_DIR)
|
||||||
|
FLOORPLAN_LEGACY_BASE_URL = 'https://tools.ietf.org/agenda/{meeting.number}/venue/'
|
||||||
|
FLOORPLAN_LAST_LEGACY_MEETING = 95 # last meeting to use FLOORPLAN_LEGACY_BASE_URL
|
||||||
|
|
||||||
MEETING_USES_CODIMD_DATE = datetime.date(2020,7,6)
|
MEETING_USES_CODIMD_DATE = datetime.date(2020,7,6)
|
||||||
|
MEETING_LEGACY_OFFICE_HOURS_END = 111 # last meeting to use legacy office hours representation
|
||||||
|
|
||||||
# Maximum dimensions to accept at all
|
# Maximum dimensions to accept at all
|
||||||
MEETINGHOST_LOGO_MAX_UPLOAD_WIDTH = 400
|
MEETINGHOST_LOGO_MAX_UPLOAD_WIDTH = 400
|
||||||
|
|
|
@ -1545,6 +1545,43 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
|
||||||
width: 10em;
|
width: 10em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.timeslot-edit .tstable div.timeslot {
|
||||||
|
border: #000000 solid 1px;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeslot-edit .tstable .timeslot .ts-name {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.timeslot-edit .tstable .timeslot .ts-type {
|
||||||
|
font-size: smaller;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeslot-edit .tstable .timeslot .timeslot-buttons {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeslot-edit .tstable .timeslot.in-official-use {
|
||||||
|
background-color: #d9edf7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeslot-edit .tstable .timeslot.in-unofficial-use {
|
||||||
|
background-color: #f8f8e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeslot-edit .tstable td.timeslot-collision {
|
||||||
|
background-color: #ffa0a0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeslot-edit .tstable .tstype_unavail {
|
||||||
|
background-color:#666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeslot-edit .official-use-warning {
|
||||||
|
color: #ff0000;
|
||||||
|
}
|
||||||
|
|
||||||
.rightmarker, .leftmarker {
|
.rightmarker, .leftmarker {
|
||||||
width: 3px;
|
width: 3px;
|
||||||
padding-right: 0px !important;
|
padding-right: 0px !important;
|
||||||
|
|
109
ietf/static/ietf/js/meeting/session_details_form.js
Normal file
109
ietf/static/ietf/js/meeting/session_details_form.js
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
/* Copyright The IETF Trust 2021, All Rights Reserved
|
||||||
|
*
|
||||||
|
* JS support for the SessionDetailsForm
|
||||||
|
* */
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/* Find the id prefix for each widget. Individual elements have a _<number> suffix. */
|
||||||
|
function get_widget_ids(elements) {
|
||||||
|
const ids = new Set();
|
||||||
|
for (let ii=0; ii < elements.length; ii++) {
|
||||||
|
const parts = elements[ii].id.split('_');
|
||||||
|
parts.pop();
|
||||||
|
ids.add(parts.join('_'));
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Set the 'type' element to a type valid for the currently selected purpose, if possible */
|
||||||
|
function set_valid_type(type_elt, purpose, allowed_types) {
|
||||||
|
const valid_types = allowed_types[purpose] || [];
|
||||||
|
if (valid_types.indexOf(type_elt.value) === -1) {
|
||||||
|
type_elt.value = (valid_types.length > 0) ? valid_types[0] : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide any type options not allowed for the selected purpose */
|
||||||
|
function update_type_option_visibility(type_option_elts, purpose, allowed_types) {
|
||||||
|
const valid_types = allowed_types[purpose] || [];
|
||||||
|
for (const elt of type_option_elts) {
|
||||||
|
if (valid_types.indexOf(elt.value) === -1) {
|
||||||
|
elt.setAttribute('hidden', 'hidden');
|
||||||
|
} else {
|
||||||
|
elt.removeAttribute('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Update visibility of 'type' select so it is only shown when multiple options are available */
|
||||||
|
function update_widget_visibility(elt, purpose, allowed_types) {
|
||||||
|
const valid_types = allowed_types[purpose] || [];
|
||||||
|
if (valid_types.length > 1) {
|
||||||
|
elt.removeAttribute('hidden'); // make visible
|
||||||
|
} else {
|
||||||
|
elt.setAttribute('hidden', 'hidden'); // make invisible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Update the 'type' select to reflect a change in the selected purpose */
|
||||||
|
function update_type_element(type_elt, purpose, type_options, allowed_types) {
|
||||||
|
update_widget_visibility(type_elt, purpose, allowed_types);
|
||||||
|
update_type_option_visibility(type_options, purpose, allowed_types);
|
||||||
|
set_valid_type(type_elt, purpose, allowed_types);
|
||||||
|
}
|
||||||
|
|
||||||
|
function update_name_field_visibility(name_elt, purpose) {
|
||||||
|
const row = name_elt.closest('tr');
|
||||||
|
if (purpose === 'session') {
|
||||||
|
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) {
|
||||||
|
return function(event) {
|
||||||
|
const purpose = event.target.value;
|
||||||
|
update_name_field_visibility(name_elt, purpose);
|
||||||
|
update_type_element(type_elt, purpose, type_options, allowed_types);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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 type_options = type_elt.getElementsByTagName('option');
|
||||||
|
const allowed_types = JSON.parse(type_elt.dataset.allowedOptions);
|
||||||
|
|
||||||
|
// update on future changes
|
||||||
|
purpose_elt.addEventListener(
|
||||||
|
'change',
|
||||||
|
purpose_change_handler(name_elt, type_elt, type_options, allowed_types)
|
||||||
|
);
|
||||||
|
|
||||||
|
// update immediately
|
||||||
|
update_type_element(type_elt, purpose_elt.value, type_options, allowed_types);
|
||||||
|
update_name_field_visibility(name_elt, purpose_elt.value);
|
||||||
|
|
||||||
|
// hide the purpose selector if only one option
|
||||||
|
const purpose_options = purpose_elt.querySelectorAll('option:not([value=""])');
|
||||||
|
if (purpose_options.length < 2) {
|
||||||
|
purpose_elt.closest('tr').setAttribute('hidden', 'hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Initialization */
|
||||||
|
function on_load() {
|
||||||
|
/* Find elements that are parts of the session details forms. This is an
|
||||||
|
* HTMLCollection that will update if the DOM changes, so ok to evaluate immediately. */
|
||||||
|
const forms = document.getElementsByClassName('session-details-form');
|
||||||
|
for (const form of forms) {
|
||||||
|
add_purpose_change_handler(form);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('load', on_load, false);
|
||||||
|
})();
|
|
@ -4,7 +4,7 @@
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load ietf_filters %}
|
{% load ietf_filters %}
|
||||||
{% load textfilters %}
|
{% load textfilters %}
|
||||||
{% load htmlfilters %}
|
{% load htmlfilters agenda_custom_tags%}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
IETF {{ schedule.meeting.number }} meeting agenda
|
IETF {{ schedule.meeting.number }} meeting agenda
|
||||||
|
@ -143,119 +143,105 @@
|
||||||
</tr>
|
</tr>
|
||||||
{% endifchanged %}
|
{% endifchanged %}
|
||||||
|
|
||||||
{% if item.timeslot.type_id == 'regular' %}
|
{% if item|is_special_agenda_item %}
|
||||||
{% ifchanged %}
|
<tr id="row-{{ item.slug }}" data-filter-keywords="{{ item.filter_keywords|join:',' }}"
|
||||||
<tr class="info session-label-row"
|
data-slot-start-ts="{{item.start_timestamp}}"
|
||||||
data-slot-start-ts="{{item.start_timestamp}}"
|
data-slot-end-ts="{{item.end_timestamp}}">
|
||||||
data-slot-end-ts="{{item.end_timestamp}}">
|
<td class="leftmarker"></td>
|
||||||
<td class="leftmarker"></td>
|
<td class="text-nowrap text-right">
|
||||||
<th class="text-nowrap text-right">
|
|
||||||
<span class="hidden-xs">
|
<span class="hidden-xs">
|
||||||
{% include "meeting/timeslot_start_end.html" %}
|
{% include "meeting/timeslot_start_end.html" %}
|
||||||
</span>
|
</span>
|
||||||
</th>
|
</td>
|
||||||
<th colspan="4">
|
<td colspan="3">
|
||||||
<span class="hidden-sm hidden-md hidden-lg">
|
<span class="hidden-sm hidden-md hidden-lg">
|
||||||
{% include "meeting/timeslot_start_end.html" %}
|
{% include "meeting/timeslot_start_end.html" %}
|
||||||
</span>
|
</span>
|
||||||
{{ item.timeslot.time|date:"l"}}
|
{% location_anchor item.timeslot %}
|
||||||
{{item.timeslot.name|capfirst_allcaps}}
|
{{ item.timeslot.get_html_location }}
|
||||||
</th>
|
{% end_location_anchor %}
|
||||||
<td class="rightmarker"></td>
|
{% if item.timeslot.show_location and item.timeslot.get_html_location %}
|
||||||
</tr>
|
{% with item.timeslot.location.floorplan as floor %}
|
||||||
{% endifchanged %}
|
{% if item.timeslot.location.floorplan %}
|
||||||
{% endif %}
|
<span class="hidden-xs">
|
||||||
|
<a href="{% url 'ietf.meeting.views.floor_plan' num=schedule.meeting.number %}#{{floor.name|xslugify}}"
|
||||||
{% if item.timeslot.type.slug == 'break' or item.timeslot.type.slug == 'reg' or item.timeslot.type.slug == 'other' %}
|
class="pull-right" title="{{floor.name}}"><span class="label label-blank label-wide">{{floor.short}}</span></a>
|
||||||
<tr id="row-{{ item.slug }}" data-filter-keywords="{{ item.filter_keywords|join:',' }}"
|
</span>
|
||||||
data-slot-start-ts="{{item.start_timestamp}}"
|
{% endif %}
|
||||||
data-slot-end-ts="{{item.end_timestamp}}">
|
{% endwith %}
|
||||||
<td class="leftmarker"></td>
|
|
||||||
<td class="text-nowrap text-right">
|
|
||||||
<span class="hidden-xs">
|
|
||||||
{% include "meeting/timeslot_start_end.html" %}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td colspan="3">
|
|
||||||
<span class="hidden-sm hidden-md hidden-lg">
|
|
||||||
{% include "meeting/timeslot_start_end.html" %}
|
|
||||||
</span>
|
|
||||||
{% if item.timeslot.show_location and item.timeslot.get_html_location %}
|
|
||||||
{% if schedule.meeting.number|add:"0" < 96 %}
|
|
||||||
<a href="https://tools.ietf.org/agenda/{{schedule.meeting.number}}/venue/?room={{ item.timeslot.get_html_location|xslugify }}">{{item.timeslot.get_html_location}}</a>
|
|
||||||
{% elif item.timeslot.location.floorplan %}
|
|
||||||
<a href="{% url 'ietf.meeting.views.floor_plan' num=schedule.meeting.number %}?room={{ item.timeslot.get_html_location|xslugify }}">{{item.timeslot.get_html_location}}</a>
|
|
||||||
{% else %}
|
|
||||||
{{item.timeslot.get_html_location}}
|
|
||||||
{% endif %}
|
|
||||||
{% with item.timeslot.location.floorplan as floor %}
|
|
||||||
{% if item.timeslot.location.floorplan %}
|
|
||||||
<span class="hidden-xs">
|
|
||||||
<a href="{% url 'ietf.meeting.views.floor_plan' num=schedule.meeting.number %}#{{floor.name|xslugify}}"
|
|
||||||
class="pull-right" title="{{floor.name}}"><span class="label label-blank label-wide">{{floor.short}}</span></a>
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if item.session.agenda %}
|
|
||||||
<a href="{{ item.session.agenda.get_href }}">
|
|
||||||
{{item.timeslot.name}}
|
|
||||||
</a>
|
|
||||||
{% else %}
|
|
||||||
{{item.timeslot.name}}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if item.session.current_status == 'canceled' %}
|
|
||||||
<span class="label label-danger pull-right">CANCELLED</span>
|
|
||||||
{% else %}
|
|
||||||
<div class="pull-right padded-left">
|
|
||||||
{% if item.timeslot.type.slug == 'other' %}
|
|
||||||
{% if item.session.agenda or item.session.remote_instructions or item.session.agenda_note %}
|
|
||||||
{% include "meeting/session_buttons_include.html" with show_agenda=True item=item schedule=schedule %}
|
|
||||||
{% else %}
|
|
||||||
{% for slide in item.session.slides %}
|
|
||||||
<a href="{{slide.get_href}}">{{ slide.title|clean_whitespace }}</a>
|
|
||||||
<br>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</td>
|
||||||
{% endif %}
|
<td>
|
||||||
</td>
|
{% agenda_anchor item.session %}
|
||||||
<td class="rightmarker"></td>
|
{% assignment_display_name item %}
|
||||||
</tr>
|
{% end_agenda_anchor %}
|
||||||
{% endif %}
|
|
||||||
|
{% if item.session.current_status == 'canceled' %}
|
||||||
|
<span class="label label-danger pull-right">CANCELLED</span>
|
||||||
|
{% else %}
|
||||||
|
<div class="pull-right padded-left">
|
||||||
|
{% if item.slot_type.slug == 'other' %}
|
||||||
|
{% if item.session.agenda or item.session.remote_instructions or item.session.agenda_note %}
|
||||||
|
{% include "meeting/session_buttons_include.html" with show_agenda=True item=item schedule=schedule %}
|
||||||
|
{% else %}
|
||||||
|
{% for slide in item.session.slides %}
|
||||||
|
<a href="{{slide.get_href}}">{{ slide.title|clean_whitespace }}</a>
|
||||||
|
<br>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="rightmarker"></td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{% elif item|is_regular_agenda_item or item|is_plenary_agenda_item %}
|
||||||
|
|
||||||
|
{% if item|is_regular_agenda_item %}
|
||||||
|
{% ifchanged %}
|
||||||
|
<tr class="info session-label-row"
|
||||||
|
data-slot-start-ts="{{item.start_timestamp}}"
|
||||||
|
data-slot-end-ts="{{item.end_timestamp}}">
|
||||||
|
<td class="leftmarker"></td>
|
||||||
|
<th class="text-nowrap text-right">
|
||||||
|
<span class="hidden-xs">
|
||||||
|
{% include "meeting/timeslot_start_end.html" %}
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
<th colspan="4">
|
||||||
|
<span class="hidden-sm hidden-md hidden-lg">
|
||||||
|
{% include "meeting/timeslot_start_end.html" %}
|
||||||
|
</span>
|
||||||
|
{{ item.timeslot.time|date:"l"}}
|
||||||
|
{{item.timeslot.name|capfirst_allcaps}}
|
||||||
|
</th>
|
||||||
|
<td class="rightmarker"></td>
|
||||||
|
</tr>
|
||||||
|
{% endifchanged %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if item.timeslot.type_id == 'regular' or item.timeslot.type.slug == 'plenary' %}
|
|
||||||
{% if item.session.historic_group %}
|
{% if item.session.historic_group %}
|
||||||
<tr id="row-{{item.slug}}"
|
<tr id="row-{{item.slug}}"
|
||||||
{% if item.timeslot.type.slug == 'plenary' %}class="{{item.timeslot.type.slug}}danger"{% endif %}
|
{% if item.slot_type.slug == 'plenary' %}class="{{item.slot_type.slug}}danger"{% endif %}
|
||||||
data-filter-keywords="{{ item.filter_keywords|join:',' }}"
|
data-filter-keywords="{{ item.filter_keywords|join:',' }}"
|
||||||
data-slot-start-ts="{{item.start_timestamp}}"
|
data-slot-start-ts="{{item.start_timestamp}}"
|
||||||
data-slot-end-ts="{{item.end_timestamp}}">
|
data-slot-end-ts="{{item.end_timestamp}}">
|
||||||
<td class="leftmarker"></td>
|
<td class="leftmarker"></td>
|
||||||
{% if item.timeslot.type.slug == 'plenary' %}
|
{% if item.slot_type.slug == 'plenary' %}
|
||||||
<th class="text-nowrap text-right">
|
<th class="text-nowrap text-right">
|
||||||
<span class="hidden-xs">
|
<span class="hidden-xs">
|
||||||
{% include "meeting/timeslot_start_end.html" %}
|
{% include "meeting/timeslot_start_end.html" %}
|
||||||
</span>
|
</span>
|
||||||
</th>
|
</th>
|
||||||
<td colspan="3">
|
<td colspan="3">
|
||||||
<span class="hidden-sm hidden-md hidden-lg">
|
<span class="hidden-sm hidden-md hidden-lg">
|
||||||
{% include "meeting/timeslot_start_end.html" %}
|
{% include "meeting/timeslot_start_end.html" %}
|
||||||
</span>
|
</span>
|
||||||
{% if item.timeslot.show_location and item.timeslot.get_html_location %}
|
{% location_anchor item.timeslot %}
|
||||||
{% if schedule.meeting.number|add:"0" < 96 %}
|
{{ item.timeslot.get_html_location }}
|
||||||
<a href="https://tools.ietf.org/agenda/{{schedule.meeting.number}}/venue/?room={{ item.timeslot.get_html_location|xslugify }}">{{item.timeslot.get_html_location}}</a>
|
{% end_location_anchor %}
|
||||||
{% elif item.timeslot.location.floorplan %}
|
</td>
|
||||||
<a href="{% url 'ietf.meeting.views.floor_plan' num=schedule.meeting.number %}?room={{ item.timeslot.get_html_location|xslugify }}">{{item.timeslot.get_html_location}}</a>
|
|
||||||
{% else %}
|
|
||||||
{{item.timeslot.get_html_location}}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<td>
|
<td>
|
||||||
|
@ -269,15 +255,9 @@
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if item.timeslot.show_location and item.timeslot.get_html_location %}
|
{% location_anchor item.timeslot %}
|
||||||
{% if schedule.meeting.number|add:"0" < 96 %}
|
{{ item.timeslot.get_html_location }}
|
||||||
<a href="https://tools.ietf.org/agenda/{{schedule.meeting.number}}/venue/?room={{ item.timeslot.get_html_location|xslugify }}">{{item.timeslot.get_html_location}}</a>
|
{% end_location_anchor %}
|
||||||
{% elif item.timeslot.location.floorplan %}
|
|
||||||
<a href="{% url 'ietf.meeting.views.floor_plan' num=schedule.meeting.number %}?room={{ item.timeslot.get_html_location|xslugify }}">{{item.timeslot.get_html_location}}</a>
|
|
||||||
{% else %}
|
|
||||||
{{item.timeslot.get_html_location}}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td><span class="hidden-xs">{{item.session.historic_group.historic_parent.acronym}}</span></td>
|
<td><span class="hidden-xs">{{item.session.historic_group.historic_parent.acronym}}</span></td>
|
||||||
|
@ -292,18 +272,9 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
{% if item.session.agenda %}
|
{% agenda_anchor item.session %}
|
||||||
<a href="{{ item.session.agenda.get_href }}">
|
{% assignment_display_name item %}
|
||||||
{% endif %}
|
{% end_agenda_anchor %}
|
||||||
{% if item.timeslot.type.slug == 'plenary' %}
|
|
||||||
{{item.timeslot.name}}
|
|
||||||
{% else %}
|
|
||||||
{{item.session.historic_group.name}}
|
|
||||||
{% endif %}
|
|
||||||
{% if item.session.agenda %}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if item.session.current_status == 'canceled' %}
|
{% if item.session.current_status == 'canceled' %}
|
||||||
<span class="label label-danger pull-right">CANCELLED</span>
|
<span class="label label-danger pull-right">CANCELLED</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -453,9 +424,9 @@
|
||||||
// either have not yet loaded the iframe or we do not support history replacement
|
// either have not yet loaded the iframe or we do not support history replacement
|
||||||
wv_iframe.src = new_url;
|
wv_iframe.src = new_url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function update_view(filter_params) {
|
function update_view(filter_params) {
|
||||||
update_agenda_display(filter_params);
|
update_agenda_display(filter_params);
|
||||||
update_weekview(filter_params)
|
update_weekview(filter_params)
|
||||||
|
|
|
@ -14,17 +14,17 @@
|
||||||
|
|
||||||
|
|
||||||
{{ item.timeslot.time|date:"l"|upper }}, {{ item.timeslot.time|date:"F j, Y" }}
|
{{ item.timeslot.time|date:"l"|upper }}, {{ item.timeslot.time|date:"F j, Y" }}
|
||||||
{% endifchanged %}{% if item.timeslot.type.slug == "reg" %}
|
{% endifchanged %}{% if item.slot_type.slug == "reg" %}
|
||||||
{{ item.timeslot.time_desc }} {{ item.timeslot.name }}{% if schedule.meeting.reg_area %} - {{ schedule.meeting.reg_area }}{% endif %}{% endif %}{% if item.timeslot.type.slug == "plenary" %}
|
{{ item.timeslot.time_desc }} {{ item.timeslot.name }}{% if schedule.meeting.reg_area %} - {{ schedule.meeting.reg_area }}{% endif %}{% endif %}{% if item.slot_type.slug == "plenary" %}
|
||||||
{{ item.timeslot.time_desc }} {{ item.session.name }} - {{ item.timeslot.location.name }}
|
{{ item.timeslot.time_desc }} {{ item.session.name }} - {{ item.timeslot.location.name }}
|
||||||
|
|
||||||
{{ item.session.agenda_text.strip|indent:"3" }}
|
{{ item.session.agenda_text.strip|indent:"3" }}
|
||||||
{% endif %}{% if item.timeslot.type_id == 'regular' %}{% if item.session.historic_group %}{% ifchanged %}
|
{% endif %}{% if item.slot_type.slug == 'regular' %}{% if item.session.historic_group %}{% ifchanged %}
|
||||||
|
|
||||||
{{ item.timeslot.time_desc }} {{ item.timeslot.name }}
|
{{ item.timeslot.time_desc }} {{ item.timeslot.name }}
|
||||||
{% endifchanged %}{{ item.timeslot.location.name|ljust:14 }} {{ item.session.historic_group.historic_parent.acronym|upper|ljust:4 }} {{ item.session.historic_group.acronym|ljust:10 }} {{ item.session.historic_group.name }} {% if item.session.historic_group.state_id == "bof" %}BOF{% elif item.session.historic_group.type_id == "wg" %}WG{% endif %}{% if item.session.agenda_note %} - {{ item.session.agenda_note }}{% endif %}{% if item.session.current_status == 'canceled' %} *** CANCELLED ***{% elif item.session.current_status == 'resched' %} *** RESCHEDULED{% if item.session.rescheduled_to %} TO {{ item.session.rescheduled_to.time|date:"l G:i"|upper }}-{{ item.session.rescheduled_to.end_time|date:"G:i" }}{% endif %} ***{% endif %}
|
{% endifchanged %}{{ item.timeslot.location.name|ljust:14 }} {{ item.session.historic_group.historic_parent.acronym|upper|ljust:4 }} {{ item.session.historic_group.acronym|ljust:10 }} {{ item.session.historic_group.name }} {% if item.session.historic_group.state_id == "bof" %}BOF{% elif item.session.historic_group.type_id == "wg" %}WG{% endif %}{% if item.session.agenda_note %} - {{ item.session.agenda_note }}{% endif %}{% if item.session.current_status == 'canceled' %} *** CANCELLED ***{% elif item.session.current_status == 'resched' %} *** RESCHEDULED{% if item.session.rescheduled_to %} TO {{ item.session.rescheduled_to.time|date:"l G:i"|upper }}-{{ item.session.rescheduled_to.end_time|date:"G:i" }}{% endif %} ***{% endif %}
|
||||||
{% endif %}{% endif %}{% if item.timeslot.type.slug == "break" %}
|
{% endif %}{% endif %}{% if item.slot_type.slug == "break" %}
|
||||||
{{ item.timeslot.time_desc }} {{ item.timeslot.name }}{% if schedule.meeting.break_area and item.timeslot.show_location %} - {{ schedule.meeting.break_area }}{% endif %}{% endif %}{% if item.timeslot.type.slug == "other" %}
|
{{ item.timeslot.time_desc }} {{ item.timeslot.name }}{% if schedule.meeting.break_area and item.timeslot.show_location %} - {{ schedule.meeting.break_area }}{% endif %}{% endif %}{% if item.slot_type.slug == "other" %}
|
||||||
{{ item.timeslot.time_desc }} {{ item.timeslot.name }} - {{ item.timeslot.location.name }}{% endif %}{% endfor %}
|
{{ item.timeslot.time_desc }} {{ item.timeslot.name }} - {{ item.timeslot.location.name }}{% endif %}{% endfor %}
|
||||||
|
|
||||||
====================================================================
|
====================================================================
|
||||||
|
|
|
@ -29,7 +29,7 @@ ul.sessionlist { list-style:none; padding-left:2em; margin-bottom:10px;}
|
||||||
<li class="roomlistentry"><h3>{{room.grouper|default:"Location Unavailable"}}</h3>
|
<li class="roomlistentry"><h3>{{room.grouper|default:"Location Unavailable"}}</h3>
|
||||||
<ul class="sessionlist">
|
<ul class="sessionlist">
|
||||||
{% for ss in room.list %}
|
{% for ss in room.list %}
|
||||||
<li class="sessionlistentry type-{{ss.timeslot.type_id}} {% if ss.schedule_id != meeting.schedule_id %}from-base-schedule{% endif %}">{{ss.timeslot.time|date:"H:i"}}-{{ss.timeslot.end_time|date:"H:i"}} {{ss.session.short_name}}</li>
|
<li class="sessionlistentry type-{{ss.slot_type.slug}} {% if ss.schedule_id != meeting.schedule_id %}from-base-schedule{% endif %}">{{ss.timeslot.time|date:"H:i"}}-{{ss.timeslot.end_time|date:"H:i"}} {{ss.session.short_name}}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -61,12 +61,12 @@ Optional parameters:
|
||||||
{% for fc in filter_categories %}
|
{% for fc in filter_categories %}
|
||||||
{% if not forloop.first %} <td></td> {% endif %}
|
{% if not forloop.first %} <td></td> {% endif %}
|
||||||
{% for p in fc %}
|
{% for p in fc %}
|
||||||
<td class="view {{ p.keyword }}">
|
<td class="view{% if p.keyword %} {{ p.keyword }}{% endif %}">
|
||||||
<div class="btn-group-vertical btn-block">
|
<div class="btn-group-vertical btn-block">
|
||||||
{% for button in p.children|dictsort:"label" %}
|
{% for button in p.children %}
|
||||||
<div class="btn-group btn-group-xs btn-group-justified">
|
<div class="btn-group btn-group-xs btn-group-justified">
|
||||||
<button class="btn btn-default pickview {{ button.keyword }}"
|
<button class="btn btn-default pickview {{ button.keyword }}"
|
||||||
{% if p.keyword or button.is_bof %}data-filter-keywords="{% if p.keyword %}{{ p.keyword }}{% if button.is_bof %},{% endif %}{% endif %}{% if button.is_bof %}bof{% endif %}"{% endif %}
|
{% if button.toggled_by %}data-filter-keywords="{{ button.toggled_by|join:"," }}"{% endif %}
|
||||||
data-filter-item="{{ button.keyword }}">
|
data-filter-item="{{ button.keyword }}">
|
||||||
{% if button.is_bof %}
|
{% if button.is_bof %}
|
||||||
<i>{{ button.label }}</i>
|
<i>{{ button.label }}</i>
|
||||||
|
|
27
ietf/templates/meeting/create_timeslot.html
Executable file
27
ietf/templates/meeting/create_timeslot.html
Executable 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 %}Create timeslot for {{meeting}}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% origin %}
|
||||||
|
<h1>Create timeslot for {{meeting}}</h1>
|
||||||
|
<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_timeslots' num=meeting.number %}">Cancel</a>
|
||||||
|
{% endbuttons %}
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
{{ form.media.js }}
|
||||||
|
{% endblock %}
|
35
ietf/templates/meeting/edit_timeslot.html
Normal file
35
ietf/templates/meeting/edit_timeslot.html
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{# Copyright The IETF Trust 2021, All Rights Reserved #}
|
||||||
|
{% load origin %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
|
||||||
|
{% block pagehead %}
|
||||||
|
{{ form.media.css }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block title %}Edit timeslot "{{ timeslot.name }}" for {{ timeslot.meeting }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% origin %}
|
||||||
|
<h1>Edit timeslot "{{ timeslot.name }}" for {{ timeslot.meeting }}</h1>
|
||||||
|
{% if sessions %}
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
This timeslot currently has the following sessions assigned to it:
|
||||||
|
{% for s in sessions %}
|
||||||
|
<div>{{s}}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<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_timeslots' num=timeslot.meeting.number %}">Cancel</a>
|
||||||
|
{% endbuttons %}
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
{{ form.media.js }}
|
||||||
|
{% endblock %}
|
|
@ -16,13 +16,13 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<!-- etherpad -->
|
<!-- etherpad -->
|
||||||
{% if use_codimd %}
|
{% if use_codimd %}
|
||||||
{% if item.timeslot.type.slug == 'plenary' %}
|
{% if item.slot_type.slug == 'plenary' %}
|
||||||
<a class="" href="https://codimd.ietf.org/notes-ietf-{{ meeting.number }}-plenary title="Etherpad for note-takers"><span class="fa fa-fw fa-edit"></span></a>
|
<a class="" href="https://codimd.ietf.org/notes-ietf-{{ meeting.number }}-plenary title="Etherpad for note-takers"><span class="fa fa-fw fa-edit"></span></a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a class="" href="https://codimd.ietf.org/notes-ietf-{{ meeting.number }}-{{acronym}}" title="Etherpad for note-takers"><span class="fa fa-fw fa-edit"></span></a>
|
<a class="" href="https://codimd.ietf.org/notes-ietf-{{ meeting.number }}-{{acronym}}" title="Etherpad for note-takers"><span class="fa fa-fw fa-edit"></span></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if item.timeslot.type.slug == 'plenary' %}
|
{% if item.slot_type.slug == 'plenary' %}
|
||||||
<a class="" href="https://etherpad.ietf.org:9009/p/notes-ietf-{{ meeting.number }}-plenary?useMonospaceFont=true" title="Etherpad for note-takers"><span class="fa fa-fw fa-edit"></span></a>
|
<a class="" href="https://etherpad.ietf.org:9009/p/notes-ietf-{{ meeting.number }}-plenary?useMonospaceFont=true" title="Etherpad for note-takers"><span class="fa fa-fw fa-edit"></span></a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a class="" href="https://etherpad.ietf.org:9009/p/notes-ietf-{{ meeting.number }}-{{acronym}}?useMonospaceFont=true" title="Etherpad for note-takers"><span class="fa fa-fw fa-edit"></span></a>
|
<a class="" href="https://etherpad.ietf.org:9009/p/notes-ietf-{{ meeting.number }}-{{acronym}}?useMonospaceFont=true" title="Etherpad for note-takers"><span class="fa fa-fw fa-edit"></span></a>
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
{% for ss in assignments %}
|
{% for ss in assignments %}
|
||||||
if (room_names.indexOf("{{ss.timeslot.get_hidden_location}}") >= 0 )
|
if (room_names.indexOf("{{ss.timeslot.get_hidden_location}}") >= 0 )
|
||||||
{
|
{
|
||||||
items.push({room_index:room_names.indexOf("{{ss.timeslot.get_hidden_location}}"),day:{{ss.day}}, delta_from_beginning:{{ss.delta_from_beginning}},time:"{{ss.timeslot.time|date:"Hi"}}-{{ss.timeslot.end_time|date:"Hi"}}", verbose_time:"{{ss.timeslot.time|date:"D M d Hi"}}-{{ss.timeslot.end_time|date:"Hi"}}",duration:{{ss.timeslot.duration.total_seconds}}, type:"{{ss.timeslot.type}}", {% if ss.session.name %}name:"{{ss.session.name|escapejs}}",{% if ss.session.group.acronym %} wg:"{{ss.session.group.acronym}}",{%endif%}{% else %}{% if ss.timeslot.type.name == "Break" %}name:"{{ss.timeslot.name|escapejs}}", area:"break", wg:"break",{% elif ss.timeslot.type_id == "unavail" %}name:"Unavailable",{% else %}name:"{{ss.session.group.name|escapejs}}{%if ss.session.group.state.name == "BOF"%} BOF{%endif%}",wg:"{{ss.session.group.acronym}}",state:"{{ss.session.group.state}}",area:"{{ss.session.group.parent.acronym}}",{% endif %}{% endif %} dayname:"{{ ss.timeslot.time|date:"l"|upper }}, {{ ss.timeslot.time|date:"F j, Y" }}"{% if ss.session.agenda %}, agenda:"{{ss.session.agenda.get_href}}"{% endif %}, from_base_schedule: {% if ss.schedule_id != meeting.schedule_id %}true{% else %}false{% endif %} });
|
items.push({room_index:room_names.indexOf("{{ss.timeslot.get_hidden_location}}"),day:{{ss.day}}, delta_from_beginning:{{ss.delta_from_beginning}},time:"{{ss.timeslot.time|date:"Hi"}}-{{ss.timeslot.end_time|date:"Hi"}}", verbose_time:"{{ss.timeslot.time|date:"D M d Hi"}}-{{ss.timeslot.end_time|date:"Hi"}}",duration:{{ss.timeslot.duration.total_seconds}}, type:"{{ss.slot_type}}", {% if ss.session.name %}name:"{{ss.session.name|escapejs}}",{% if ss.session.group.acronym %} wg:"{{ss.session.group.acronym}}",{%endif%}{% else %}{% if ss.slot_type.name == "Break" %}name:"{{ss.timeslot.name|escapejs}}", area:"break", wg:"break",{% elif ss.slot_type.slug == "unavail" %}name:"Unavailable",{% else %}name:"{{ss.session.group.name|escapejs}}{%if ss.session.group.state.name == "BOF"%} BOF{%endif%}",wg:"{{ss.session.group.acronym}}",state:"{{ss.session.group.state}}",area:"{{ss.session.group.parent.acronym}}",{% endif %}{% endif %} dayname:"{{ ss.timeslot.time|date:"l"|upper }}, {{ ss.timeslot.time|date:"F j, Y" }}"{% if ss.session.agenda %}, agenda:"{{ss.session.agenda.get_href}}"{% endif %}, from_base_schedule: {% if ss.schedule_id != meeting.schedule_id %}true{% else %}false{% endif %} });
|
||||||
}
|
}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endautoescape %}
|
{% endautoescape %}
|
||||||
|
|
|
@ -10,6 +10,9 @@
|
||||||
<h1>{% block title %}Possible Meeting Agendas for IETF {{ meeting.number }}{% endblock %}</h1>
|
<h1>{% block title %}Possible Meeting Agendas for IETF {{ meeting.number }}{% endblock %}</h1>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
{% if can_edit_timeslots %}
|
||||||
|
<p><a href="{% url "ietf.meeting.views.edit_timeslots" num=meeting.number %}">Edit timeslots and room availability</a></p>
|
||||||
|
{% endif %}
|
||||||
{% for schedules, own, label in schedule_groups %}
|
{% for schedules, own, label in schedule_groups %}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
|
|
|
@ -2,8 +2,10 @@
|
||||||
{% load origin %}
|
{% load origin %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load textfilters %}
|
{% load textfilters %}
|
||||||
|
{% load ietf_filters %}
|
||||||
{% origin %}
|
{% origin %}
|
||||||
|
|
||||||
|
{% if item|should_show_agenda_session_buttons %}
|
||||||
{% with slug=item.slug %}{% with session=item.session %}{% with timeslot=item.timeslot %}{% with meeting=schedule.meeting %}
|
{% with slug=item.slug %}{% with session=item.session %}{% with timeslot=item.timeslot %}{% with meeting=schedule.meeting %}
|
||||||
<span id="session-buttons-{{session.pk}}" class="text-nowrap">
|
<span id="session-buttons-{{session.pk}}" class="text-nowrap">
|
||||||
{% with acronym=session.historic_group.acronym %}
|
{% with acronym=session.historic_group.acronym %}
|
||||||
|
@ -117,4 +119,5 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</span>
|
</span>
|
||||||
{% endwith %}{% endwith %}{% endwith %}{% endwith %}
|
{% endwith %}{% endwith %}{% endwith %}{% endwith %}
|
||||||
|
{% endif %}
|
24
ietf/templates/meeting/session_details_form.html
Normal file
24
ietf/templates/meeting/session_details_form.html
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{# 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 }}
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>{{ form.name.label_tag }}</th>
|
||||||
|
<td>{{ form.name }}{{ form.purpose.errors }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{{ form.purpose.label_tag }}</th>
|
||||||
|
<td>
|
||||||
|
{{ form.purpose }} {{ form.type }}
|
||||||
|
{{ form.purpose.errors }}{{ form.type.errors }}
|
||||||
|
</td>
|
||||||
|
<tr>
|
||||||
|
<th>{{ form.requested_duration.label_tag }}</th>
|
||||||
|
<td>{{ form.requested_duration }}{{ form.requested_duration.errors }}</td>
|
||||||
|
</tr>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
|
@ -3,55 +3,410 @@
|
||||||
{% load origin %}
|
{% load origin %}
|
||||||
{% load agenda_custom_tags %}
|
{% load agenda_custom_tags %}
|
||||||
|
|
||||||
{% block title %}IETF {{ meeting.number }} Meeting Agenda: Timeslot/Room Availability{% endblock %}
|
{% block title %}IETF {{ meeting.number }} Meeting Agenda: Timeslots and Room Availability{% endblock %}
|
||||||
|
|
||||||
{% block morecss %}
|
{% block morecss %}
|
||||||
.tstable { width: 100%;}
|
.tstable { width: 100%;}
|
||||||
.tstable th { white-space: nowrap;}
|
.tstable th { white-space: nowrap;}
|
||||||
.tstable td { white-space: nowrap;}
|
.tstable td { white-space: nowrap;}
|
||||||
.capacity { font-size:80%; font-weight: normal;}
|
.capacity { font-size:80%; font-weight: normal;}
|
||||||
|
|
||||||
.tstable .tstype_unavail {background-color:#666;}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% origin %}
|
{% origin %}
|
||||||
|
|
||||||
|
<p class="pull-right">
|
||||||
|
<a href="{% url "ietf.meeting.views.create_timeslot" num=meeting.number %}">New timeslot</a>
|
||||||
|
·
|
||||||
|
{% if meeting.schedule %}
|
||||||
|
<a href="{% url "ietf.secr.meetings.views.rooms" meeting_id=meeting.number schedule_name=meeting.schedule.name %}">Edit rooms</a>
|
||||||
|
{% else %} {# secr app room view requires a schedule - show something for now (should try to avoid this possibility) #}
|
||||||
|
<span title="Must create meeting schedule to edit rooms">Edit rooms</span>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
|
||||||
<table class="tstable table table-striped table-compact table-bordered">
|
<p> IETF {{ meeting.number }} Meeting Agenda: Timeslots and Room Availability</p>
|
||||||
<thead>
|
<div class="timeslot-edit">
|
||||||
<tr>
|
{% if rooms|length == 0 %}
|
||||||
<th></th>
|
<p>No rooms exist for this meeting yet.</p>
|
||||||
{% for day in time_slices %}
|
{% if meeting.schedule %}
|
||||||
<th colspan="{{date_slices|colWidth:day}}">
|
<a href="{% url "ietf.secr.meetings.views.rooms" meeting_id=meeting.number schedule_name=meeting.schedule.name %}">Create rooms</a>
|
||||||
{{day|date:'D'}} ({{day}})
|
{% else %}{# provide as helpful a link we can if we don't have an official schedule #}
|
||||||
</th>
|
<a href="{% url "ietf.secr.meetings.views.view" meeting_id=meeting.number %}">Create rooms through the secr app</a>
|
||||||
{% endfor %}
|
{% endif %}
|
||||||
</tr>
|
{% else %}
|
||||||
<tr>
|
<table id="timeslot-table" class="tstable table table-striped table-compact table-bordered">
|
||||||
<th></th>
|
{% with have_no_timeslots=time_slices|length_is:0 %}
|
||||||
{% for day in time_slices %}
|
<thead>
|
||||||
{% for slot in slot_slices|lookup:day %}
|
<tr>
|
||||||
<th>
|
{% if have_no_timeslots %}
|
||||||
{{slot.time|date:'Hi'}}-{{slot.end_time|date:'Hi'}}
|
<th></th>
|
||||||
|
<th></th>
|
||||||
|
{% else %}
|
||||||
|
<th></th>
|
||||||
|
{% for day in time_slices %}
|
||||||
|
<th class="day-label"
|
||||||
|
colspan="{{date_slices|colWidth:day}}">
|
||||||
|
{{day|date:'D'}} ({{day}})
|
||||||
|
<span class="fa fa-trash delete-button"
|
||||||
|
title="Delete all on this day"
|
||||||
|
data-delete-scope="day"
|
||||||
|
data-date-id="{{ day.isoformat }}">
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
{% if have_no_timeslots %}
|
||||||
|
<th></th>
|
||||||
|
<th></th>
|
||||||
|
{% else %}
|
||||||
|
<th></th>
|
||||||
|
{% for day in time_slices %}
|
||||||
|
{% for slot in slot_slices|lookup:day %}
|
||||||
|
<th class="time-label">
|
||||||
|
{{slot.time|date:'H:i'}}-{{slot.end_time|date:'H:i'}}
|
||||||
|
<span class="fa fa-trash delete-button"
|
||||||
|
data-delete-scope="column"
|
||||||
|
data-date-id="{{ day.isoformat }}"
|
||||||
|
data-col-id="{{ day.isoformat }}T{{slot.time|date:'H:i'}}-{{slot.end_time|date:'H:i'}}"
|
||||||
|
title="Delete all in this column">
|
||||||
|
</span>
|
||||||
</th>
|
</th>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
{% endif %}
|
||||||
</thead>
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
{% for room in rooms %}
|
<tbody>
|
||||||
<tr>
|
{% for room in rooms %}
|
||||||
<th>{{room.name}}<span class='capacity'>{% if room.capacity %} ({{room.capacity}}){% endif %}</th>
|
<tr>
|
||||||
{% for day in time_slices %}
|
<th><span class="room-heading">{{room.name}}{% if room.capacity %} <span class='capacity'>({{room.capacity}})</span>{% endif %}</span></th>
|
||||||
{% for slice in date_slices|lookup:day %}
|
{% if have_no_timeslots and forloop.first %}
|
||||||
{% with ts=ts_list.popleft %}
|
<td rowspan="{{ rooms|length }}">
|
||||||
<td{% if ts %} class="tstype_{{ts.type.slug}}"{% endif %}>{% if ts %}<a href="{% url 'ietf.meeting.views.edit_timeslot_type' num=meeting.number slot_id=ts.id %}">{{ts.type.slug}}</a>{% endif %}</td>
|
<p>No timeslots exist for this meeting yet.</p>
|
||||||
{% endwith %}
|
<a href="{% url "ietf.meeting.views.create_timeslot" num=meeting.number %}">Create a timeslot.</a>
|
||||||
|
</td>
|
||||||
|
{% else %}
|
||||||
|
{% for day in time_slices %}
|
||||||
|
{% for slice in date_slices|lookup:day %}{% with cell_ts=ts_list.popleft %}
|
||||||
|
<td class="tscell {% if cell_ts|length > 1 %}timeslot-collision {% endif %}{% for ts in cell_ts %}tstype_{{ ts.type.slug }} {% endfor %}">
|
||||||
|
{% for ts in cell_ts %}
|
||||||
|
{% include 'meeting/timeslot_edit_timeslot.html' with ts=ts in_use=ts_with_any_assignments in_official_use=ts_with_official_assignments only %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endwith %}{% endfor %}
|
||||||
|
</td>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
</tbody>
|
||||||
</tr>
|
{% endwith %}
|
||||||
{% endfor %}
|
</table>
|
||||||
</table>
|
{% endif %}
|
||||||
|
|
||||||
|
{# Modal to confirm timeslot deletion #}
|
||||||
|
<div id="delete-modal" class="modal" tabindex="-1" role="dialog">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
<span class="sr-only">Close</span>
|
||||||
|
</button>
|
||||||
|
<h4 class="modal-title">Confirm Delete</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>
|
||||||
|
<span class="ts-singular">The following timeslot</span>
|
||||||
|
<span class="ts-plural"><span class="ts-count"></span> timeslots</span>
|
||||||
|
will be deleted:
|
||||||
|
</p>
|
||||||
|
<dl class="dl-horizontal">
|
||||||
|
<dt>Name</dt><dd><span class="ts-name"></span></dd>
|
||||||
|
<dt>Date</dt><dd><span class="ts-date"></span></dd>
|
||||||
|
<dt>Time</dt><dd><span class="ts-time"></span></dd>
|
||||||
|
<dt>Location</dt><dd><span class="ts-location"></span></dd>
|
||||||
|
</dl>
|
||||||
|
<p class="unofficial-use-warning">
|
||||||
|
The official schedule will not be affected, but sessions in
|
||||||
|
unofficial schedules currently assigned to
|
||||||
|
<span class="ts-singular">this timeslot</span>
|
||||||
|
<span class="ts-plural">any of these timeslots</span>
|
||||||
|
will become unassigned.
|
||||||
|
</p>
|
||||||
|
<p class="official-use-warning">
|
||||||
|
The official schedule will be affected.
|
||||||
|
Sessions currently assigned to
|
||||||
|
<span class="ts-singular">this timeslot</span>
|
||||||
|
<span class="ts-plural">any of these timeslots</span>
|
||||||
|
will become unassigned.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span class="ts-singular">Are you sure you want to delete this timeslot?</span>
|
||||||
|
<span class="ts-plural">Are you sure you want to delete these <span class="ts-count"></span> timeslots?</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||||
|
<button type="button" id="confirm-delete-button" class="btn btn-primary">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
<script type="text/javascript">
|
||||||
|
// create a namespace for local JS
|
||||||
|
timeslotEdit = (function() {
|
||||||
|
let deleteModal;
|
||||||
|
let timeslotTableBody = document.querySelector('#timeslot-table tbody');
|
||||||
|
|
||||||
|
function initializeDeleteModal() {
|
||||||
|
deleteModal = jQuery('#delete-modal');
|
||||||
|
deleteModal.eltsToDelete = null; // PK of TimeSlot that modal 'Delete' button should delete
|
||||||
|
let spans = deleteModal.find('span');
|
||||||
|
deleteModal.elts = {
|
||||||
|
unofficialUseWarning: deleteModal.find('.unofficial-use-warning'),
|
||||||
|
officialUseWarning: deleteModal.find('.official-use-warning'),
|
||||||
|
timeslotNameSpans: spans.filter('.ts-name'),
|
||||||
|
timeslotDateSpans: spans.filter('.ts-date'),
|
||||||
|
timeslotTimeSpans: spans.filter('.ts-time'),
|
||||||
|
timeslotLocSpans: spans.filter('.ts-location'),
|
||||||
|
timeslotCountSpans: spans.filter('.ts-count'),
|
||||||
|
pluralSpans: spans.filter('.ts-plural'),
|
||||||
|
singularSpans: spans.filter('.ts-singular')
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('confirm-delete-button').addEventListener(
|
||||||
|
'click',
|
||||||
|
() => timeslotEdit.handleDeleteButtonClick()
|
||||||
|
);
|
||||||
|
|
||||||
|
function uniqueArray(a) {
|
||||||
|
let s = new Set();
|
||||||
|
a.forEach(item => s.add(item));
|
||||||
|
return Array.from(s);
|
||||||
|
}
|
||||||
|
deleteModal.openModal = function (eltsToDelete) {
|
||||||
|
let eltArray = Array.from(eltsToDelete); // make sure this is an array
|
||||||
|
|
||||||
|
if (eltArray.length > 1) {
|
||||||
|
deleteModal.elts.pluralSpans.show();
|
||||||
|
deleteModal.elts.singularSpans.hide();
|
||||||
|
} else {
|
||||||
|
deleteModal.elts.pluralSpans.hide();
|
||||||
|
deleteModal.elts.singularSpans.show();
|
||||||
|
}
|
||||||
|
deleteModal.elts.timeslotCountSpans.text(String(eltArray.length));
|
||||||
|
|
||||||
|
let names = uniqueArray(eltArray.map(elt => elt.dataset.timeslotName));
|
||||||
|
if (names.length === 1) {
|
||||||
|
names = names[0];
|
||||||
|
} else {
|
||||||
|
names.sort();
|
||||||
|
names = names.join(', ');
|
||||||
|
}
|
||||||
|
deleteModal.elts.timeslotNameSpans.text(names);
|
||||||
|
|
||||||
|
let dates = uniqueArray(eltArray.map(elt => elt.dataset.timeslotDate));
|
||||||
|
if (dates.length === 1) {
|
||||||
|
dates = dates[0];
|
||||||
|
} else {
|
||||||
|
dates = 'Multiple';
|
||||||
|
}
|
||||||
|
deleteModal.elts.timeslotDateSpans.text(dates);
|
||||||
|
|
||||||
|
let times = uniqueArray(eltArray.map(elt => elt.dataset.timeslotTime));
|
||||||
|
if (times.length === 1) {
|
||||||
|
times = times[0];
|
||||||
|
} else {
|
||||||
|
times = 'Multiple';
|
||||||
|
}
|
||||||
|
deleteModal.elts.timeslotTimeSpans.text(times);
|
||||||
|
|
||||||
|
let locs = uniqueArray(eltArray.map(elt => elt.dataset.timeslotLocation));
|
||||||
|
if (locs.length === 1) {
|
||||||
|
locs = locs[0];
|
||||||
|
} else {
|
||||||
|
locs = 'Multiple';
|
||||||
|
}
|
||||||
|
deleteModal.elts.timeslotLocSpans.text(locs);
|
||||||
|
|
||||||
|
// Check whether any of the elts are used in official / unofficial schedules
|
||||||
|
let unofficialUse = eltArray.some(elt => elt.dataset.unofficialUse === 'true');
|
||||||
|
let officialUse = eltArray.some(elt => elt.dataset.officialUse === 'true');
|
||||||
|
deleteModal.elts.unofficialUseWarning.hide();
|
||||||
|
deleteModal.elts.officialUseWarning.hide();
|
||||||
|
if (officialUse) {
|
||||||
|
deleteModal.elts.officialUseWarning.show();
|
||||||
|
} else if (unofficialUse) {
|
||||||
|
deleteModal.elts.unofficialUseWarning.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteModal.eltsToDelete = eltsToDelete;
|
||||||
|
deleteModal.modal('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle deleting a single timeslot
|
||||||
|
*
|
||||||
|
* clicked arg is the clicked element, which must be a child of the timeslot element
|
||||||
|
*/
|
||||||
|
function deleteSingleTimeSlot(clicked) {
|
||||||
|
deleteModal.openModal([clicked.closest('.timeslot')]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle deleting an entire day worth of timeslots
|
||||||
|
*
|
||||||
|
* clicked arg is the clicked element, which must be a child of the day header element
|
||||||
|
*/
|
||||||
|
function deleteDay(clicked) {
|
||||||
|
// Find all timeslots for this day
|
||||||
|
let dateId = clicked.dataset.dateId;
|
||||||
|
let timeslots = timeslotTableBody.querySelectorAll(
|
||||||
|
':scope .timeslot[data-date-id="' + dateId + '"]' // :scope prevents picking up results outside table body
|
||||||
|
);
|
||||||
|
if (timeslots.length > 0) {
|
||||||
|
deleteModal.openModal(timeslots);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle deleting an entire column worth of timeslots
|
||||||
|
*
|
||||||
|
* clicked arg is the clicked element, which must be a child of the column header element
|
||||||
|
*/
|
||||||
|
function deleteColumn(clicked) {
|
||||||
|
let colId = clicked.dataset.colId;
|
||||||
|
let timeslots = timeslotTableBody.querySelectorAll(
|
||||||
|
':scope .timeslot[data-col-id="' + colId + '"]' // :scope prevents picking up results outside table body
|
||||||
|
);
|
||||||
|
if (timeslots.length > 0) {
|
||||||
|
deleteModal.openModal(timeslots);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event handler for clicks on the timeslot table
|
||||||
|
*
|
||||||
|
* Handles clicks on all the delete buttons to avoid large numbers of event handlers.
|
||||||
|
*/
|
||||||
|
document.getElementById('timeslot-table').addEventListener('click', function(event) {
|
||||||
|
let clicked = event.target; // find out what was clicked
|
||||||
|
if (clicked.dataset.deleteScope) {
|
||||||
|
switch (clicked.dataset.deleteScope) {
|
||||||
|
case 'timeslot':
|
||||||
|
deleteSingleTimeSlot(clicked)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'column':
|
||||||
|
deleteColumn(clicked)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'day':
|
||||||
|
deleteDay(clicked)
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error('Unexpected deleteScope "' + clicked.dataset.deleteScope + '"')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update timeslot classes when DOM changes
|
||||||
|
function tstableObserveCallback(mutationList) {
|
||||||
|
mutationList.forEach(mutation => {
|
||||||
|
if (mutation.type === 'childList' && mutation.target.classList.contains('tscell')) {
|
||||||
|
const tscell = mutation.target;
|
||||||
|
// mark collisions
|
||||||
|
if (tscell.getElementsByClassName('timeslot').length > 1) {
|
||||||
|
tscell.classList.add('timeslot-collision');
|
||||||
|
} else {
|
||||||
|
tscell.classList.remove('timeslot-collision');
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove timeslot type classes for any removed timeslots
|
||||||
|
mutation.removedNodes.forEach(node => {
|
||||||
|
if (node.classList.contains('timeslot') && node.dataset.timeslotType) {
|
||||||
|
tscell.classList.remove('tstype_' + node.dataset.timeslotType);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// now add timeslot type classes for any remaining timeslots
|
||||||
|
Array.from(tscell.getElementsByClassName('timeslot')).forEach(elt => {
|
||||||
|
if (elt.dataset.timeslotType) {
|
||||||
|
tscell.classList.add('tstype_' + elt.dataset.timeslotType);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeTsTableObserver() {
|
||||||
|
const observer = new MutationObserver(tstableObserveCallback);
|
||||||
|
observer.observe(timeslotTableBody, { childList: true, subtree: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('load', function (event) {
|
||||||
|
initializeTsTableObserver();
|
||||||
|
initializeDeleteModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
function removeTimeslotElement(elt) {
|
||||||
|
if (elt.parentNode) {
|
||||||
|
elt.parentNode.removeChild(elt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteButtonClick() {
|
||||||
|
if (!deleteModal || !deleteModal.eltsToDelete) {
|
||||||
|
return; // do nothing if not yet initialized
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeslotElts = Array.from(deleteModal.eltsToDelete); // make own copy as Array so we have .map()
|
||||||
|
ajaxDeleteTimeSlot(timeslotElts.map(elt => elt.dataset.timeslotPk))
|
||||||
|
.error(function(jqXHR, textStatus) {
|
||||||
|
displayError('Error deleting timeslot: ' + jqXHR.responseText)
|
||||||
|
})
|
||||||
|
.done(function () {timeslotElts.forEach(tse => tse.parentNode.removeChild(tse))})
|
||||||
|
.always(function () {deleteModal.modal('hide')});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make an AJAX request to delete a TimeSlot
|
||||||
|
*
|
||||||
|
* @param pkArray array of PKs of timeslots to delete
|
||||||
|
* @returns jqXHR object corresponding to jQuery ajax request
|
||||||
|
*/
|
||||||
|
function ajaxDeleteTimeSlot(pkArray) {
|
||||||
|
return jQuery.ajax({
|
||||||
|
method: 'post',
|
||||||
|
timeout: 5 * 1000,
|
||||||
|
data: {
|
||||||
|
action: 'delete',
|
||||||
|
slot_id: pkArray.join(',')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayError(msg) {
|
||||||
|
window.alert(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// export callable methods
|
||||||
|
return {
|
||||||
|
handleDeleteButtonClick: handleDeleteButtonClick,
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
26
ietf/templates/meeting/timeslot_edit_timeslot.html
Normal file
26
ietf/templates/meeting/timeslot_edit_timeslot.html
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<div id="timeslot{{ ts.pk }}"
|
||||||
|
class="timeslot {% if ts in in_official_use %}in-official-use{% elif ts in in_use %}in-unofficial-use{% endif %}"
|
||||||
|
data-timeslot-pk="{{ ts.pk }}"
|
||||||
|
data-date-id="{{ ts.time.date.isoformat }}"{# used for identifying day/col contents #}
|
||||||
|
data-col-id="{{ ts.time.date.isoformat }}T{{ ts.time|date:"H:i" }}-{{ ts.end_time|date:"H:i" }}" {# used for identifying column contents #}
|
||||||
|
data-timeslot-name="{{ ts.name }}"
|
||||||
|
data-timeslot-type="{{ ts.type.slug }}"
|
||||||
|
data-timeslot-location="{{ ts.location.name }}"
|
||||||
|
data-timeslot-date="{{ ts.time|date:"l (Y-m-d)" }}"
|
||||||
|
data-timeslot-time="{{ ts.time|date:"H:i" }}-{{ ts.end_time|date:"H:i" }}"
|
||||||
|
data-unofficial-use="{% if ts in in_use and ts not in in_official_use %}true{% else %}false{% endif %}"
|
||||||
|
data-official-use="{% if ts in in_official_use %}true{% else %}false{% endif %}">
|
||||||
|
<div class="ts-name">
|
||||||
|
<span
|
||||||
|
title="{% if ts in in_official_use %}Used in official schedule{% elif ts in in_use %}Used in unofficial schedule{% else %}Unused{% endif %}">
|
||||||
|
{{ ts.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="timeslot-buttons">
|
||||||
|
<a href="{% url 'ietf.meeting.views.edit_timeslot' num=ts.meeting.number slot_id=ts.id %}">
|
||||||
|
<span class="fa fa-pencil-square-o"></span>
|
||||||
|
</a>
|
||||||
|
<span class="fa fa-trash delete-button" data-delete-scope="timeslot" title="Delete this timeslot"></span>
|
||||||
|
</div>
|
||||||
|
<div class="ts-type">{{ ts.type }}</div>
|
||||||
|
</div>
|
|
@ -63,22 +63,67 @@ def yyyymmdd_to_strftime_format(fmt):
|
||||||
remaining = remaining[1:]
|
remaining = remaining[1:]
|
||||||
return res
|
return res
|
||||||
|
|
||||||
class DatepickerDateField(forms.DateField):
|
|
||||||
"""DateField with some glue for triggering JS Bootstrap datepicker."""
|
|
||||||
|
|
||||||
def __init__(self, date_format, picker_settings={}, *args, **kwargs):
|
class DatepickerMedia:
|
||||||
|
"""Media definitions needed for Datepicker widgets"""
|
||||||
|
css = dict(all=('bootstrap-datepicker/css/bootstrap-datepicker3.min.css',))
|
||||||
|
js = ('bootstrap-datepicker/js/bootstrap-datepicker.min.js',)
|
||||||
|
|
||||||
|
|
||||||
|
class DatepickerDateInput(forms.DateInput):
|
||||||
|
"""DateInput that uses the Bootstrap datepicker
|
||||||
|
|
||||||
|
The format must be in the Bootstrap datepicker format (yyyy-mm-dd, e.g.), not the
|
||||||
|
strftime format. The picker_settings argument is a dict of parameters for the datepicker,
|
||||||
|
converting their camelCase names to dash-separated lowercase names and omitting the
|
||||||
|
'data-date' prefix to the key.
|
||||||
|
"""
|
||||||
|
Media = DatepickerMedia
|
||||||
|
|
||||||
|
def __init__(self, attrs=None, date_format=None, picker_settings=None):
|
||||||
|
super().__init__(
|
||||||
|
attrs,
|
||||||
|
yyyymmdd_to_strftime_format(date_format),
|
||||||
|
)
|
||||||
|
self.attrs.setdefault('data-provide', 'datepicker')
|
||||||
|
self.attrs.setdefault('data-date-format', date_format)
|
||||||
|
self.attrs.setdefault("data-date-autoclose", "1")
|
||||||
|
self.attrs.setdefault('placeholder', date_format)
|
||||||
|
if picker_settings is not None:
|
||||||
|
for k, v in picker_settings.items():
|
||||||
|
self.attrs['data-date-{}'.format(k)] = v
|
||||||
|
|
||||||
|
|
||||||
|
class DatepickerSplitDateTimeWidget(forms.SplitDateTimeWidget):
|
||||||
|
"""Split datetime widget using Bootstrap datepicker
|
||||||
|
|
||||||
|
The format must be in the Bootstrap datepicker format (yyyy-mm-dd, e.g.), not the
|
||||||
|
strftime format. The picker_settings argument is a dict of parameters for the datepicker,
|
||||||
|
converting their camelCase names to dash-separated lowercase names and omitting the
|
||||||
|
'data-date' prefix to the key.
|
||||||
|
"""
|
||||||
|
Media = DatepickerMedia
|
||||||
|
|
||||||
|
def __init__(self, *, date_format='yyyy-mm-dd', picker_settings=None, **kwargs):
|
||||||
|
date_attrs = kwargs.setdefault('date_attrs', dict())
|
||||||
|
date_attrs.setdefault("data-provide", "datepicker")
|
||||||
|
date_attrs.setdefault("data-date-format", date_format)
|
||||||
|
date_attrs.setdefault("data-date-autoclose", "1")
|
||||||
|
date_attrs.setdefault("placeholder", date_format)
|
||||||
|
if picker_settings is not None:
|
||||||
|
for k, v in picker_settings.items():
|
||||||
|
date_attrs['data-date-{}'.format(k)] = v
|
||||||
|
super().__init__(date_format=yyyymmdd_to_strftime_format(date_format), **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class DatepickerDateField(forms.DateField):
|
||||||
|
"""DateField with some glue for triggering JS Bootstrap datepicker"""
|
||||||
|
def __init__(self, date_format, picker_settings=None, *args, **kwargs):
|
||||||
strftime_format = yyyymmdd_to_strftime_format(date_format)
|
strftime_format = yyyymmdd_to_strftime_format(date_format)
|
||||||
kwargs["input_formats"] = [strftime_format]
|
kwargs["input_formats"] = [strftime_format]
|
||||||
kwargs["widget"] = forms.DateInput(format=strftime_format)
|
kwargs["widget"] = DatepickerDateInput(dict(placeholder=date_format), date_format, picker_settings)
|
||||||
super(DatepickerDateField, self).__init__(*args, **kwargs)
|
super(DatepickerDateField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.widget.attrs["data-provide"] = "datepicker"
|
|
||||||
self.widget.attrs["data-date-format"] = date_format
|
|
||||||
if "placeholder" not in self.widget.attrs:
|
|
||||||
self.widget.attrs["placeholder"] = date_format
|
|
||||||
for k, v in picker_settings.items():
|
|
||||||
self.widget.attrs["data-date-%s" % k] = v
|
|
||||||
|
|
||||||
|
|
||||||
# This accepts any ordered combination of labelled days, hours, minutes, seconds
|
# This accepts any ordered combination of labelled days, hours, minutes, seconds
|
||||||
ext_duration_re = re.compile(
|
ext_duration_re = re.compile(
|
||||||
|
|
|
@ -8,8 +8,9 @@ from pyquery import PyQuery
|
||||||
from urllib.parse import urlparse, urlsplit, urlunsplit
|
from urllib.parse import urlparse, urlsplit, urlunsplit
|
||||||
|
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||||
from django.core.validators import RegexValidator, URLValidator, EmailValidator, _lazy_re_compile, BaseValidator
|
from django.core.validators import RegexValidator, URLValidator, EmailValidator, _lazy_re_compile, BaseValidator
|
||||||
from django.template.defaultfilters import filesizeformat
|
from django.template.defaultfilters import filesizeformat
|
||||||
from django.utils.deconstruct import deconstructible
|
from django.utils.deconstruct import deconstructible
|
||||||
|
@ -248,3 +249,38 @@ class MaxImageSizeValidator(BaseValidator):
|
||||||
return x.width, x.height
|
return x.width, x.height
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
return 0, 0 # don't fail if the image is missing
|
return 0, 0 # don't fail if the image is missing
|
||||||
|
|
||||||
|
|
||||||
|
@deconstructible
|
||||||
|
class JSONForeignKeyListValidator:
|
||||||
|
"""Validate that a JSONField is a list of valid foreign key references"""
|
||||||
|
def __init__(self, model_class, field_name='pk'):
|
||||||
|
# model_class can be a class or a string like "app_name.ModelName"
|
||||||
|
self._model = model_class
|
||||||
|
self.field_name = field_name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def model(self):
|
||||||
|
# Lazy model lookup is needed because the validator's __init__() is usually
|
||||||
|
# called while Django is still setting up. It's likely that the model is not
|
||||||
|
# registered at that point, otherwise the caller would not have used the
|
||||||
|
# string form to refer to it.
|
||||||
|
if isinstance(self._model, str):
|
||||||
|
self._model = apps.get_model(self._model)
|
||||||
|
return self._model
|
||||||
|
|
||||||
|
def __call__(self, value):
|
||||||
|
for val in value:
|
||||||
|
try:
|
||||||
|
self.model.objects.get(**{self.field_name: val})
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
raise ValidationError(
|
||||||
|
f'No {self.model.__name__} with {self.field_name} == "{val}" exists.'
|
||||||
|
)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (
|
||||||
|
isinstance(other, self.__class__)
|
||||||
|
and (self.model == other.model)
|
||||||
|
and (self.field_name == other.field_name)
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in a new issue