Merged in ^/personal/jennifer/7.39.1.dev2 from jennifer@painless-security.com:
Create dev branch for session purpose work (from revision [19414]) Snapshot of dev work to add session purpose annotation (from revision [19415]) Allow non-WG-like groups to request additional sessions/durations and bypass approval (from revision [19424]) Add 'closed' session purpose, assign purposes for nomcom groups, and update schedule editor to enforce timeslot type and allow blurring sessions by purpose (from revision [19427]) Add management command to set up timeslots/sessions for testing/demoing 'purpose' field (from revision [19430]) Update session purposes and group type -> purpose map to match notes page, change 'session' purpose to 'regular' (from revision [19433]) Redirect edit_schedule urls to edit_meeting_schedule view (from revision [19434]) Allow hiding/blurring sessions and timeslots based on TimeSlotType in the schedule editor (from revision [19438]) Disable session purpose/timeslot type hiding on schedule editor when only 0 or 1 options (from revision [19439]) Improvements to the timeslot and schedule editors (move new toggles to modals, handle overflowing session names, fix timeslot editor scrolling, add buttons to quickly create single timeslot, accept trailing slash on edit URL) (from revision [19449]) Update purpose/types after discussions, add on_agenda Session field, prevent session requests for groups with no allowed purpose, handle addition fields in session request, fix editing session requests, add session edit form/access from schedule editor, eliminate TimeSlotTypeName 'private' field, add server-side timeslot type filtering to schedule editor (from revision [19549]) Eliminate the officehours timeslot type, update/renumber migrations, mark offagenda/reserved TimeSlotTypeNames as not used, add a 'none' SessionPurposeName and disallow null, update agenda filter keywords/filter helpers, fix broken tests and general debugging (from revision [19550]) Tear out old meeting schedule editor and related code (from revision [19551]) Fix merge errors in preceding commits (from revision [19556]) Add missing ifSeleniumEnabled guard for meeting.tests_js.EditTimeslotsTests (from revision [19592]) Remove 'before' version of migrations that were renumbered before merge. (from revision [19595]) - Legacy-Id: 19596 Note: SVN reference [19415] has been migrated to Git commit1054f90873
Note: SVN reference [19424] has been migrated to Git commit5318081608
Note: SVN reference [19427] has been migrated to Git commit173e438aee
Note: SVN reference [19430] has been migrated to Git commit7a2530a0a6
Note: SVN reference [19433] has been migrated to Git commit3be50d6e39
Note: SVN reference [19434] has been migrated to Git commit3e3d681e5f
Note: SVN reference [19438] has been migrated to Git commitb6ac3d4b1d
Note: SVN reference [19439] has been migrated to Git commit446ac7a47e
Note: SVN reference [19449] has been migrated to Git commit5cbe402036
Note: SVN reference [19549] has been migrated to Git commit3dfce7b850
Note: SVN reference [19550] has been migrated to Git commit7b35c09c40
Note: SVN reference [19551] has been migrated to Git commitd7f20342b6
Note: SVN reference [19556] has been migrated to Git commit2b1864f5a0
Note: SVN reference [19592] has been migrated to Git commitc3f28eeedb
Note: SVN reference [19595] has been migrated to Git commit83f5c7e9b6
This commit is contained in:
commit
fd5595039c
|
@ -1,11 +1,13 @@
|
||||||
# -*- conf-mode -*-
|
# -*- conf-mode -*-
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
/personal/lars/7.39.1.dev0@19495 # Hold the modal 'give us your xml' poking until bibxml service is stable
|
/personal/lars/7.39.1.dev0@19495 # Hold the modal 'give us your xml' poking until bibxml service is stable
|
||||||
# and maybe until we have rendered previews.
|
# and maybe until we have rendered previews.
|
||||||
|
|
||||||
# Everyting below this line is OBE
|
# Everyting below this line is OBE
|
||||||
|
|
||||||
|
/personal/rjs/7.39.1.dev1@19554 # Optimization wasn't measured correctly
|
||||||
/personal/rjs/7.36.1.dev0@19318 # Folded this into r19336
|
/personal/rjs/7.36.1.dev0@19318 # Folded this into r19336
|
||||||
/personal/rjs/7.36.1.dev0@19302 # Handled this in an earlier merge
|
/personal/rjs/7.36.1.dev0@19302 # Handled this in an earlier merge
|
||||||
|
|
||||||
|
|
|
@ -631,6 +631,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-112 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-112 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'), ('112', 'acme office hours')]
|
||||||
|
>>> any(should_show_agenda_session_buttons(factory(*tc)) for tc in test_cases)
|
||||||
|
False
|
||||||
|
>>> test_cases = [('interim-2020-acme-113', 'acme'), ('113', 'acme'), ('150', 'acme'), ('105', 'acme'),]
|
||||||
|
>>> test_cases.extend([('112', 'acme'), ('interim-2020-acme-113', 'acme office hours')])
|
||||||
|
>>> test_cases.extend([('113', '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):
|
||||||
|
|
|
@ -40,7 +40,7 @@ from ietf.doc.utils import create_ballot_if_not_open, uppercase_std_abbreviated_
|
||||||
from ietf.group.models import Group
|
from ietf.group.models import Group
|
||||||
from ietf.group.factories import GroupFactory, RoleFactory
|
from ietf.group.factories import GroupFactory, RoleFactory
|
||||||
from ietf.ipr.factories import HolderIprDisclosureFactory
|
from ietf.ipr.factories import HolderIprDisclosureFactory
|
||||||
from ietf.meeting.models import Meeting, Session, SessionPresentation, SchedulingEvent
|
from ietf.meeting.models import Meeting, SessionPresentation, SchedulingEvent
|
||||||
from ietf.meeting.factories import ( MeetingFactory, SessionFactory, SessionPresentationFactory,
|
from ietf.meeting.factories import ( MeetingFactory, SessionFactory, SessionPresentationFactory,
|
||||||
ProceedingsMaterialFactory )
|
ProceedingsMaterialFactory )
|
||||||
|
|
||||||
|
@ -1465,12 +1465,12 @@ class DocTestCase(TestCase):
|
||||||
)
|
)
|
||||||
doc.set_state(State.objects.get(type="slides", slug="active"))
|
doc.set_state(State.objects.get(type="slides", slug="active"))
|
||||||
|
|
||||||
session = Session.objects.create(
|
session = SessionFactory(
|
||||||
name = "session-72-mars-1",
|
name = "session-72-mars-1",
|
||||||
meeting = Meeting.objects.get(number='72'),
|
meeting = Meeting.objects.get(number='72'),
|
||||||
group = Group.objects.get(acronym='mars'),
|
group = Group.objects.get(acronym='mars'),
|
||||||
modified = datetime.datetime.now(),
|
modified = datetime.datetime.now(),
|
||||||
type_id = 'regular',
|
add_to_schedule=False,
|
||||||
)
|
)
|
||||||
SchedulingEvent.objects.create(
|
SchedulingEvent.objects.create(
|
||||||
session=session,
|
session=session,
|
||||||
|
|
|
@ -18,8 +18,8 @@ from django.urls import reverse as urlreverse
|
||||||
from ietf.doc.models import Document, State, DocAlias, NewRevisionDocEvent
|
from ietf.doc.models import Document, State, DocAlias, NewRevisionDocEvent
|
||||||
from ietf.group.factories import RoleFactory
|
from ietf.group.factories import RoleFactory
|
||||||
from ietf.group.models import Group
|
from ietf.group.models import Group
|
||||||
from ietf.meeting.factories import MeetingFactory
|
from ietf.meeting.factories import MeetingFactory, SessionFactory
|
||||||
from ietf.meeting.models import Meeting, Session, SessionPresentation, SchedulingEvent
|
from ietf.meeting.models import Meeting, SessionPresentation, SchedulingEvent
|
||||||
from ietf.name.models import SessionStatusName
|
from ietf.name.models import SessionStatusName
|
||||||
from ietf.person.models import Person
|
from ietf.person.models import Person
|
||||||
from ietf.utils.test_utils import TestCase, login_testing_unauthorized
|
from ietf.utils.test_utils import TestCase, login_testing_unauthorized
|
||||||
|
@ -151,12 +151,11 @@ class GroupMaterialTests(TestCase):
|
||||||
def test_revise(self):
|
def test_revise(self):
|
||||||
doc = self.create_slides()
|
doc = self.create_slides()
|
||||||
|
|
||||||
session = Session.objects.create(
|
session = SessionFactory(
|
||||||
name = "session-42-mars-1",
|
name = "session-42-mars-1",
|
||||||
meeting = Meeting.objects.get(number='42'),
|
meeting = Meeting.objects.get(number='42'),
|
||||||
group = Group.objects.get(acronym='mars'),
|
group = Group.objects.get(acronym='mars'),
|
||||||
modified = datetime.datetime.now(),
|
modified = datetime.datetime.now(),
|
||||||
type_id='regular',
|
|
||||||
)
|
)
|
||||||
SchedulingEvent.objects.create(
|
SchedulingEvent.objects.create(
|
||||||
session=session,
|
session=session,
|
||||||
|
|
|
@ -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', '0049_auto_20211019_1136'),
|
||||||
|
]
|
||||||
|
|
||||||
|
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', '0050_groupfeatures_agenda_filter_type'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(forward, reverse),
|
||||||
|
]
|
24
ietf/group/migrations/0052_groupfeatures_session_purposes.py
Normal file
24
ietf/group/migrations/0052_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', '0051_populate_groupfeatures_agenda_filter_type'),
|
||||||
|
('name', '0034_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,64 @@
|
||||||
|
# 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(
|
||||||
|
adhoc=['presentation'],
|
||||||
|
adm=['closed_meeting', 'officehours'],
|
||||||
|
ag=['regular'],
|
||||||
|
area=['regular'],
|
||||||
|
dir=['open_meeting', 'presentation', 'regular', 'social', 'tutorial'],
|
||||||
|
iab=['closed_meeting', 'regular'],
|
||||||
|
iabasg=['closed_meeting', 'officehours', 'open_meeting'],
|
||||||
|
iana=['officehours'],
|
||||||
|
iesg=['closed_meeting', 'open_meeting'],
|
||||||
|
ietf=['admin', 'plenary', 'presentation', 'social'],
|
||||||
|
irtf=[],
|
||||||
|
ise=['officehours'],
|
||||||
|
isoc=['officehours', 'open_meeting', 'presentation'],
|
||||||
|
nomcom=['closed_meeting', 'officehours'],
|
||||||
|
program=['regular', 'tutorial'],
|
||||||
|
rag=['regular'],
|
||||||
|
review=['open_meeting', 'social'],
|
||||||
|
rfcedtyp=['officehours'],
|
||||||
|
rg=['regular'],
|
||||||
|
team=['coding', 'presentation', 'social', 'tutorial'],
|
||||||
|
wg=['regular'],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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', '0052_groupfeatures_session_purposes'),
|
||||||
|
('name', '0035_populate_sessionpurposename'),
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(forward, reverse),
|
||||||
|
]
|
|
@ -8,25 +8,23 @@ import jsonfield
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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.db import IETFJSONField
|
from ietf.utils.db import IETFJSONField
|
||||||
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):
|
||||||
|
@ -167,30 +165,6 @@ class Group(GroupInfo):
|
||||||
def bg_color(self):
|
def bg_color(self):
|
||||||
return bg_group_colors[self.upcase_acronym]
|
return bg_group_colors[self.upcase_acronym]
|
||||||
|
|
||||||
def json_url(self):
|
|
||||||
return "/group/%s.json" % (self.acronym,)
|
|
||||||
|
|
||||||
def json_dict(self, host_scheme):
|
|
||||||
group1= dict()
|
|
||||||
group1['href'] = urljoin(host_scheme, self.json_url())
|
|
||||||
group1['acronym'] = self.acronym
|
|
||||||
group1['name'] = self.name
|
|
||||||
group1['state'] = self.state.slug
|
|
||||||
group1['type'] = self.type.slug
|
|
||||||
if self.parent is not None:
|
|
||||||
group1['parent_href'] = urljoin(host_scheme, self.parent.json_url())
|
|
||||||
# uncomment when people URL handle is created
|
|
||||||
try:
|
|
||||||
if self.ad_role() is not None:
|
|
||||||
group1['ad_href'] = urljoin(host_scheme, self.ad_role().person.json_url())
|
|
||||||
except Person.DoesNotExist:
|
|
||||||
pass
|
|
||||||
group1['list_email'] = self.list_email
|
|
||||||
group1['list_subscribe'] = self.list_subscribe
|
|
||||||
group1['list_archive'] = self.list_archive
|
|
||||||
group1['comments'] = self.comments
|
|
||||||
return group1
|
|
||||||
|
|
||||||
def liaison_approvers(self):
|
def liaison_approvers(self):
|
||||||
'''Returns roles that have liaison statement approval authority for group'''
|
'''Returns roles that have liaison statement approval authority for group'''
|
||||||
|
|
||||||
|
@ -248,6 +222,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()
|
||||||
|
@ -275,6 +250,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)
|
||||||
|
@ -289,6 +265,9 @@ class GroupFeatures(models.Model):
|
||||||
matman_roles = IETFJSONField(max_length=128, accepted_empty_values=[[], {}], blank=False, default=["ad","chair","delegate","secr"])
|
matman_roles = IETFJSONField(max_length=128, accepted_empty_values=[[], {}], blank=False, default=["ad","chair","delegate","secr"])
|
||||||
role_order = IETFJSONField(max_length=128, accepted_empty_values=[[], {}], blank=False, default=["chair","secr","member"],
|
role_order = IETFJSONField(max_length=128, accepted_empty_values=[[], {}], 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 = IETFJSONField(max_length=256, accepted_empty_values=[[], {}], blank=False, default=[],
|
||||||
|
help_text="Allowed session purposes for this group type",
|
||||||
|
validators=[JSONForeignKeyListValidator(SessionPurposeName)])
|
||||||
|
|
||||||
|
|
||||||
class GroupHistory(GroupInfo):
|
class GroupHistory(GroupInfo):
|
||||||
|
|
|
@ -54,7 +54,6 @@ info_detail_urls = [
|
||||||
group_urls = [
|
group_urls = [
|
||||||
url(r'^$', views.active_groups),
|
url(r'^$', views.active_groups),
|
||||||
url(r'^groupmenu.json', views.group_menu_data, None, 'ietf.group.views.group_menu_data'),
|
url(r'^groupmenu.json', views.group_menu_data, None, 'ietf.group.views.group_menu_data'),
|
||||||
url(r'^%(acronym)s.json$' % settings.URL_REGEXPS, views.group_json),
|
|
||||||
url(r'^chartering/$', views.chartering_groups),
|
url(r'^chartering/$', views.chartering_groups),
|
||||||
url(r'^chartering/create/(?P<group_type>(wg|rg))/$', views.edit, {'action': "charter"}),
|
url(r'^chartering/create/(?P<group_type>(wg|rg))/$', views.edit, {'action': "charter"}),
|
||||||
url(r'^concluded/$', views.concluded_groups),
|
url(r'^concluded/$', views.concluded_groups),
|
||||||
|
|
|
@ -38,7 +38,6 @@ import copy
|
||||||
import datetime
|
import datetime
|
||||||
import itertools
|
import itertools
|
||||||
import io
|
import io
|
||||||
import json
|
|
||||||
import markdown
|
import markdown
|
||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
|
@ -1299,13 +1298,6 @@ def stream_edit(request, acronym):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def group_json(request, acronym):
|
|
||||||
group = get_object_or_404(Group, acronym=acronym)
|
|
||||||
|
|
||||||
return HttpResponse(json.dumps(group.json_dict(request.build_absolute_uri('/')),
|
|
||||||
sort_keys=True, indent=2),
|
|
||||||
content_type="application/json")
|
|
||||||
|
|
||||||
@cache_control(public=True, max_age=30*60)
|
@cache_control(public=True, max_age=30*60)
|
||||||
@cache_page(30 * 60)
|
@cache_page(30 * 60)
|
||||||
def group_menu_data(request):
|
def group_menu_data(request):
|
||||||
|
|
|
@ -1,629 +0,0 @@
|
||||||
# Copyright The IETF Trust 2013-2019, All Rights Reserved
|
|
||||||
import json
|
|
||||||
|
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
|
||||||
from django.http import HttpResponse
|
|
||||||
from django.http import QueryDict
|
|
||||||
from django.http import Http404
|
|
||||||
from django.views.decorators.http import require_POST
|
|
||||||
|
|
||||||
from ietf.ietfauth.utils import role_required, has_role
|
|
||||||
from ietf.meeting.helpers import get_meeting, get_schedule, schedule_permissions, get_person_by_email, get_schedule_by_name
|
|
||||||
from ietf.meeting.models import TimeSlot, Session, Schedule, Room, Constraint, SchedTimeSessAssignment, ResourceAssociation
|
|
||||||
from ietf.meeting.views import edit_timeslots, edit_schedule
|
|
||||||
|
|
||||||
import debug # pyflakes:ignore
|
|
||||||
|
|
||||||
def is_truthy_enough(value):
|
|
||||||
return not (value == "0" or value == 0 or value=="false")
|
|
||||||
|
|
||||||
# look up a schedule by number, owner and schedule name, returning an API error if it can not be found
|
|
||||||
def get_meeting_schedule(num, owner, name):
|
|
||||||
meeting = get_meeting(num)
|
|
||||||
person = get_person_by_email(owner)
|
|
||||||
schedule = get_schedule_by_name(meeting, person, name)
|
|
||||||
|
|
||||||
if schedule is None or person is None or meeting is None:
|
|
||||||
meeting_pk = 0
|
|
||||||
person_pk = 0
|
|
||||||
schedule_pk =0
|
|
||||||
# to make diagnostics more meaningful, log what we found
|
|
||||||
if meeting:
|
|
||||||
meeting_pk = meeting.pk
|
|
||||||
if person:
|
|
||||||
person_pk = person.pk
|
|
||||||
if schedule:
|
|
||||||
schedule_pk=schedule.pk
|
|
||||||
return HttpResponse(json.dumps({'error' : 'invalid meeting=%s/person=%s/schedule=%s' % (num,owner,name),
|
|
||||||
'meeting': meeting_pk,
|
|
||||||
'person': person_pk,
|
|
||||||
'schedule': schedule_pk}),
|
|
||||||
content_type="application/json",
|
|
||||||
status=404);
|
|
||||||
return meeting, person, schedule
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# should asking if an schedule is read-only require any kind of permission?
|
|
||||||
def schedule_permission_api(request, num, owner, name):
|
|
||||||
meeting = get_meeting(num)
|
|
||||||
person = get_person_by_email(owner)
|
|
||||||
schedule = get_schedule_by_name(meeting, person, name)
|
|
||||||
|
|
||||||
save_perm = False
|
|
||||||
secretariat = False
|
|
||||||
cansee = False
|
|
||||||
canedit = False
|
|
||||||
owner_href = ""
|
|
||||||
|
|
||||||
if schedule is not None:
|
|
||||||
cansee,canedit,secretariat = schedule_permissions(meeting, schedule, request.user)
|
|
||||||
owner_href = request.build_absolute_uri(schedule.owner.json_url())
|
|
||||||
|
|
||||||
if has_role(request.user, "Area Director") or secretariat:
|
|
||||||
save_perm = True
|
|
||||||
|
|
||||||
return HttpResponse(json.dumps({'secretariat': secretariat,
|
|
||||||
'save_perm': save_perm,
|
|
||||||
'read_only': canedit==False,
|
|
||||||
'owner_href': owner_href}),
|
|
||||||
content_type="application/json")
|
|
||||||
|
|
||||||
#############################################################################
|
|
||||||
## ROOM API
|
|
||||||
#############################################################################
|
|
||||||
from django.forms.models import modelform_factory
|
|
||||||
AddRoomForm = modelform_factory(Room, exclude=('meeting','time'))
|
|
||||||
|
|
||||||
# no authorization required
|
|
||||||
def timeslot_roomlist(request, mtg):
|
|
||||||
rooms = mtg.room_set.all()
|
|
||||||
json_array=[]
|
|
||||||
for room in rooms:
|
|
||||||
json_array.append(room.json_dict(request.build_absolute_uri('/')))
|
|
||||||
return HttpResponse(json.dumps(json_array),
|
|
||||||
content_type="application/json")
|
|
||||||
|
|
||||||
@role_required('Secretariat')
|
|
||||||
def timeslot_addroom(request, meeting):
|
|
||||||
newroomform = AddRoomForm(request.POST)
|
|
||||||
if not newroomform.is_valid():
|
|
||||||
return HttpResponse(status=404)
|
|
||||||
|
|
||||||
newroom = newroomform.save(commit=False)
|
|
||||||
newroom.meeting = meeting
|
|
||||||
newroom.save()
|
|
||||||
newroom.create_timeslots()
|
|
||||||
|
|
||||||
if "HTTP_ACCEPT" in request.META and "application/json" in request.META['HTTP_ACCEPT']:
|
|
||||||
return redirect(timeslot_roomurl, meeting.number, newroom.pk)
|
|
||||||
else:
|
|
||||||
return redirect(edit_timeslots, meeting.number)
|
|
||||||
|
|
||||||
@role_required('Secretariat')
|
|
||||||
def timeslot_delroom(request, meeting, roomid):
|
|
||||||
room = get_object_or_404(meeting.room_set, pk=roomid)
|
|
||||||
|
|
||||||
room.delete_timeslots()
|
|
||||||
room.delete()
|
|
||||||
return HttpResponse('{"error":"none"}', status = 200)
|
|
||||||
|
|
||||||
@role_required('Secretariat')
|
|
||||||
def timeslot_updroom(request, meeting, roomid):
|
|
||||||
room = get_object_or_404(meeting.room_set, pk=roomid)
|
|
||||||
|
|
||||||
if "name" in request.POST:
|
|
||||||
room.name = request.POST["name"]
|
|
||||||
|
|
||||||
if "capacity" in request.POST:
|
|
||||||
room.capacity = request.POST["capacity"]
|
|
||||||
|
|
||||||
if "resources" in request.POST:
|
|
||||||
new_resource_ids = request.POST["resources"]
|
|
||||||
new_resources = [ ResourceAssociation.objects.get(pk=a)
|
|
||||||
for a in new_resource_ids]
|
|
||||||
room.resources = new_resources
|
|
||||||
|
|
||||||
room.save()
|
|
||||||
return HttpResponse('{"error":"none"}', status = 200)
|
|
||||||
|
|
||||||
def timeslot_roomsurl(request, num=None):
|
|
||||||
meeting = get_meeting(num)
|
|
||||||
|
|
||||||
if request.method == 'GET':
|
|
||||||
return timeslot_roomlist(request, meeting)
|
|
||||||
elif request.method == 'POST':
|
|
||||||
return timeslot_addroom(request, meeting)
|
|
||||||
|
|
||||||
# unacceptable reply
|
|
||||||
return HttpResponse(status=406)
|
|
||||||
|
|
||||||
def timeslot_roomurl(request, num=None, roomid=None):
|
|
||||||
meeting = get_meeting(num)
|
|
||||||
|
|
||||||
if request.method == 'GET':
|
|
||||||
room = get_object_or_404(meeting.room_set, pk=roomid)
|
|
||||||
return HttpResponse(json.dumps(room.json_dict(request.build_absolute_uri('/'))),
|
|
||||||
content_type="application/json")
|
|
||||||
elif request.method == 'PUT':
|
|
||||||
return timeslot_updroom(request, meeting, roomid)
|
|
||||||
elif request.method == 'DELETE':
|
|
||||||
return timeslot_delroom(request, meeting, roomid)
|
|
||||||
|
|
||||||
#############################################################################
|
|
||||||
## DAY/SLOT API
|
|
||||||
## -- this creates groups of timeslots, and associated schedtimesessassignments.
|
|
||||||
#############################################################################
|
|
||||||
AddSlotForm = modelform_factory(TimeSlot, exclude=('meeting','name','location','sessions', 'modified'))
|
|
||||||
|
|
||||||
# no authorization required to list.
|
|
||||||
def timeslot_slotlist(request, mtg):
|
|
||||||
slots = mtg.timeslot_set.all()
|
|
||||||
# Restrict graphical editing to slots of type 'regular' for now
|
|
||||||
slots = slots.filter(type__slug='regular')
|
|
||||||
json_array=[]
|
|
||||||
for slot in slots:
|
|
||||||
json_array.append(slot.json_dict(request.build_absolute_uri('/')))
|
|
||||||
return HttpResponse(json.dumps(json_array, sort_keys=True, indent=2),
|
|
||||||
content_type="application/json")
|
|
||||||
|
|
||||||
@role_required('Secretariat')
|
|
||||||
def timeslot_addslot(request, meeting):
|
|
||||||
addslotform = AddSlotForm(request.POST)
|
|
||||||
#debug.log("newslot: %u" % ( addslotform.is_valid() ))
|
|
||||||
if not addslotform.is_valid():
|
|
||||||
return HttpResponse(status=404)
|
|
||||||
|
|
||||||
newslot = addslotform.save(commit=False)
|
|
||||||
newslot.meeting = meeting
|
|
||||||
newslot.save()
|
|
||||||
|
|
||||||
# XXX FIXME: timeslot_dayurl is undefined. Placeholder:
|
|
||||||
# timeslot_dayurl = None
|
|
||||||
# XXX FIXME: newroom is undefined. Placeholder:
|
|
||||||
# newroom = None
|
|
||||||
values = newslot.json_dict(request.build_absolute_uri('/'))
|
|
||||||
response = HttpResponse(json.dumps(values),
|
|
||||||
content_type="application/json",
|
|
||||||
status=201)
|
|
||||||
response['Location'] = values['href']
|
|
||||||
return response
|
|
||||||
|
|
||||||
@role_required('Secretariat')
|
|
||||||
def timeslot_updslot(request, meeting, slotid):
|
|
||||||
slot = get_object_or_404(meeting.timeslot_set, pk=slotid)
|
|
||||||
|
|
||||||
# at present, updates to the purpose only is supported.
|
|
||||||
# updates to time or duration would need likely need to be
|
|
||||||
# propogated to the entire vertical part of the grid, and nothing
|
|
||||||
# needs to do that yet.
|
|
||||||
if request.method == 'POST':
|
|
||||||
put_vars = request.POST
|
|
||||||
slot.type_id = put_vars["purpose"]
|
|
||||||
else:
|
|
||||||
put_vars = QueryDict(request.body)
|
|
||||||
slot.type_id = put_vars.get("purpose")
|
|
||||||
|
|
||||||
slot.save()
|
|
||||||
|
|
||||||
# WORKAROUND: Right now, if there are sessions scheduled in this timeslot
|
|
||||||
# when it is marked unavailable (or any other value besides 'regular') they
|
|
||||||
# become unreachable from the editing screen. The session is listed in the
|
|
||||||
# "unscheduled" block incorrectly, and drag-dropping it onto the a new
|
|
||||||
# timeslot produces erroneous results. To avoid this, we will silently
|
|
||||||
# unschedule any sessions in the timeslot that has just been made
|
|
||||||
# unavailable.
|
|
||||||
|
|
||||||
if slot.type_id != 'regular':
|
|
||||||
slot.sessionassignments.all().delete()
|
|
||||||
|
|
||||||
# ENDWORKAROUND
|
|
||||||
|
|
||||||
# need to return the new object.
|
|
||||||
dict1 = slot.json_dict(request.build_absolute_uri('/'))
|
|
||||||
dict1['message'] = 'valid'
|
|
||||||
return HttpResponse(json.dumps(dict1),
|
|
||||||
content_type="application/json")
|
|
||||||
|
|
||||||
@role_required('Secretariat')
|
|
||||||
def timeslot_delslot(request, meeting, slotid):
|
|
||||||
slot = get_object_or_404(meeting.timeslot_set, pk=slotid)
|
|
||||||
|
|
||||||
# this will delete self as well.
|
|
||||||
slot.delete_concurrent_timeslots()
|
|
||||||
return HttpResponse('{"error":"none"}', status = 200)
|
|
||||||
|
|
||||||
def timeslot_slotsurl(request, num=None):
|
|
||||||
meeting = get_meeting(num)
|
|
||||||
|
|
||||||
if request.method == 'GET':
|
|
||||||
return timeslot_slotlist(request, meeting)
|
|
||||||
elif request.method == 'POST':
|
|
||||||
return timeslot_addslot(request, meeting)
|
|
||||||
|
|
||||||
# unacceptable reply
|
|
||||||
return HttpResponse(status=406)
|
|
||||||
|
|
||||||
def timeslot_sloturl(request, num=None, slotid=None):
|
|
||||||
meeting = get_meeting(num)
|
|
||||||
|
|
||||||
if request.method == 'GET':
|
|
||||||
slot = get_object_or_404(meeting.timeslot_set, pk=slotid)
|
|
||||||
return HttpResponse(json.dumps(slot.json_dict(request.build_absolute_uri('/'))),
|
|
||||||
content_type="application/json")
|
|
||||||
elif request.method == 'POST' or request.method == 'PUT':
|
|
||||||
return timeslot_updslot(request, meeting, slotid)
|
|
||||||
elif request.method == 'DELETE':
|
|
||||||
return timeslot_delslot(request, meeting, slotid)
|
|
||||||
|
|
||||||
#############################################################################
|
|
||||||
## Schedule List API
|
|
||||||
#############################################################################
|
|
||||||
ScheduleEntryForm = modelform_factory(Schedule, exclude=('meeting','owner'))
|
|
||||||
EditScheduleEntryForm = modelform_factory(Schedule, exclude=('meeting','owner', 'name'))
|
|
||||||
|
|
||||||
@role_required('Area Director','Secretariat')
|
|
||||||
def schedule_list(request, mtg):
|
|
||||||
schedules = mtg.schedule_set.all()
|
|
||||||
json_array=[]
|
|
||||||
for schedule in schedules:
|
|
||||||
json_array.append(schedule.json_dict(request.build_absolute_uri('/')))
|
|
||||||
return HttpResponse(json.dumps(json_array),
|
|
||||||
content_type="application/json")
|
|
||||||
|
|
||||||
# duplicates save-as functionality below.
|
|
||||||
@role_required('Area Director','Secretariat')
|
|
||||||
def schedule_add(request, meeting):
|
|
||||||
newscheduleform = ScheduleEntryForm(request.POST)
|
|
||||||
if not newscheduleform.is_valid():
|
|
||||||
return HttpResponse(status=404)
|
|
||||||
|
|
||||||
newschedule = newscheduleform.save(commit=False)
|
|
||||||
newschedule.meeting = meeting
|
|
||||||
newschedule.owner = request.user.person
|
|
||||||
newschedule.save()
|
|
||||||
|
|
||||||
if "HTTP_ACCEPT" in request.META and "application/json" in request.META['HTTP_ACCEPT']:
|
|
||||||
return redirect(schedule_infourl, meeting.number, newschedule.owner_email(), newschedule.name)
|
|
||||||
else:
|
|
||||||
return redirect(edit_schedule, meeting.number, newschedule.owner_email(), newschedule.name)
|
|
||||||
|
|
||||||
@require_POST
|
|
||||||
def schedule_update(request, meeting, schedule):
|
|
||||||
# forms are completely useless for update actions that want to
|
|
||||||
# accept a subset of values. (huh? we could use required=False)
|
|
||||||
|
|
||||||
user = request.user
|
|
||||||
|
|
||||||
if not user.is_authenticated:
|
|
||||||
return HttpResponse({'error':'no permission'}, status=403)
|
|
||||||
|
|
||||||
cansee,canedit,secretariat = schedule_permissions(meeting, schedule, request.user)
|
|
||||||
#read_only = not canedit ## not used
|
|
||||||
|
|
||||||
# TODO: Secretariat should always get canedit
|
|
||||||
if not (canedit or secretariat):
|
|
||||||
return HttpResponse({'error':'no permission'}, status=403)
|
|
||||||
|
|
||||||
if "public" in request.POST:
|
|
||||||
schedule.public = is_truthy_enough(request.POST["public"])
|
|
||||||
|
|
||||||
if "visible" in request.POST:
|
|
||||||
schedule.visible = is_truthy_enough(request.POST["visible"])
|
|
||||||
|
|
||||||
if "name" in request.POST:
|
|
||||||
schedule.name = request.POST["name"]
|
|
||||||
|
|
||||||
schedule.save()
|
|
||||||
|
|
||||||
# enforce that a non-public schedule can not be the public one.
|
|
||||||
if meeting.schedule == schedule and not schedule.public:
|
|
||||||
meeting.schedule = None
|
|
||||||
meeting.save()
|
|
||||||
|
|
||||||
if "HTTP_ACCEPT" in request.META and "application/json" in request.META['HTTP_ACCEPT']:
|
|
||||||
return HttpResponse(json.dumps(schedule.json_dict(request.build_absolute_uri('/'))),
|
|
||||||
content_type="application/json")
|
|
||||||
else:
|
|
||||||
return redirect(edit_schedule, meeting.number, schedule.owner_email(), schedule.name)
|
|
||||||
|
|
||||||
@role_required('Secretariat')
|
|
||||||
def schedule_del(request, meeting, schedule):
|
|
||||||
schedule.delete_assignments()
|
|
||||||
#debug.log("deleting meeting: %s schedule: %s" % (meeting, meeting.schedule))
|
|
||||||
if meeting.schedule == schedule:
|
|
||||||
meeting.schedule = None
|
|
||||||
meeting.save()
|
|
||||||
schedule.delete()
|
|
||||||
return HttpResponse('{"error":"none"}', status = 200)
|
|
||||||
|
|
||||||
def schedule_infosurl(request, num=None):
|
|
||||||
meeting = get_meeting(num)
|
|
||||||
|
|
||||||
if request.method == 'GET':
|
|
||||||
return schedule_list(request, meeting)
|
|
||||||
elif request.method == 'POST':
|
|
||||||
return schedule_add(request, meeting)
|
|
||||||
|
|
||||||
# unacceptable action
|
|
||||||
return HttpResponse(status=406)
|
|
||||||
|
|
||||||
def schedule_infourl(request, num=None, owner=None, name=None):
|
|
||||||
meeting = get_meeting(num)
|
|
||||||
person = get_person_by_email(owner)
|
|
||||||
schedule = get_schedule_by_name(meeting, person, name)
|
|
||||||
if schedule is None:
|
|
||||||
raise Http404("No meeting information for meeting %s schedule %s available" % (num,name))
|
|
||||||
|
|
||||||
#debug.log("results in schedule: %u / %s" % (schedule.id, request.method))
|
|
||||||
|
|
||||||
if request.method == 'GET':
|
|
||||||
return HttpResponse(json.dumps(schedule.json_dict(request.build_absolute_uri('/'))),
|
|
||||||
content_type="application/json")
|
|
||||||
elif request.method == 'POST':
|
|
||||||
return schedule_update(request, meeting, schedule)
|
|
||||||
elif request.method == 'DELETE':
|
|
||||||
return schedule_del(request, meeting, schedule)
|
|
||||||
else:
|
|
||||||
return HttpResponse(status=406)
|
|
||||||
|
|
||||||
#############################################################################
|
|
||||||
## Meeting API (very limited)
|
|
||||||
#############################################################################
|
|
||||||
|
|
||||||
def meeting_get(request, meeting):
|
|
||||||
return HttpResponse(json.dumps(meeting.json_dict(request.build_absolute_uri('/')),
|
|
||||||
sort_keys=True, indent=2),
|
|
||||||
content_type="application/json")
|
|
||||||
|
|
||||||
@role_required('Secretariat')
|
|
||||||
def meeting_update(request, meeting):
|
|
||||||
# at present, only the official schedule can be updated from this interface.
|
|
||||||
|
|
||||||
#debug.log("1 meeting.schedule: %s / %s / %s" % (meeting.schedule, update_dict, request.body))
|
|
||||||
if "schedule" in request.POST:
|
|
||||||
value = request.POST["schedule"]
|
|
||||||
#debug.log("4 meeting.schedule: %s" % (value))
|
|
||||||
if not value or value == "None": # value == "None" is just weird, better with empty string
|
|
||||||
meeting.set_official_schedule(None)
|
|
||||||
else:
|
|
||||||
schedule = get_schedule(meeting, value)
|
|
||||||
if not schedule.public:
|
|
||||||
return HttpResponse(status = 406)
|
|
||||||
#debug.log("3 meeting.schedule: %s" % (schedule))
|
|
||||||
meeting.set_official_schedule(schedule)
|
|
||||||
|
|
||||||
#debug.log("2 meeting.schedule: %s" % (meeting.schedule))
|
|
||||||
meeting.save()
|
|
||||||
return meeting_get(request, meeting)
|
|
||||||
|
|
||||||
def meeting_json(request, num):
|
|
||||||
meeting = get_meeting(num)
|
|
||||||
|
|
||||||
if request.method == 'GET':
|
|
||||||
return meeting_get(request, meeting)
|
|
||||||
elif request.method == 'POST':
|
|
||||||
return meeting_update(request, meeting)
|
|
||||||
else:
|
|
||||||
return HttpResponse(status=406)
|
|
||||||
|
|
||||||
|
|
||||||
#############################################################################
|
|
||||||
## Session details API functions
|
|
||||||
#############################################################################
|
|
||||||
|
|
||||||
def session_json(request, num, sessionid):
|
|
||||||
meeting = get_meeting(num)
|
|
||||||
|
|
||||||
try:
|
|
||||||
session = meeting.session_set.get(pk=int(sessionid))
|
|
||||||
except Session.DoesNotExist:
|
|
||||||
# return json.dumps({'error':"no such session %s" % sessionid})
|
|
||||||
return HttpResponse(json.dumps({'error':"no such session %s" % sessionid}),
|
|
||||||
status = 404,
|
|
||||||
content_type="application/json")
|
|
||||||
|
|
||||||
sess1 = session.json_dict(request.build_absolute_uri('/'))
|
|
||||||
return HttpResponse(json.dumps(sess1, sort_keys=True, indent=2),
|
|
||||||
content_type="application/json")
|
|
||||||
|
|
||||||
# get group of all sessions.
|
|
||||||
def sessions_json(request, num):
|
|
||||||
meeting = get_meeting(num)
|
|
||||||
|
|
||||||
sessions = meeting.session_set.that_can_meet().with_requested_time().with_requested_by()
|
|
||||||
|
|
||||||
sess1_dict = [ x.json_dict(request.build_absolute_uri('/')) for x in sessions ]
|
|
||||||
return HttpResponse(json.dumps(sess1_dict, sort_keys=True, indent=2),
|
|
||||||
content_type="application/json")
|
|
||||||
|
|
||||||
#############################################################################
|
|
||||||
## Scheduledsesion
|
|
||||||
#############################################################################
|
|
||||||
|
|
||||||
# this creates an entirely *NEW* schedtimesessassignment
|
|
||||||
def assignments_post(request, meeting, schedule):
|
|
||||||
cansee,canedit,secretariat = schedule_permissions(meeting, schedule, request.user)
|
|
||||||
if not canedit:
|
|
||||||
return HttpResponse(json.dumps({'error':'no permission to modify this schedule'}),
|
|
||||||
status = 403,
|
|
||||||
content_type="application/json")
|
|
||||||
|
|
||||||
# get JSON out of raw body. XXX should check Content-Type!
|
|
||||||
newvalues = json.loads(request.body)
|
|
||||||
if not ("session_id" in newvalues) or not ("timeslot_id" in newvalues):
|
|
||||||
return HttpResponse(json.dumps({'error':'missing values, timeslot_id and session_id required'}),
|
|
||||||
status = 406,
|
|
||||||
content_type="application/json")
|
|
||||||
|
|
||||||
try:
|
|
||||||
Session.objects.get(pk=newvalues["session_id"])
|
|
||||||
except Session.DoesNotExist:
|
|
||||||
return HttpResponse(json.dumps({'error':'session has been deleted'}),
|
|
||||||
status = 406,
|
|
||||||
content_type="application/json")
|
|
||||||
|
|
||||||
ss1 = SchedTimeSessAssignment(schedule = schedule,
|
|
||||||
session_id = newvalues["session_id"],
|
|
||||||
timeslot_id = newvalues["timeslot_id"])
|
|
||||||
if("extendedfrom_id" in newvalues):
|
|
||||||
val = int(newvalues["extendedfrom_id"])
|
|
||||||
try:
|
|
||||||
ss2 = schedule.assignments.get(pk = val)
|
|
||||||
ss1.extendedfrom = ss2
|
|
||||||
except SchedTimeSessAssignment.DoesNotExist:
|
|
||||||
return HttpResponse(json.dumps({'error':'invalid extendedfrom value: %u' % val}),
|
|
||||||
status = 406,
|
|
||||||
content_type="application/json")
|
|
||||||
ss1.save()
|
|
||||||
ss1_dict = ss1.json_dict(request.build_absolute_uri('/'))
|
|
||||||
response = HttpResponse(json.dumps(ss1_dict),
|
|
||||||
status = 201,
|
|
||||||
content_type="application/json")
|
|
||||||
# 201 code needs a Location: header.
|
|
||||||
response['Location'] = ss1_dict["href"],
|
|
||||||
return response
|
|
||||||
|
|
||||||
def assignments_get(request, num, schedule):
|
|
||||||
assignments = schedule.assignments.all()
|
|
||||||
|
|
||||||
absolute_url = request.build_absolute_uri('/')
|
|
||||||
sess1_dict = [ x.json_dict(absolute_url) for x in assignments ]
|
|
||||||
return HttpResponse(json.dumps(sess1_dict, sort_keys=True, indent=2),
|
|
||||||
content_type="application/json")
|
|
||||||
|
|
||||||
# this returns the list of scheduled sessions for the given named schedule
|
|
||||||
def assignments_json(request, num, owner, name):
|
|
||||||
info = get_meeting_schedule(num, owner, name)
|
|
||||||
# The return values from get_meeting_schedule() are silly, in that it
|
|
||||||
# is a tuple for non-error return, but a HTTPResponse when error, but
|
|
||||||
# work around that for the moment
|
|
||||||
if isinstance(info, HttpResponse):
|
|
||||||
return info
|
|
||||||
meeting, person, schedule = info
|
|
||||||
|
|
||||||
if request.method == 'GET':
|
|
||||||
return assignments_get(request, meeting, schedule)
|
|
||||||
elif request.method == 'POST':
|
|
||||||
return assignments_post(request, meeting, schedule)
|
|
||||||
else:
|
|
||||||
return HttpResponse(json.dumps({'error':'inappropriate action: %s' % (request.method)}),
|
|
||||||
status = 406,
|
|
||||||
content_type="application/json")
|
|
||||||
|
|
||||||
# accepts both POST and PUT in order to implement Postel Doctrine.
|
|
||||||
def assignment_update(request, meeting, schedule, ss):
|
|
||||||
cansee,canedit,secretariat = schedule_permissions(meeting, schedule, request.user)
|
|
||||||
if not canedit:
|
|
||||||
return HttpResponse(json.dumps({'error':'no permission to update this schedule'}),
|
|
||||||
status = 403,
|
|
||||||
content_type="application/json")
|
|
||||||
|
|
||||||
if request.method == 'POST':
|
|
||||||
put_vars = request.POST
|
|
||||||
ss.pinned = is_truthy_enough(put_vars["pinned"])
|
|
||||||
else:
|
|
||||||
put_vars = QueryDict(request.body)
|
|
||||||
ss.pinned = is_truthy_enough(put_vars.get("pinned"))
|
|
||||||
|
|
||||||
ss.save()
|
|
||||||
return HttpResponse(json.dumps({'message':'valid'}),
|
|
||||||
content_type="application/json")
|
|
||||||
|
|
||||||
def assignment_delete(request, meeting, schedule, ss):
|
|
||||||
cansee,canedit,secretariat = schedule_permissions(meeting, schedule, request.user)
|
|
||||||
if not canedit:
|
|
||||||
return HttpResponse(json.dumps({'error':'no permission to update this schedule'}),
|
|
||||||
status = 403,
|
|
||||||
content_type="application/json")
|
|
||||||
|
|
||||||
# in case there is, somehow, more than one item with the same pk.. XXX
|
|
||||||
assignments = schedule.assignments.filter(pk = ss.pk)
|
|
||||||
if len(assignments) == 0:
|
|
||||||
return HttpResponse(json.dumps({'error':'no such object'}),
|
|
||||||
status = 404,
|
|
||||||
content_type="application/json")
|
|
||||||
count=0
|
|
||||||
for ss in assignments:
|
|
||||||
ss.delete()
|
|
||||||
count += 1
|
|
||||||
|
|
||||||
return HttpResponse(json.dumps({'result':"%u objects deleted"%(count)}),
|
|
||||||
status = 200,
|
|
||||||
content_type="application/json")
|
|
||||||
|
|
||||||
def assignment_get(request, meeting, schedule, ss):
|
|
||||||
cansee,canedit,secretariat = schedule_permissions(meeting, schedule, request.user)
|
|
||||||
|
|
||||||
if not cansee:
|
|
||||||
return HttpResponse(json.dumps({'error':'no permission to see this schedule'}),
|
|
||||||
status = 403,
|
|
||||||
content_type="application/json")
|
|
||||||
|
|
||||||
sess1_dict = ss.json_dict(request.build_absolute_uri('/'))
|
|
||||||
return HttpResponse(json.dumps(sess1_dict, sort_keys=True, indent=2),
|
|
||||||
content_type="application/json")
|
|
||||||
|
|
||||||
# this return a specific session, updates a session or deletes a SPECIFIC scheduled session
|
|
||||||
def assignment_json(request, num, owner, name, assignment_id):
|
|
||||||
meeting, person, schedule = get_meeting_schedule(num, owner, name)
|
|
||||||
|
|
||||||
assignments = schedule.assignments.filter(pk = assignment_id)
|
|
||||||
if len(assignments) == 0:
|
|
||||||
return HttpResponse(json.dumps({'error' : 'invalid assignment'}),
|
|
||||||
content_type="application/json",
|
|
||||||
status=404);
|
|
||||||
ss = assignments[0]
|
|
||||||
|
|
||||||
if request.method == 'GET':
|
|
||||||
return assignment_get(request, meeting, schedule, ss)
|
|
||||||
elif request.method == 'PUT' or request.method=='POST':
|
|
||||||
return assignment_update(request, meeting, schedule, ss)
|
|
||||||
elif request.method == 'DELETE':
|
|
||||||
return assignment_delete(request, meeting, schedule, ss)
|
|
||||||
|
|
||||||
#############################################################################
|
|
||||||
## Constraints API
|
|
||||||
#############################################################################
|
|
||||||
|
|
||||||
|
|
||||||
# Would like to cache for 1 day, but there are invalidation issues.
|
|
||||||
#@cache_page(86400)
|
|
||||||
def constraint_json(request, num, constraintid):
|
|
||||||
meeting = get_meeting(num)
|
|
||||||
|
|
||||||
try:
|
|
||||||
constraint = meeting.constraint_set.get(pk=int(constraintid))
|
|
||||||
except Constraint.DoesNotExist:
|
|
||||||
return HttpResponse(json.dumps({'error':"no such constraint %s" % constraintid}),
|
|
||||||
status = 404,
|
|
||||||
content_type="application/json")
|
|
||||||
|
|
||||||
json1 = constraint.json_dict(request.build_absolute_uri('/'))
|
|
||||||
return HttpResponse(json.dumps(json1, sort_keys=True, indent=2),
|
|
||||||
content_type="application/json")
|
|
||||||
|
|
||||||
|
|
||||||
# Cache for 2 hour2
|
|
||||||
#@cache_page(7200)
|
|
||||||
# caching is a problem if there Host: header changes.
|
|
||||||
#
|
|
||||||
def session_constraints(request, num, sessionid):
|
|
||||||
meeting = get_meeting(num)
|
|
||||||
|
|
||||||
#print "Getting meeting=%s session contraints for %s" % (num, sessionid)
|
|
||||||
try:
|
|
||||||
session = meeting.session_set.get(pk=int(sessionid))
|
|
||||||
except Session.DoesNotExist:
|
|
||||||
return json.dumps({"error":"no such session"})
|
|
||||||
|
|
||||||
constraint_list = session.constraints_dict(request.build_absolute_uri('/'))
|
|
||||||
|
|
||||||
json_str = json.dumps(constraint_list,
|
|
||||||
sort_keys=True, indent=2),
|
|
||||||
#print " gives: %s" % (json_str)
|
|
||||||
|
|
||||||
return HttpResponse(json_str, content_type="application/json")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,8 @@ from django.db.models import Q
|
||||||
from ietf.meeting.models import (Meeting, Session, SchedulingEvent, Schedule,
|
from ietf.meeting.models import (Meeting, Session, SchedulingEvent, Schedule,
|
||||||
TimeSlot, SessionPresentation, FloorPlan, Room, SlideSubmission, Constraint,
|
TimeSlot, SessionPresentation, FloorPlan, Room, SlideSubmission, Constraint,
|
||||||
MeetingHost, ProceedingsMaterial)
|
MeetingHost, ProceedingsMaterial)
|
||||||
from ietf.name.models import ConstraintName, SessionStatusName, ProceedingsMaterialTypeName, TimerangeName
|
from ietf.name.models import (ConstraintName, SessionStatusName, ProceedingsMaterialTypeName,
|
||||||
|
TimerangeName, SessionPurposeName)
|
||||||
from ietf.doc.factories import ProceedingsMaterialDocFactory
|
from ietf.doc.factories import ProceedingsMaterialDocFactory
|
||||||
from ietf.group.factories import GroupFactory
|
from ietf.group.factories import GroupFactory
|
||||||
from ietf.person.factories import PersonFactory
|
from ietf.person.factories import PersonFactory
|
||||||
|
@ -104,9 +105,11 @@ class SessionFactory(factory.django.DjangoModelFactory):
|
||||||
model = Session
|
model = Session
|
||||||
|
|
||||||
meeting = factory.SubFactory(MeetingFactory)
|
meeting = factory.SubFactory(MeetingFactory)
|
||||||
type_id='regular'
|
purpose_id = 'regular'
|
||||||
|
type_id = 'regular'
|
||||||
group = factory.SubFactory(GroupFactory)
|
group = factory.SubFactory(GroupFactory)
|
||||||
requested_duration = datetime.timedelta(hours=1)
|
requested_duration = datetime.timedelta(hours=1)
|
||||||
|
on_agenda = factory.lazy_attribute(lambda obj: SessionPurposeName.objects.get(pk=obj.purpose_id).on_agenda)
|
||||||
|
|
||||||
@factory.post_generation
|
@factory.post_generation
|
||||||
def status_id(obj, create, extracted, **kwargs):
|
def status_id(obj, create, extracted, **kwargs):
|
||||||
|
@ -128,7 +131,7 @@ class SessionFactory(factory.django.DjangoModelFactory):
|
||||||
status=SessionStatusName.objects.get(slug=extracted),
|
status=SessionStatusName.objects.get(slug=extracted),
|
||||||
by=PersonFactory(),
|
by=PersonFactory(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@factory.post_generation
|
@factory.post_generation
|
||||||
def add_to_schedule(obj, create, extracted, **kwargs): # pylint: disable=no-self-argument
|
def add_to_schedule(obj, create, extracted, **kwargs): # pylint: disable=no-self-argument
|
||||||
'''
|
'''
|
||||||
|
|
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 # pyflakes: ignore
|
||||||
|
|
||||||
|
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)
|
||||||
|
@ -253,6 +257,7 @@ class InterimSessionModelForm(forms.ModelForm):
|
||||||
session = super(InterimSessionModelForm, self).save(commit=False)
|
session = super(InterimSessionModelForm, self).save(commit=False)
|
||||||
session.group = self.group
|
session.group = self.group
|
||||||
session.type_id = 'regular'
|
session.type_id = 'regular'
|
||||||
|
session.purpose_id = 'regular'
|
||||||
if kwargs.get('commit', True) is True:
|
if kwargs.get('commit', True) is True:
|
||||||
super(InterimSessionModelForm, self).save(commit=True)
|
super(InterimSessionModelForm, self).save(commit=True)
|
||||||
return session
|
return session
|
||||||
|
@ -417,3 +422,258 @@ 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):
|
||||||
|
def __init__(self, durations=None, *args, **kwargs):
|
||||||
|
if durations is None:
|
||||||
|
durations = (3600, 7200)
|
||||||
|
super().__init__(
|
||||||
|
choices=self._make_choices(durations),
|
||||||
|
*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 isinstance(value, datetime.timedelta) else str(value)
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
return datetime.timedelta(seconds=round(float(value))) if value not in self.empty_values else None
|
||||||
|
|
||||||
|
def valid_value(self, value):
|
||||||
|
return super().valid_value(self.prepare_value(value))
|
||||||
|
|
||||||
|
def _format_duration_choice(self, dur):
|
||||||
|
seconds = int(dur.total_seconds()) if isinstance(dur, datetime.timedelta) else int(dur)
|
||||||
|
hours = int(seconds / 3600)
|
||||||
|
minutes = round((seconds - 3600 * hours) / 60)
|
||||||
|
hr_str = '{} hour{}'.format(hours, '' if hours == 1 else 's')
|
||||||
|
min_str = '{} minute{}'.format(minutes, '' if minutes == 1 else 's')
|
||||||
|
if hours > 0 and minutes > 0:
|
||||||
|
time_str = ' '.join((hr_str, min_str))
|
||||||
|
elif hours > 0:
|
||||||
|
time_str = hr_str
|
||||||
|
else:
|
||||||
|
time_str = min_str
|
||||||
|
return (str(seconds), time_str)
|
||||||
|
|
||||||
|
def _make_choices(self, durations):
|
||||||
|
return (
|
||||||
|
('','--Please select'),
|
||||||
|
*[self._format_duration_choice(dur) for dur in durations])
|
||||||
|
|
||||||
|
def _set_durations(self, durations):
|
||||||
|
self.choices = self._make_choices(durations)
|
||||||
|
|
||||||
|
durations = property(None, _set_durations)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
if not group.features.acts_like_wg:
|
||||||
|
self.fields['requested_duration'].durations = [datetime.timedelta(minutes=m) for m in range(30, 241, 30)]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Session
|
||||||
|
fields = (
|
||||||
|
'name', 'short', 'purpose', 'type', 'requested_duration',
|
||||||
|
'on_agenda', 'remote_instructions', 'attendees', 'comments',
|
||||||
|
)
|
||||||
|
labels = {'requested_duration': 'Length'}
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
if 'purpose' in self.cleaned_data and (
|
||||||
|
'purpose' in self.changed_data or self.instance.pk is None
|
||||||
|
):
|
||||||
|
self.cleaned_data['on_agenda'] = self.cleaned_data['purpose'].on_agenda
|
||||||
|
|
||||||
|
return self.cleaned_data
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
js = ('ietf/js/meeting/session_details_form.js',)
|
||||||
|
|
||||||
|
|
||||||
|
class SessionEditForm(SessionDetailsForm):
|
||||||
|
"""Form to edit an existing session"""
|
||||||
|
def __init__(self, instance, *args, **kwargs):
|
||||||
|
kw_group = kwargs.pop('group', None)
|
||||||
|
if kw_group is not None and kw_group != instance.group:
|
||||||
|
raise ValueError('Session group does not match group keyword')
|
||||||
|
super().__init__(instance=instance, group=instance.group, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
return 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]
|
||||||
|
|
||||||
|
def sessiondetailsformset_factory(min_num=1, max_num=3):
|
||||||
|
return forms.inlineformset_factory(
|
||||||
|
Group,
|
||||||
|
Session,
|
||||||
|
formset=SessionDetailsInlineFormset,
|
||||||
|
form=SessionDetailsForm,
|
||||||
|
can_delete=True,
|
||||||
|
can_order=False,
|
||||||
|
min_num=min_num,
|
||||||
|
max_num=max_num,
|
||||||
|
extra=max_num, # only creates up to max_num total
|
||||||
|
)
|
||||||
|
|
|
@ -9,13 +9,11 @@ import os
|
||||||
import re
|
import re
|
||||||
from tempfile import mkstemp
|
from tempfile import mkstemp
|
||||||
|
|
||||||
from django.http import HttpRequest, Http404
|
from django.http import Http404
|
||||||
from django.db.models import F, Max, Q, Prefetch
|
from django.db.models import F, Prefetch
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
from django.core.cache import cache
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.cache import get_cache_key
|
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
|
|
||||||
|
@ -30,93 +28,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.history import find_history_active_at, find_history_replacements_active_at
|
from ietf.utils import log
|
||||||
|
from ietf.utils.history import 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):
|
|
||||||
ads = []
|
|
||||||
meeting_time = datetime.datetime.combine(meeting.date, datetime.time(0, 0, 0))
|
|
||||||
|
|
||||||
num = 0
|
|
||||||
# get list of ADs which are/were active at the time of the meeting.
|
|
||||||
# (previous [x for x in y] syntax changed to aid debugging)
|
|
||||||
for g in Group.objects.filter(type="area").order_by("acronym"):
|
|
||||||
history = find_history_active_at(g, meeting_time)
|
|
||||||
num = num +1
|
|
||||||
if history and history != g:
|
|
||||||
#print " history[%u]: %s" % (num, history)
|
|
||||||
if history.state_id == "active":
|
|
||||||
for x in history.rolehistory_set.filter(name="ad",group__type='area').select_related('group', 'person', 'email'):
|
|
||||||
#print "xh[%u]: %s" % (num, x)
|
|
||||||
ads.append(x)
|
|
||||||
else:
|
|
||||||
#print " group[%u]: %s" % (num, g)
|
|
||||||
if g.state_id == "active":
|
|
||||||
for x in g.role_set.filter(name="ad",group__type='area').select_related('group', 'person', 'email'):
|
|
||||||
#print "xg[%u]: %s (#%u)" % (num, x, x.pk)
|
|
||||||
ads.append(x)
|
|
||||||
return ads
|
|
||||||
|
|
||||||
|
|
||||||
# get list of all areas, + IRTF + IETF (plenaries).
|
|
||||||
def get_pseudo_areas():
|
|
||||||
return Group.objects.filter(Q(state="active", name="IRTF")|
|
|
||||||
Q(state="active", name="IETF")|
|
|
||||||
Q(state="active", type="area")).order_by('acronym')
|
|
||||||
|
|
||||||
# get list of all areas, + IRTF.
|
|
||||||
def get_areas():
|
|
||||||
return Group.objects.filter(Q(state="active",
|
|
||||||
name="IRTF")|
|
|
||||||
Q(state="active", type="area")).order_by('acronym')
|
|
||||||
|
|
||||||
# get list of areas that are referenced.
|
|
||||||
def get_area_list_from_sessions(assignments, num):
|
|
||||||
return assignments.filter(timeslot__type = 'regular',
|
|
||||||
session__group__parent__isnull = False).order_by(
|
|
||||||
'session__group__parent__acronym').distinct().values_list(
|
|
||||||
'session__group__parent__acronym',flat=True)
|
|
||||||
|
|
||||||
def build_all_agenda_slices(meeting):
|
|
||||||
time_slices = []
|
|
||||||
date_slices = {}
|
|
||||||
|
|
||||||
for ts in meeting.timeslot_set.filter(type__in=['regular',]).order_by('time','name'):
|
|
||||||
ymd = ts.time.date()
|
|
||||||
|
|
||||||
if ymd not in date_slices and ts.location != None:
|
|
||||||
date_slices[ymd] = []
|
|
||||||
time_slices.append(ymd)
|
|
||||||
|
|
||||||
if ymd in date_slices:
|
|
||||||
if [ts.time, ts.time+ts.duration] not in date_slices[ymd]: # only keep unique entries
|
|
||||||
date_slices[ymd].append([ts.time, ts.time+ts.duration])
|
|
||||||
|
|
||||||
time_slices.sort()
|
|
||||||
return time_slices,date_slices
|
|
||||||
|
|
||||||
def get_all_assignments_from_schedule(schedule):
|
|
||||||
ss = schedule.assignments.filter(timeslot__location__isnull = False)
|
|
||||||
ss = ss.filter(session__type__slug='regular')
|
|
||||||
ss = ss.order_by('timeslot__time','timeslot__name')
|
|
||||||
|
|
||||||
return ss
|
|
||||||
|
|
||||||
def get_modified_from_assignments(assignments):
|
|
||||||
return assignments.aggregate(Max('timeslot__modified'))['timeslot__modified__max']
|
|
||||||
|
|
||||||
def get_wg_name_list(assignments):
|
|
||||||
return assignments.filter(timeslot__type = 'regular',
|
|
||||||
session__group__isnull = False,
|
|
||||||
session__group__parent__isnull = False).order_by(
|
|
||||||
'session__group__acronym').distinct().values_list(
|
|
||||||
'session__group__acronym',flat=True)
|
|
||||||
|
|
||||||
def get_wg_list(assignments):
|
|
||||||
wg_name_list = get_wg_name_list(assignments)
|
|
||||||
return Group.objects.filter(acronym__in = set(wg_name_list)).order_by('parent__acronym','acronym')
|
|
||||||
|
|
||||||
def get_meeting(num=None,type_in=['ietf',],days=28):
|
def get_meeting(num=None,type_in=['ietf',],days=28):
|
||||||
meetings = Meeting.objects
|
meetings = Meeting.objects
|
||||||
|
@ -229,7 +147,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 +171,463 @@ 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='none').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',)
|
||||||
|
# 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
|
||||||
|
if self._use_legacy_keywords():
|
||||||
|
self.extra_labels += ('Plenary',) # need this when not using session purpose
|
||||||
|
|
||||||
|
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(sessions)
|
||||||
|
|
||||||
|
# Not using legacy version
|
||||||
|
filter_cols = []
|
||||||
|
for purpose in self.filterable_purposes:
|
||||||
|
if purpose.slug == 'regular':
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 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, sessions):
|
||||||
|
"""Get list of non-group filters for older meetings
|
||||||
|
|
||||||
|
Returns a list of filter columns
|
||||||
|
"""
|
||||||
|
office_hours_items = set()
|
||||||
|
suffix = ' office hours'
|
||||||
|
for s in sessions:
|
||||||
|
if s.name.lower().endswith(suffix):
|
||||||
|
office_hours_items.add((s.name[:-len(suffix)].strip(), s.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 = 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"""
|
||||||
|
extra = []
|
||||||
|
if session.type_id == 'plenary':
|
||||||
|
extra.append('plenary')
|
||||||
|
office_hours_match = re.match(r'^ *\w+(?: +\w+)* +office hours *$', session.name, re.IGNORECASE)
|
||||||
|
if office_hours_match is not None:
|
||||||
|
suffix = 'officehours'
|
||||||
|
extra.extend([
|
||||||
|
'officehours',
|
||||||
|
session.name.lower().replace(' ', '')[:-len(suffix)] + '-officehours',
|
||||||
|
])
|
||||||
|
return extra
|
||||||
|
|
||||||
|
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
|
||||||
|
@ -387,15 +714,6 @@ def schedule_permissions(meeting, schedule, user):
|
||||||
|
|
||||||
return cansee, canedit, secretariat
|
return cansee, canedit, secretariat
|
||||||
|
|
||||||
def session_constraint_expire(request,session):
|
|
||||||
from .ajax import session_constraints
|
|
||||||
path = reverse(session_constraints, args=[session.meeting.number, session.pk])
|
|
||||||
temp_request = HttpRequest()
|
|
||||||
temp_request.path = path
|
|
||||||
temp_request.META['HTTP_HOST'] = request.META['HTTP_HOST']
|
|
||||||
key = get_cache_key(temp_request)
|
|
||||||
if key is not None and key in cache:
|
|
||||||
cache.delete(key)
|
|
||||||
|
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
# Interim Meeting Helpers
|
# Interim Meeting Helpers
|
||||||
|
|
91
ietf/meeting/management/commands/session_purpose_demo.py
Normal file
91
ietf/meeting/management/commands/session_purpose_demo.py
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
import datetime
|
||||||
|
import random
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
|
||||||
|
from ietf.group.models import Group
|
||||||
|
from ietf.meeting.factories import RoomFactory, TimeSlotFactory, SessionFactory
|
||||||
|
from ietf.meeting.helpers import get_meeting
|
||||||
|
from ietf.meeting.models import Room, Session
|
||||||
|
from ietf.name.models import SessionPurposeName
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Set up a demo of the session purpose updates'
|
||||||
|
|
||||||
|
DEMO_PREFIX='PDemo' # used to identify things added by this command
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('--remove', action='store_true')
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
if options['remove']:
|
||||||
|
self.remove_demo()
|
||||||
|
else:
|
||||||
|
self.install_demo()
|
||||||
|
|
||||||
|
def remove_demo(self):
|
||||||
|
self.stdout.write(f'Removing rooms with "{self.DEMO_PREFIX}" name prefix...\n')
|
||||||
|
Room.objects.filter(name__startswith=self.DEMO_PREFIX).delete()
|
||||||
|
self.stdout.write(f'Removing sessions with "{self.DEMO_PREFIX}" name prefix...\n')
|
||||||
|
Session.objects.filter(name__startswith=self.DEMO_PREFIX).delete()
|
||||||
|
|
||||||
|
def install_demo(self):
|
||||||
|
# get meeting
|
||||||
|
try:
|
||||||
|
meeting = get_meeting(days=14) # matches how secr app finds meetings
|
||||||
|
except:
|
||||||
|
raise CommandError('No upcoming meeting to modify')
|
||||||
|
|
||||||
|
# create rooms
|
||||||
|
self.stdout.write('Creating rooms...\n')
|
||||||
|
rooms = [
|
||||||
|
RoomFactory(meeting=meeting, name=f'{self.DEMO_PREFIX} 1'),
|
||||||
|
RoomFactory(meeting=meeting, name=f'{self.DEMO_PREFIX} 2'),
|
||||||
|
RoomFactory(meeting=meeting, name=f'{self.DEMO_PREFIX} 3'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# get all the timeslot types used by a session purpose
|
||||||
|
type_ids = set()
|
||||||
|
for purpose in SessionPurposeName.objects.filter(used=True):
|
||||||
|
type_ids.update(purpose.timeslot_types)
|
||||||
|
|
||||||
|
# set up timeslots
|
||||||
|
self.stdout.write('Creating timeslots...\n')
|
||||||
|
for room in rooms:
|
||||||
|
for day in range(meeting.days):
|
||||||
|
date = meeting.get_meeting_date(day)
|
||||||
|
for n, type_id in enumerate(type_ids):
|
||||||
|
TimeSlotFactory(
|
||||||
|
type_id=type_id,
|
||||||
|
meeting=meeting,
|
||||||
|
location=room,
|
||||||
|
time=datetime.datetime.combine(date, datetime.time(10, 0, 0)) + datetime.timedelta(hours=n),
|
||||||
|
duration=datetime.timedelta(hours=1),
|
||||||
|
)
|
||||||
|
|
||||||
|
# set up sessions
|
||||||
|
self.stdout.write('Creating sessions...')
|
||||||
|
groups_for_session_purpose = {
|
||||||
|
purpose.slug: list(
|
||||||
|
Group.objects.filter(
|
||||||
|
type__features__session_purposes__contains=f'"{purpose.slug}"',
|
||||||
|
state_id='active',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for purpose in SessionPurposeName.objects.filter(used=True)
|
||||||
|
}
|
||||||
|
for purpose in SessionPurposeName.objects.filter(used=True):
|
||||||
|
for type_id in purpose.timeslot_types:
|
||||||
|
group=random.choice(groups_for_session_purpose[purpose.slug])
|
||||||
|
SessionFactory(
|
||||||
|
meeting=meeting,
|
||||||
|
purpose=purpose,
|
||||||
|
type_id=type_id,
|
||||||
|
group=group,
|
||||||
|
name=f'{self.DEMO_PREFIX} for {group.acronym}',
|
||||||
|
status_id='schedw',
|
||||||
|
add_to_schedule=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.stdout.write(f'\nRooms and sessions created with "{self.DEMO_PREFIX}" as name prefix\n')
|
22
ietf/meeting/migrations/0049_session_purpose.py
Normal file
22
ietf/meeting/migrations/0049_session_purpose.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# 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', '0035_populate_sessionpurposename'),
|
||||||
|
('meeting', '0048_auto_20211008_0907'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='session',
|
||||||
|
name='purpose',
|
||||||
|
field=ietf.utils.models.ForeignKey(default='none', help_text='Purpose of the session', on_delete=django.db.models.deletion.CASCADE, to='name.SessionPurposeName'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
18
ietf/meeting/migrations/0050_session_on_agenda.py
Normal file
18
ietf/meeting/migrations/0050_session_on_agenda.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 2.2.24 on 2021-10-22 06:58
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('meeting', '0049_session_purpose'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='session',
|
||||||
|
name='on_agenda',
|
||||||
|
field=models.BooleanField(default=True, help_text='Is this session visible on the meeting agenda?'),
|
||||||
|
),
|
||||||
|
]
|
39
ietf/meeting/migrations/0051_populate_session_on_agenda.py
Normal file
39
ietf/meeting/migrations/0051_populate_session_on_agenda.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
# Generated by Django 2.2.24 on 2021-10-22 06:58
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def forward(apps, schema_editor):
|
||||||
|
Session = apps.get_model('meeting', 'Session')
|
||||||
|
SchedTimeSessAssignment = apps.get_model('meeting', 'SchedTimeSessAssignment')
|
||||||
|
# find official assignments that are to private timeslots and fill in session.on_agenda
|
||||||
|
private_assignments = SchedTimeSessAssignment.objects.filter(
|
||||||
|
models.Q(
|
||||||
|
schedule=models.F('session__meeting__schedule')
|
||||||
|
) | models.Q(
|
||||||
|
schedule=models.F('session__meeting__schedule__base')
|
||||||
|
),
|
||||||
|
timeslot__type__private=True,
|
||||||
|
)
|
||||||
|
for pa in private_assignments:
|
||||||
|
pa.session.on_agenda = False
|
||||||
|
pa.session.save()
|
||||||
|
# Also update any sessions to match their purpose's default setting (this intentionally
|
||||||
|
# overrides the timeslot settings above, but that is unlikely to matter because the
|
||||||
|
# purposes will roll out at the same time as the on_agenda field)
|
||||||
|
Session.objects.filter(purpose__on_agenda=False).update(on_agenda=False)
|
||||||
|
Session.objects.filter(purpose__on_agenda=True).update(on_agenda=True)
|
||||||
|
|
||||||
|
def reverse(apps, schema_editor):
|
||||||
|
Session = apps.get_model('meeting', 'Session')
|
||||||
|
Session.objects.update(on_agenda=True) # restore all to default on_agenda=True state
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('meeting', '0050_session_on_agenda'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(forward, reverse),
|
||||||
|
]
|
|
@ -14,7 +14,6 @@ import string
|
||||||
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.parse import urljoin
|
|
||||||
|
|
||||||
import debug # pyflakes:ignore
|
import debug # pyflakes:ignore
|
||||||
|
|
||||||
|
@ -23,9 +22,7 @@ from django.db import models
|
||||||
from django.db.models import Max, Subquery, OuterRef, TextField, Value, Q
|
from django.db.models import Max, Subquery, OuterRef, TextField, Value, Q
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
# mostly used by json_dict()
|
from django.urls import reverse as urlreverse
|
||||||
#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.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 +33,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
|
||||||
|
@ -308,36 +306,17 @@ class Meeting(models.Model):
|
||||||
slugs = ('conflict', 'conflic2', 'conflic3')
|
slugs = ('conflict', 'conflic2', 'conflic3')
|
||||||
return ConstraintName.objects.filter(slug__in=slugs)
|
return ConstraintName.objects.filter(slug__in=slugs)
|
||||||
|
|
||||||
def json_url(self):
|
|
||||||
return "/meeting/%s/json" % (self.number, )
|
|
||||||
|
|
||||||
def base_url(self):
|
def base_url(self):
|
||||||
return "/meeting/%s" % (self.number, )
|
return "/meeting/%s" % (self.number, )
|
||||||
|
|
||||||
def json_dict(self, host_scheme):
|
|
||||||
# unfortunately, using the datetime aware json encoder seems impossible,
|
|
||||||
# so the dates are formatted as strings here.
|
|
||||||
agenda_url = ""
|
|
||||||
if self.schedule:
|
|
||||||
agenda_url = urljoin(host_scheme, self.schedule.base_url())
|
|
||||||
return {
|
|
||||||
'href': urljoin(host_scheme, self.json_url()),
|
|
||||||
'name': self.number,
|
|
||||||
'submission_start_date': fmt_date(self.get_submission_start_date()),
|
|
||||||
'submission_cut_off_date': fmt_date(self.get_submission_cut_off_date()),
|
|
||||||
'submission_correction_date': fmt_date(self.get_submission_correction_date()),
|
|
||||||
'date': fmt_date(self.date),
|
|
||||||
'agenda_href': agenda_url,
|
|
||||||
'city': self.city,
|
|
||||||
'country': self.country,
|
|
||||||
'time_zone': self.time_zone,
|
|
||||||
'venue_name': self.venue_name,
|
|
||||||
'venue_addr': self.venue_addr,
|
|
||||||
'break_area': self.break_area,
|
|
||||||
'reg_area': self.reg_area
|
|
||||||
}
|
|
||||||
|
|
||||||
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 +338,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
|
||||||
|
@ -428,13 +408,6 @@ class ResourceAssociation(models.Model):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.desc
|
return self.desc
|
||||||
|
|
||||||
def json_dict(self, host_scheme):
|
|
||||||
res1 = dict()
|
|
||||||
res1['name'] = self.name.slug
|
|
||||||
res1['icon'] = "/images/%s" % (self.icon)
|
|
||||||
res1['desc'] = self.desc
|
|
||||||
res1['resource_id'] = self.pk
|
|
||||||
return res1
|
|
||||||
|
|
||||||
class Room(models.Model):
|
class Room(models.Model):
|
||||||
meeting = ForeignKey(Meeting)
|
meeting = ForeignKey(Meeting)
|
||||||
|
@ -477,16 +450,19 @@ class Room(models.Model):
|
||||||
def dom_id(self):
|
def dom_id(self):
|
||||||
return "room%u" % (self.pk)
|
return "room%u" % (self.pk)
|
||||||
|
|
||||||
def json_url(self):
|
|
||||||
return "/meeting/%s/room/%s.json" % (self.meeting.number, self.id)
|
|
||||||
|
|
||||||
def json_dict(self, host_scheme):
|
|
||||||
return {
|
|
||||||
'href': urljoin(host_scheme, self.json_url()),
|
|
||||||
'name': self.name,
|
|
||||||
'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):
|
||||||
|
@ -678,29 +654,8 @@ class TimeSlot(models.Model):
|
||||||
dom_id = self.location.dom_id()
|
dom_id = self.location.dom_id()
|
||||||
return "%s_%s_%s" % (dom_id, self.time.strftime('%Y-%m-%d'), self.time.strftime('%H%M'))
|
return "%s_%s_%s" % (dom_id, self.time.strftime('%Y-%m-%d'), self.time.strftime('%H%M'))
|
||||||
|
|
||||||
def json_dict(self, host_scheme):
|
|
||||||
ts = dict()
|
|
||||||
ts['timeslot_id'] = self.id
|
|
||||||
ts['href'] = urljoin(host_scheme, self.json_url())
|
|
||||||
ts['room'] = self.get_location()
|
|
||||||
ts['roomtype'] = self.type.slug
|
|
||||||
if self.location is not None:
|
|
||||||
ts['capacity'] = self.location.capacity
|
|
||||||
ts["time"] = date_format(self.time, 'Hi')
|
|
||||||
ts["date"] = fmt_date(self.time)
|
|
||||||
ts["domid"] = self.js_identifier
|
|
||||||
following = self.slot_to_the_right
|
|
||||||
if following is not None:
|
|
||||||
ts["following_timeslot_id"] = following.id
|
|
||||||
return ts
|
|
||||||
|
|
||||||
def json_url(self):
|
|
||||||
return "/meeting/%s/timeslot/%s.json" % (self.meeting.number, self.id)
|
|
||||||
|
|
||||||
"""
|
|
||||||
This routine deletes all timeslots which are in the same time as this slot.
|
|
||||||
"""
|
|
||||||
def delete_concurrent_timeslots(self):
|
def delete_concurrent_timeslots(self):
|
||||||
|
"""Delete all timeslots which are in the same time as this slot"""
|
||||||
# can not include duration in filter, because there is no support
|
# can not include duration in filter, because there is no support
|
||||||
# for having it a WHERE clause.
|
# for having it a WHERE clause.
|
||||||
# below will delete self as well.
|
# below will delete self as well.
|
||||||
|
@ -804,25 +759,6 @@ class Schedule(models.Model):
|
||||||
def delete_assignments(self):
|
def delete_assignments(self):
|
||||||
self.assignments.all().delete()
|
self.assignments.all().delete()
|
||||||
|
|
||||||
def json_url(self):
|
|
||||||
return "%s.json" % self.base_url()
|
|
||||||
|
|
||||||
def json_dict(self, host_scheme):
|
|
||||||
sch = dict()
|
|
||||||
sch['schedule_id'] = self.id
|
|
||||||
sch['href'] = urljoin(host_scheme, self.json_url())
|
|
||||||
if self.visible:
|
|
||||||
sch['visible'] = "visible"
|
|
||||||
else:
|
|
||||||
sch['visible'] = "hidden"
|
|
||||||
if self.public:
|
|
||||||
sch['public'] = "public"
|
|
||||||
else:
|
|
||||||
sch['public'] = "private"
|
|
||||||
sch['owner'] = urljoin(host_scheme, self.owner.json_url())
|
|
||||||
# should include href to list of assignments, but they have no direct API yet.
|
|
||||||
return sch
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def qs_assignments_with_sessions(self):
|
def qs_assignments_with_sessions(self):
|
||||||
return self.assignments.filter(session__isnull=False)
|
return self.assignments.filter(session__isnull=False)
|
||||||
|
@ -878,39 +814,13 @@ class SchedTimeSessAssignment(models.Model):
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def json_url(self):
|
def meeting(self):
|
||||||
if not hasattr(self, '_cached_json_url'):
|
"""Get the meeting to which this assignment belongs"""
|
||||||
self._cached_json_url = "/meeting/%s/agenda/%s/%s/session/%u.json" % (
|
return self.session.meeting
|
||||||
self.schedule.meeting.number,
|
|
||||||
self.schedule.owner_email(),
|
|
||||||
self.schedule.name, self.id )
|
|
||||||
return self._cached_json_url
|
|
||||||
|
|
||||||
def json_dict(self, host_scheme):
|
def slot_type(self):
|
||||||
if not hasattr(self, '_cached_json_dict'):
|
"""Get the TimeSlotTypeName that applies to this assignment"""
|
||||||
ss = dict()
|
return self.timeslot.type
|
||||||
ss['assignment_id'] = self.id
|
|
||||||
ss['href'] = urljoin(host_scheme, self.json_url())
|
|
||||||
ss['timeslot_id'] = self.timeslot.id
|
|
||||||
|
|
||||||
efset = self.session.timeslotassignments.filter(schedule=self.schedule).order_by("timeslot__time")
|
|
||||||
if efset.count() > 1:
|
|
||||||
# now we know that there is some work to do finding the extendedfrom_id.
|
|
||||||
# loop through the list of items
|
|
||||||
previous = None
|
|
||||||
for efss in efset:
|
|
||||||
if efss.pk == self.pk:
|
|
||||||
extendedfrom = previous
|
|
||||||
break
|
|
||||||
previous = efss
|
|
||||||
if extendedfrom is not None:
|
|
||||||
ss['extendedfrom_id'] = extendedfrom.id
|
|
||||||
|
|
||||||
if self.session:
|
|
||||||
ss['session_id'] = self.session.id
|
|
||||||
ss["pinned"] = self.pinned
|
|
||||||
self._cached_json_dict = ss
|
|
||||||
return self._cached_json_dict
|
|
||||||
|
|
||||||
def slug(self):
|
def slug(self):
|
||||||
"""Return sensible id string for session, e.g. suitable for use as HTML anchor."""
|
"""Return sensible id string for session, e.g. suitable for use as HTML anchor."""
|
||||||
|
@ -928,12 +838,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
|
||||||
|
@ -1003,30 +913,6 @@ class Constraint(models.Model):
|
||||||
elif not self.target and self.person:
|
elif not self.target and self.person:
|
||||||
return "%s " % (self.person)
|
return "%s " % (self.person)
|
||||||
|
|
||||||
def json_url(self):
|
|
||||||
return "/meeting/%s/constraint/%s.json" % (self.meeting.number, self.id)
|
|
||||||
|
|
||||||
def json_dict(self, host_scheme):
|
|
||||||
ct1 = dict()
|
|
||||||
ct1['constraint_id'] = self.id
|
|
||||||
ct1['href'] = urljoin(host_scheme, self.json_url())
|
|
||||||
ct1['name'] = self.name.slug
|
|
||||||
if self.person is not None:
|
|
||||||
ct1['person_href'] = urljoin(host_scheme, self.person.json_url())
|
|
||||||
if self.source is not None:
|
|
||||||
ct1['source_href'] = urljoin(host_scheme, self.source.json_url())
|
|
||||||
if self.target is not None:
|
|
||||||
ct1['target_href'] = urljoin(host_scheme, self.target.json_url())
|
|
||||||
ct1['meeting_href'] = urljoin(host_scheme, self.meeting.json_url())
|
|
||||||
if self.time_relation:
|
|
||||||
ct1['time_relation'] = self.time_relation
|
|
||||||
ct1['time_relation_display'] = self.get_time_relation_display()
|
|
||||||
if self.timeranges.count():
|
|
||||||
ct1['timeranges_cant_meet'] = [t.slug for t in self.timeranges.all()]
|
|
||||||
timeranges_str = ", ".join([t.desc for t in self.timeranges.all()])
|
|
||||||
ct1['timeranges_display'] = "Can't meet %s" % timeranges_str
|
|
||||||
return ct1
|
|
||||||
|
|
||||||
|
|
||||||
class SessionPresentation(models.Model):
|
class SessionPresentation(models.Model):
|
||||||
session = ForeignKey('Session')
|
session = ForeignKey('Session')
|
||||||
|
@ -1098,6 +984,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
|
||||||
|
|
||||||
|
@ -1109,6 +1002,11 @@ class SessionQuerySet(models.QuerySet):
|
||||||
type__slug='regular'
|
type__slug='regular'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def requests(self):
|
||||||
|
"""Queryset containing sessions that may be handled as requests"""
|
||||||
|
return self.exclude(
|
||||||
|
type__in=('offagenda', 'reserved', 'unavail')
|
||||||
|
)
|
||||||
|
|
||||||
class Session(models.Model):
|
class Session(models.Model):
|
||||||
"""Session records that a group should have a session on the
|
"""Session records that a group should have a session on the
|
||||||
|
@ -1120,6 +1018,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=False, 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)
|
||||||
|
@ -1130,6 +1029,7 @@ class Session(models.Model):
|
||||||
scheduled = models.DateTimeField(null=True, blank=True)
|
scheduled = models.DateTimeField(null=True, blank=True)
|
||||||
modified = models.DateTimeField(auto_now=True)
|
modified = models.DateTimeField(auto_now=True)
|
||||||
remote_instructions = models.CharField(blank=True,max_length=1024)
|
remote_instructions = models.CharField(blank=True,max_length=1024)
|
||||||
|
on_agenda = models.BooleanField(default=True, help_text='Is this session visible on the meeting agenda?')
|
||||||
|
|
||||||
tombstone_for = models.ForeignKey('Session', blank=True, null=True, help_text="This session is the tombstone for a session that was rescheduled", on_delete=models.CASCADE)
|
tombstone_for = models.ForeignKey('Session', blank=True, null=True, help_text="This session is the tombstone for a session that was rescheduled", on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
@ -1323,92 +1223,10 @@ class Session(models.Model):
|
||||||
def official_timeslotassignment(self):
|
def official_timeslotassignment(self):
|
||||||
return self.timeslotassignments.filter(schedule__in=[self.meeting.schedule, self.meeting.schedule.base if self.meeting.schedule else None]).first()
|
return self.timeslotassignments.filter(schedule__in=[self.meeting.schedule, self.meeting.schedule.base if self.meeting.schedule else None]).first()
|
||||||
|
|
||||||
def constraints_dict(self, host_scheme):
|
|
||||||
constraint_list = []
|
|
||||||
for constraint in self.constraints():
|
|
||||||
ct1 = constraint.json_dict(host_scheme)
|
|
||||||
constraint_list.append(ct1)
|
|
||||||
|
|
||||||
for constraint in self.reverse_constraints():
|
|
||||||
ct1 = constraint.json_dict(host_scheme)
|
|
||||||
constraint_list.append(ct1)
|
|
||||||
return constraint_list
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def people_constraints(self):
|
def people_constraints(self):
|
||||||
return self.group.constraint_source_set.filter(meeting=self.meeting, name='bethere')
|
return self.group.constraint_source_set.filter(meeting=self.meeting, name='bethere')
|
||||||
|
|
||||||
def json_url(self):
|
|
||||||
return "/meeting/%s/session/%s.json" % (self.meeting.number, self.id)
|
|
||||||
|
|
||||||
def json_dict(self, host_scheme):
|
|
||||||
sess1 = dict()
|
|
||||||
sess1['href'] = urljoin(host_scheme, self.json_url())
|
|
||||||
if self.group is not None:
|
|
||||||
sess1['group'] = self.group.json_dict(host_scheme)
|
|
||||||
sess1['group_href'] = urljoin(host_scheme, self.group.json_url())
|
|
||||||
if self.group.parent is not None:
|
|
||||||
sess1['area'] = self.group.parent.acronym.upper()
|
|
||||||
sess1['description'] = self.group.name
|
|
||||||
sess1['group_id'] = str(self.group.pk)
|
|
||||||
reslist = []
|
|
||||||
for r in self.resources.all():
|
|
||||||
reslist.append(r.json_dict(host_scheme))
|
|
||||||
sess1['resources'] = reslist
|
|
||||||
sess1['session_id'] = str(self.pk)
|
|
||||||
sess1['name'] = self.name
|
|
||||||
sess1['title'] = self.short_name
|
|
||||||
sess1['short_name'] = self.short_name
|
|
||||||
sess1['bof'] = str(self.group.is_bof())
|
|
||||||
sess1['agenda_note'] = self.agenda_note
|
|
||||||
sess1['attendees'] = str(self.attendees)
|
|
||||||
sess1['joint_with_groups'] = self.joint_with_groups_acronyms()
|
|
||||||
|
|
||||||
# fish out scheduling information - eventually, we should pick
|
|
||||||
# this out in the caller instead
|
|
||||||
latest_event = None
|
|
||||||
first_event = None
|
|
||||||
|
|
||||||
if self.pk is not None:
|
|
||||||
if not hasattr(self, 'current_status') or not hasattr(self, 'requested_time'):
|
|
||||||
events = list(SchedulingEvent.objects.filter(session=self.pk).order_by('time', 'id'))
|
|
||||||
if events:
|
|
||||||
first_event = events[0]
|
|
||||||
latest_event = events[-1]
|
|
||||||
|
|
||||||
status_id = None
|
|
||||||
if hasattr(self, 'current_status'):
|
|
||||||
status_id = self.current_status
|
|
||||||
elif latest_event:
|
|
||||||
status_id = latest_event.status_id
|
|
||||||
|
|
||||||
sess1['status'] = SessionStatusName.objects.get(slug=status_id).name if status_id else None
|
|
||||||
if self.comments is not None:
|
|
||||||
sess1['comments'] = self.comments
|
|
||||||
|
|
||||||
requested_time = None
|
|
||||||
if hasattr(self, 'requested_time'):
|
|
||||||
requested_time = self.requested_time
|
|
||||||
elif first_event:
|
|
||||||
requested_time = first_event.time
|
|
||||||
sess1['requested_time'] = requested_time.strftime("%Y-%m-%d") if requested_time else None
|
|
||||||
|
|
||||||
|
|
||||||
requested_by = None
|
|
||||||
if hasattr(self, 'requested_by'):
|
|
||||||
requested_by = self.requested_by
|
|
||||||
elif first_event:
|
|
||||||
requested_by = first_event.by_id
|
|
||||||
|
|
||||||
if requested_by is not None:
|
|
||||||
requested_by_person = Person.objects.filter(pk=requested_by).first()
|
|
||||||
if requested_by_person:
|
|
||||||
sess1['requested_by'] = str(requested_by_person)
|
|
||||||
|
|
||||||
sess1['requested_duration']= "%.1f" % (float(self.requested_duration.seconds) / 3600)
|
|
||||||
sess1['special_request'] = str(self.special_request_token)
|
|
||||||
return sess1
|
|
||||||
|
|
||||||
def agenda_text(self):
|
def agenda_text(self):
|
||||||
doc = self.agenda()
|
doc = self.agenda()
|
||||||
if doc:
|
if doc:
|
||||||
|
|
|
@ -67,4 +67,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
|
|
@ -11,8 +11,9 @@ import debug # pyflakes:ignore
|
||||||
|
|
||||||
from ietf.doc.factories import DocumentFactory
|
from ietf.doc.factories import DocumentFactory
|
||||||
from ietf.group.factories import GroupFactory, RoleFactory
|
from ietf.group.factories import GroupFactory, RoleFactory
|
||||||
from ietf.group.models import Group
|
from ietf.group.models import Group
|
||||||
from ietf.meeting.models import (Meeting, Room, TimeSlot, Session, Schedule, SchedTimeSessAssignment,
|
from ietf.meeting.factories import SessionFactory
|
||||||
|
from ietf.meeting.models import (Meeting, Room, TimeSlot, Schedule, SchedTimeSessAssignment,
|
||||||
ResourceAssociation, SessionPresentation, UrlResource, SchedulingEvent)
|
ResourceAssociation, SessionPresentation, UrlResource, SchedulingEvent)
|
||||||
from ietf.meeting.helpers import create_interim_meeting
|
from ietf.meeting.helpers import create_interim_meeting
|
||||||
from ietf.name.models import RoomResourceName
|
from ietf.name.models import RoomResourceName
|
||||||
|
@ -24,11 +25,11 @@ def make_interim_meeting(group,date,status='sched'):
|
||||||
system_person = Person.objects.get(name="(System)")
|
system_person = Person.objects.get(name="(System)")
|
||||||
time = datetime.datetime.combine(date, datetime.time(9))
|
time = datetime.datetime.combine(date, datetime.time(9))
|
||||||
meeting = create_interim_meeting(group=group,date=date)
|
meeting = create_interim_meeting(group=group,date=date)
|
||||||
session = Session.objects.create(meeting=meeting, group=group,
|
session = SessionFactory(meeting=meeting, group=group,
|
||||||
attendees=10,
|
attendees=10,
|
||||||
requested_duration=datetime.timedelta(minutes=20),
|
requested_duration=datetime.timedelta(minutes=20),
|
||||||
remote_instructions='http://webex.com',
|
remote_instructions='http://webex.com',
|
||||||
type_id='regular')
|
add_to_schedule=False)
|
||||||
SchedulingEvent.objects.create(session=session, status_id=status, by=system_person)
|
SchedulingEvent.objects.create(session=session, status_id=status, by=system_person)
|
||||||
slot = TimeSlot.objects.create(
|
slot = TimeSlot.objects.create(
|
||||||
meeting=meeting,
|
meeting=meeting,
|
||||||
|
@ -121,52 +122,52 @@ def make_meeting_test_data(meeting=None, create_interims=False):
|
||||||
time=datetime.datetime.combine(session_date, datetime.time(11,0)))
|
time=datetime.datetime.combine(session_date, datetime.time(11,0)))
|
||||||
# mars WG
|
# mars WG
|
||||||
mars = Group.objects.get(acronym='mars')
|
mars = Group.objects.get(acronym='mars')
|
||||||
mars_session = Session.objects.create(meeting=meeting, group=mars,
|
mars_session = SessionFactory(meeting=meeting, group=mars,
|
||||||
attendees=10, requested_duration=datetime.timedelta(minutes=50),
|
attendees=10, requested_duration=datetime.timedelta(minutes=50),
|
||||||
type_id='regular')
|
add_to_schedule=False)
|
||||||
SchedulingEvent.objects.create(session=mars_session, status_id='schedw', by=system_person)
|
SchedulingEvent.objects.create(session=mars_session, status_id='schedw', by=system_person)
|
||||||
SchedTimeSessAssignment.objects.create(timeslot=slot1, session=mars_session, schedule=schedule)
|
SchedTimeSessAssignment.objects.create(timeslot=slot1, session=mars_session, schedule=schedule)
|
||||||
SchedTimeSessAssignment.objects.create(timeslot=slot2, session=mars_session, schedule=unofficial_schedule)
|
SchedTimeSessAssignment.objects.create(timeslot=slot2, session=mars_session, schedule=unofficial_schedule)
|
||||||
|
|
||||||
# ames WG
|
# ames WG
|
||||||
ames_session = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym="ames"),
|
ames_session = SessionFactory(meeting=meeting, group=Group.objects.get(acronym="ames"),
|
||||||
attendees=10,
|
attendees=10,
|
||||||
requested_duration=datetime.timedelta(minutes=60),
|
requested_duration=datetime.timedelta(minutes=60),
|
||||||
type_id='regular')
|
add_to_schedule=False)
|
||||||
SchedulingEvent.objects.create(session=ames_session, status_id='schedw', by=system_person)
|
SchedulingEvent.objects.create(session=ames_session, status_id='schedw', by=system_person)
|
||||||
SchedTimeSessAssignment.objects.create(timeslot=slot2, session=ames_session, schedule=schedule)
|
SchedTimeSessAssignment.objects.create(timeslot=slot2, session=ames_session, schedule=schedule)
|
||||||
SchedTimeSessAssignment.objects.create(timeslot=slot1, session=ames_session, schedule=unofficial_schedule)
|
SchedTimeSessAssignment.objects.create(timeslot=slot1, session=ames_session, schedule=unofficial_schedule)
|
||||||
|
|
||||||
# IESG breakfast
|
# IESG breakfast
|
||||||
iesg_session = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym="iesg"),
|
iesg_session = SessionFactory(meeting=meeting, group=Group.objects.get(acronym="iesg"),
|
||||||
name="IESG Breakfast", attendees=25,
|
name="IESG Breakfast", attendees=25,
|
||||||
requested_duration=datetime.timedelta(minutes=60),
|
requested_duration=datetime.timedelta(minutes=60),
|
||||||
type_id="lead")
|
type_id="lead", purpose_id='closed_meeting', add_to_schedule=False)
|
||||||
SchedulingEvent.objects.create(session=iesg_session, status_id='schedw', by=system_person)
|
SchedulingEvent.objects.create(session=iesg_session, status_id='schedw', by=system_person)
|
||||||
SchedTimeSessAssignment.objects.create(timeslot=breakfast_slot, session=iesg_session, schedule=schedule)
|
SchedTimeSessAssignment.objects.create(timeslot=breakfast_slot, session=iesg_session, schedule=schedule)
|
||||||
# No breakfast on unofficial schedule
|
# No breakfast on unofficial schedule
|
||||||
|
|
||||||
# Registration
|
# Registration
|
||||||
reg_session = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym="secretariat"),
|
reg_session = SessionFactory(meeting=meeting, group=Group.objects.get(acronym="secretariat"),
|
||||||
name="Registration", attendees=250,
|
name="Registration", attendees=250,
|
||||||
requested_duration=datetime.timedelta(minutes=480),
|
requested_duration=datetime.timedelta(minutes=480),
|
||||||
type_id="reg")
|
type_id="reg", purpose_id='admin', add_to_schedule=False)
|
||||||
SchedulingEvent.objects.create(session=reg_session, status_id='schedw', by=system_person)
|
SchedulingEvent.objects.create(session=reg_session, status_id='schedw', by=system_person)
|
||||||
SchedTimeSessAssignment.objects.create(timeslot=reg_slot, session=reg_session, schedule=base_schedule)
|
SchedTimeSessAssignment.objects.create(timeslot=reg_slot, session=reg_session, schedule=base_schedule)
|
||||||
|
|
||||||
# Break
|
# Break
|
||||||
break_session = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym="secretariat"),
|
break_session = SessionFactory(meeting=meeting, group=Group.objects.get(acronym="secretariat"),
|
||||||
name="Morning Break", attendees=250,
|
name="Morning Break", attendees=250,
|
||||||
requested_duration=datetime.timedelta(minutes=30),
|
requested_duration=datetime.timedelta(minutes=30),
|
||||||
type_id="break")
|
type_id="break", purpose_id='social', add_to_schedule=False)
|
||||||
SchedulingEvent.objects.create(session=break_session, status_id='schedw', by=system_person)
|
SchedulingEvent.objects.create(session=break_session, status_id='schedw', by=system_person)
|
||||||
SchedTimeSessAssignment.objects.create(timeslot=break_slot, session=break_session, schedule=base_schedule)
|
SchedTimeSessAssignment.objects.create(timeslot=break_slot, session=break_session, schedule=base_schedule)
|
||||||
|
|
||||||
# IETF Plenary
|
# IETF Plenary
|
||||||
plenary_session = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym="ietf"),
|
plenary_session = SessionFactory(meeting=meeting, group=Group.objects.get(acronym="ietf"),
|
||||||
name="IETF Plenary", attendees=250,
|
name="IETF Plenary", attendees=250,
|
||||||
requested_duration=datetime.timedelta(minutes=60),
|
requested_duration=datetime.timedelta(minutes=60),
|
||||||
type_id="plenary")
|
type_id="plenary", purpose_id='plenary', add_to_schedule=False)
|
||||||
SchedulingEvent.objects.create(session=plenary_session, status_id='schedw', by=system_person)
|
SchedulingEvent.objects.create(session=plenary_session, status_id='schedw', by=system_person)
|
||||||
SchedTimeSessAssignment.objects.create(timeslot=plenary_slot, session=plenary_session, schedule=schedule)
|
SchedTimeSessAssignment.objects.create(timeslot=plenary_slot, session=plenary_session, schedule=schedule)
|
||||||
|
|
||||||
|
|
|
@ -1,501 +0,0 @@
|
||||||
# Copyright The IETF Trust 2013-2020, All Rights Reserved
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
from urllib.parse import urlsplit
|
|
||||||
|
|
||||||
from django.urls import reverse as urlreverse
|
|
||||||
|
|
||||||
import debug # pyflakes:ignore
|
|
||||||
|
|
||||||
from ietf.name.models import TimerangeName
|
|
||||||
from ietf.group.models import Group
|
|
||||||
from ietf.meeting.models import Schedule, TimeSlot, Session, SchedTimeSessAssignment, Meeting, Constraint
|
|
||||||
from ietf.meeting.test_data import make_meeting_test_data
|
|
||||||
from ietf.person.models import Person
|
|
||||||
from ietf.utils.test_utils import TestCase
|
|
||||||
from ietf.utils.mail import outbox
|
|
||||||
|
|
||||||
|
|
||||||
class ApiTests(TestCase):
|
|
||||||
def test_update_schedule(self):
|
|
||||||
meeting = make_meeting_test_data()
|
|
||||||
schedule = Schedule.objects.get(meeting__number=72,name="test-schedule")
|
|
||||||
mars_session = Session.objects.filter(meeting=meeting, group__acronym="mars").first()
|
|
||||||
ames_session = Session.objects.filter(meeting=meeting, group__acronym="ames").first()
|
|
||||||
|
|
||||||
mars_scheduled = SchedTimeSessAssignment.objects.get(session=mars_session,schedule__name='test-schedule')
|
|
||||||
mars_slot = mars_scheduled.timeslot
|
|
||||||
|
|
||||||
ames_scheduled = SchedTimeSessAssignment.objects.get(session=ames_session,schedule__name='test-schedule')
|
|
||||||
ames_slot = ames_scheduled.timeslot
|
|
||||||
|
|
||||||
def do_unschedule(assignment):
|
|
||||||
url = urlreverse("ietf.meeting.ajax.assignment_json",
|
|
||||||
kwargs=dict(num=assignment.session.meeting.number,
|
|
||||||
owner=assignment.schedule.owner_email(),
|
|
||||||
name=assignment.schedule.name,
|
|
||||||
assignment_id=assignment.pk,))
|
|
||||||
return self.client.delete(url)
|
|
||||||
|
|
||||||
def do_schedule(schedule,session,timeslot):
|
|
||||||
url = urlreverse("ietf.meeting.ajax.assignments_json",
|
|
||||||
kwargs=dict(num=session.meeting.number,
|
|
||||||
owner=schedule.owner_email(),
|
|
||||||
name=schedule.name,))
|
|
||||||
post_data = '{ "session_id": "%s", "timeslot_id": "%s" }'%(session.pk,timeslot.pk)
|
|
||||||
return self.client.post(url,post_data,content_type='application/x-www-form-urlencoded')
|
|
||||||
|
|
||||||
def do_extend(schedule, assignment):
|
|
||||||
session = assignment.session
|
|
||||||
url = urlreverse("ietf.meeting.ajax.assignments_json",
|
|
||||||
kwargs=dict(num=session.meeting.number,
|
|
||||||
owner=schedule.owner_email(),
|
|
||||||
name=schedule.name,))
|
|
||||||
post_data = '{ "session_id": "%s", "timeslot_id": "%s", "extendedfrom_id": "%s" }'%(session.pk,assignment.timeslot.slot_to_the_right.pk,assignment.pk)
|
|
||||||
|
|
||||||
|
|
||||||
return self.client.post(url,post_data,content_type='application/x-www-form-urlencoded')
|
|
||||||
|
|
||||||
# not logged in
|
|
||||||
# faulty delete
|
|
||||||
r = do_unschedule(mars_scheduled)
|
|
||||||
self.assertEqual(r.status_code, 403)
|
|
||||||
self.assertEqual(SchedTimeSessAssignment.objects.get(pk=mars_scheduled.pk).session, mars_session)
|
|
||||||
# faulty post
|
|
||||||
r = do_schedule(schedule,ames_session,mars_slot)
|
|
||||||
self.assertEqual(r.status_code, 403)
|
|
||||||
|
|
||||||
# logged in as non-owner
|
|
||||||
# faulty delete
|
|
||||||
self.client.login(username="ad", password="ad+password")
|
|
||||||
r = do_unschedule(mars_scheduled)
|
|
||||||
self.assertEqual(r.status_code, 403)
|
|
||||||
self.assertTrue("error" in r.json())
|
|
||||||
# faulty post
|
|
||||||
r = do_schedule(schedule,ames_session,mars_slot)
|
|
||||||
self.assertEqual(r.status_code, 403)
|
|
||||||
|
|
||||||
# Put ames in the same timeslot as mars
|
|
||||||
self.client.login(username="plain", password='plain+password')
|
|
||||||
r = do_unschedule(ames_scheduled)
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
self.assertNotIn("error", r.json())
|
|
||||||
|
|
||||||
r = do_schedule(schedule,ames_session,mars_slot)
|
|
||||||
self.assertEqual(r.status_code, 201)
|
|
||||||
|
|
||||||
# Move the two timeslots close enough together for extension to work
|
|
||||||
ames_slot_qs=TimeSlot.objects.filter(id=ames_slot.id)
|
|
||||||
ames_slot_qs.update(time=mars_slot.time+mars_slot.duration+datetime.timedelta(minutes=10))
|
|
||||||
|
|
||||||
# Extend the mars session
|
|
||||||
r = do_extend(schedule,mars_scheduled)
|
|
||||||
self.assertEqual(r.status_code, 201)
|
|
||||||
self.assertTrue("error" not in r.json())
|
|
||||||
self.assertEqual(mars_session.timeslotassignments.filter(schedule__name='test-schedule').count(),2)
|
|
||||||
|
|
||||||
# Unschedule mars
|
|
||||||
r = do_unschedule(mars_scheduled)
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
self.assertNotIn("error", r.json())
|
|
||||||
# Make sure it got both the original and extended session
|
|
||||||
self.assertEqual(mars_session.timeslotassignments.filter(schedule__name='test-schedule').count(),0)
|
|
||||||
|
|
||||||
self.assertEqual(SchedTimeSessAssignment.objects.get(session=ames_session,schedule__name='test-schedule').timeslot, mars_slot)
|
|
||||||
|
|
||||||
|
|
||||||
def test_constraints_json(self):
|
|
||||||
meeting = make_meeting_test_data()
|
|
||||||
session = Session.objects.filter(meeting=meeting, group__acronym="mars").select_related("group").first()
|
|
||||||
c_ames = Constraint.objects.create(meeting=meeting, source=session.group,
|
|
||||||
target=Group.objects.get(acronym="ames"),
|
|
||||||
name_id="conflict")
|
|
||||||
|
|
||||||
c_person = Constraint.objects.create(meeting=meeting, source=session.group,
|
|
||||||
person=Person.objects.get(user__username="ad"),
|
|
||||||
name_id="bethere")
|
|
||||||
|
|
||||||
c_adjacent = Constraint.objects.create(meeting=meeting, source=session.group,
|
|
||||||
target=Group.objects.get(acronym="irg"),
|
|
||||||
name_id="wg_adjacent")
|
|
||||||
|
|
||||||
c_time_relation = Constraint.objects.create(meeting=meeting, source=session.group,
|
|
||||||
time_relation='subsequent-days',
|
|
||||||
name_id="time_relation")
|
|
||||||
|
|
||||||
c_timerange = Constraint.objects.create(meeting=meeting, source=session.group,
|
|
||||||
name_id="timerange")
|
|
||||||
c_timerange.timeranges.set(TimerangeName.objects.filter(slug__startswith='monday'))
|
|
||||||
|
|
||||||
r = self.client.get(urlreverse("ietf.meeting.ajax.session_constraints", kwargs=dict(num=meeting.number, sessionid=session.pk)))
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
constraints = r.json()
|
|
||||||
expected_keys = set([c_ames.pk, c_person.pk, c_adjacent.pk, c_time_relation.pk, c_timerange.pk])
|
|
||||||
self.assertEqual(expected_keys, set(c["constraint_id"] for c in constraints))
|
|
||||||
|
|
||||||
def test_meeting_json(self):
|
|
||||||
meeting = make_meeting_test_data()
|
|
||||||
|
|
||||||
r = self.client.get(urlreverse("ietf.meeting.ajax.meeting_json", kwargs=dict(num=meeting.number)))
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
info = r.json()
|
|
||||||
self.assertEqual(info["name"], meeting.number)
|
|
||||||
|
|
||||||
def test_get_room_json(self):
|
|
||||||
meeting = make_meeting_test_data()
|
|
||||||
room = meeting.room_set.first()
|
|
||||||
|
|
||||||
r = self.client.get(urlreverse("ietf.meeting.ajax.timeslot_roomurl", kwargs=dict(num=meeting.number, roomid=room.pk)))
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
info = r.json()
|
|
||||||
self.assertEqual(info["name"], room.name)
|
|
||||||
|
|
||||||
def test_create_new_room(self):
|
|
||||||
meeting = make_meeting_test_data()
|
|
||||||
timeslots_before = meeting.timeslot_set.filter(type='regular').count()
|
|
||||||
url = urlreverse("ietf.meeting.ajax.timeslot_roomsurl", kwargs=dict(num=meeting.number))
|
|
||||||
|
|
||||||
post_data = { "name": "new room", "capacity": "50" , "resources": [], "session_types":['regular']}
|
|
||||||
|
|
||||||
# unauthorized post
|
|
||||||
r = self.client.post(url, post_data)
|
|
||||||
self.assertEqual(r.status_code, 302)
|
|
||||||
self.assertTrue(not meeting.room_set.filter(name="new room"))
|
|
||||||
|
|
||||||
# create room
|
|
||||||
self.client.login(username="secretary", password="secretary+password")
|
|
||||||
r = self.client.post(url, post_data)
|
|
||||||
self.assertEqual(r.status_code, 302)
|
|
||||||
self.assertTrue(meeting.room_set.filter(name="new room"))
|
|
||||||
|
|
||||||
timeslots_after = meeting.timeslot_set.filter(type='regular').count()
|
|
||||||
# It's not clear that what that ajax function is doing is the right thing to do,
|
|
||||||
# but it currently makes a new timeslot for any existing timeslot.
|
|
||||||
# The condition tested below relies on the timeslots before this test all having different start and end times
|
|
||||||
self.assertEqual( timeslots_after, 2 * timeslots_before)
|
|
||||||
|
|
||||||
def test_delete_room(self):
|
|
||||||
meeting = make_meeting_test_data()
|
|
||||||
room = meeting.room_set.first()
|
|
||||||
timeslots_before = list(room.timeslot_set.values_list("pk", flat=True))
|
|
||||||
|
|
||||||
url = urlreverse("ietf.meeting.ajax.timeslot_roomurl", kwargs=dict(num=meeting.number, roomid=room.pk))
|
|
||||||
|
|
||||||
# unauthorized delete
|
|
||||||
r = self.client.delete(url)
|
|
||||||
self.assertEqual(r.status_code, 302)
|
|
||||||
self.assertTrue(meeting.room_set.filter(pk=room.pk))
|
|
||||||
|
|
||||||
# delete
|
|
||||||
self.client.login(username="secretary", password="secretary+password")
|
|
||||||
r = self.client.delete(url)
|
|
||||||
self.assertTrue(not meeting.room_set.filter(pk=room.pk))
|
|
||||||
self.assertTrue(not TimeSlot.objects.filter(pk__in=timeslots_before))
|
|
||||||
|
|
||||||
# This really belongs in group tests
|
|
||||||
def test_group_json(self):
|
|
||||||
make_meeting_test_data()
|
|
||||||
group = Group.objects.get(acronym="mars")
|
|
||||||
|
|
||||||
url = urlreverse("ietf.group.views.group_json", kwargs=dict(acronym=group.acronym))
|
|
||||||
r = self.client.get(url)
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
info = r.json()
|
|
||||||
self.assertEqual(info["name"], group.name)
|
|
||||||
|
|
||||||
# This really belongs in person tests
|
|
||||||
def test_person_json(self):
|
|
||||||
make_meeting_test_data()
|
|
||||||
person = Person.objects.get(user__username="ad")
|
|
||||||
|
|
||||||
url = urlreverse("ietf.person.ajax.person_json", kwargs=dict(personid=person.pk))
|
|
||||||
r = self.client.get(url)
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
info = r.json()
|
|
||||||
self.assertEqual(info["name"], person.name)
|
|
||||||
|
|
||||||
def test_sessions_json(self):
|
|
||||||
meeting = make_meeting_test_data()
|
|
||||||
|
|
||||||
url = urlreverse("ietf.meeting.ajax.sessions_json",kwargs=dict(num=meeting.number))
|
|
||||||
r = self.client.get(url)
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
info = r.json()
|
|
||||||
self.assertEqual(set([x['short_name'] for x in info]),set([s.session.short_name for s in meeting.schedule.assignments.filter(session__type_id='regular')]))
|
|
||||||
|
|
||||||
schedule = meeting.schedule
|
|
||||||
url = urlreverse("ietf.meeting.ajax.assignments_json",
|
|
||||||
kwargs=dict(num=meeting.number,owner=schedule.owner_email(),name=schedule.name))
|
|
||||||
r = self.client.get(url)
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
info = r.json()
|
|
||||||
self.assertEqual(len(info),schedule.assignments.count())
|
|
||||||
|
|
||||||
|
|
||||||
def test_slot_json(self):
|
|
||||||
meeting = make_meeting_test_data()
|
|
||||||
slot = meeting.timeslot_set.all()[0]
|
|
||||||
|
|
||||||
url = urlreverse("ietf.meeting.ajax.timeslot_sloturl",
|
|
||||||
kwargs=dict(num=meeting.number, slotid=slot.pk))
|
|
||||||
r = self.client.get(url)
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
info = r.json()
|
|
||||||
self.assertEqual(info["timeslot_id"], slot.pk)
|
|
||||||
|
|
||||||
def test_create_new_slot(self):
|
|
||||||
meeting = make_meeting_test_data()
|
|
||||||
|
|
||||||
slot_time = datetime.date.today()
|
|
||||||
|
|
||||||
url = urlreverse("ietf.meeting.ajax.timeslot_slotsurl",
|
|
||||||
kwargs=dict(num=meeting.number))
|
|
||||||
post_data = {
|
|
||||||
'type' : 'plenary',
|
|
||||||
'time' : slot_time.strftime("%Y-%m-%d"),
|
|
||||||
'duration': '08:00:00',
|
|
||||||
}
|
|
||||||
|
|
||||||
# unauthorized post
|
|
||||||
prior_slotcount = meeting.timeslot_set.count()
|
|
||||||
self.client.login(username="ad", password="ad+password")
|
|
||||||
r = self.client.post(url, post_data)
|
|
||||||
self.assertEqual(r.status_code, 403)
|
|
||||||
self.assertEqual(meeting.timeslot_set.count(),prior_slotcount)
|
|
||||||
|
|
||||||
# create slot
|
|
||||||
self.client.login(username="secretary", password="secretary+password")
|
|
||||||
r = self.client.post(url, post_data)
|
|
||||||
self.assertEqual(r.status_code, 201)
|
|
||||||
self.assertTrue(meeting.timeslot_set.filter(time=slot_time))
|
|
||||||
self.assertEqual(meeting.timeslot_set.count(),prior_slotcount+1)
|
|
||||||
|
|
||||||
def test_delete_slot(self):
|
|
||||||
meeting = make_meeting_test_data()
|
|
||||||
slot = meeting.timeslot_set.all()[0]
|
|
||||||
|
|
||||||
url = urlreverse("ietf.meeting.ajax.timeslot_sloturl",
|
|
||||||
kwargs=dict(num=meeting.number, slotid=slot.pk))
|
|
||||||
|
|
||||||
# unauthorized delete
|
|
||||||
self.client.login(username="ad", password="ad+password")
|
|
||||||
r = self.client.delete(url)
|
|
||||||
self.assertEqual(r.status_code, 403)
|
|
||||||
|
|
||||||
# delete
|
|
||||||
self.client.login(username="secretary", password="secretary+password")
|
|
||||||
self.client.delete(url)
|
|
||||||
self.assertTrue(not meeting.timeslot_set.filter(pk=slot.pk))
|
|
||||||
|
|
||||||
def test_schedule_json(self):
|
|
||||||
meeting = make_meeting_test_data()
|
|
||||||
|
|
||||||
url = urlreverse("ietf.meeting.ajax.schedule_infourl",
|
|
||||||
kwargs=dict(num=meeting.number,
|
|
||||||
owner=meeting.schedule.owner_email(),
|
|
||||||
name=meeting.schedule.name))
|
|
||||||
|
|
||||||
r = self.client.get(url)
|
|
||||||
info = r.json()
|
|
||||||
self.assertEqual(info["schedule_id"], meeting.schedule.pk)
|
|
||||||
|
|
||||||
def test_create_new_schedule(self):
|
|
||||||
meeting = make_meeting_test_data()
|
|
||||||
|
|
||||||
url = urlreverse("ietf.meeting.ajax.schedule_infosurl",
|
|
||||||
kwargs=dict(num=meeting.number))
|
|
||||||
post_data = {
|
|
||||||
'name': 'new-schedule',
|
|
||||||
}
|
|
||||||
|
|
||||||
# unauthorized post
|
|
||||||
self.client.login(username="plain", password="plain+password")
|
|
||||||
r = self.client.post(url, post_data)
|
|
||||||
self.assertEqual(r.status_code, 403)
|
|
||||||
self.assertTrue(not meeting.schedule_set.filter(name='new-schedule'))
|
|
||||||
|
|
||||||
# create new schedule
|
|
||||||
self.client.login(username="secretary", password="secretary+password")
|
|
||||||
r = self.client.post(url, post_data)
|
|
||||||
self.assertEqual(r.status_code, 302)
|
|
||||||
self.assertTrue(meeting.schedule_set.filter(name='new-schedule'))
|
|
||||||
|
|
||||||
def test_update_meeting_schedule(self):
|
|
||||||
meeting = make_meeting_test_data()
|
|
||||||
|
|
||||||
self.assertTrue(meeting.schedule.visible)
|
|
||||||
|
|
||||||
url = urlreverse("ietf.meeting.ajax.schedule_infourl",
|
|
||||||
kwargs=dict(num=meeting.number,
|
|
||||||
owner=meeting.schedule.owner_email(),
|
|
||||||
name=meeting.schedule.name))
|
|
||||||
|
|
||||||
post_data = {
|
|
||||||
'visible': 'false',
|
|
||||||
'name': 'new-test-name',
|
|
||||||
}
|
|
||||||
|
|
||||||
# unauthorized posts
|
|
||||||
self.client.logout()
|
|
||||||
r = self.client.post(url, post_data)
|
|
||||||
self.assertEqual(r.status_code, 403)
|
|
||||||
self.client.login(username="ad", password="ad+password")
|
|
||||||
r = self.client.post(url, post_data)
|
|
||||||
self.assertEqual(r.status_code, 403)
|
|
||||||
|
|
||||||
# change schedule
|
|
||||||
self.client.login(username="secretary", password="secretary+password")
|
|
||||||
r = self.client.post(url, post_data)
|
|
||||||
self.assertEqual(r.status_code, 302)
|
|
||||||
changed_schedule = Schedule.objects.get(pk=meeting.schedule.pk)
|
|
||||||
self.assertTrue(not changed_schedule.visible)
|
|
||||||
self.assertEqual(changed_schedule.name, "new-test-name")
|
|
||||||
|
|
||||||
def test_delete_schedule(self):
|
|
||||||
meeting = make_meeting_test_data()
|
|
||||||
|
|
||||||
url = urlreverse("ietf.meeting.ajax.schedule_infourl",
|
|
||||||
kwargs=dict(num=meeting.number,
|
|
||||||
owner=meeting.schedule.owner_email(),
|
|
||||||
name=meeting.schedule.name))
|
|
||||||
# unauthorized delete
|
|
||||||
self.client.login(username="plain", password="plain+password")
|
|
||||||
r = self.client.delete(url)
|
|
||||||
self.assertEqual(r.status_code, 403)
|
|
||||||
|
|
||||||
# delete
|
|
||||||
self.client.login(username="secretary", password="secretary+password")
|
|
||||||
r = self.client.delete(url)
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
self.assertTrue(not Schedule.objects.filter(pk=meeting.schedule.pk))
|
|
||||||
|
|
||||||
def test_set_meeting_schedule(self):
|
|
||||||
meeting = make_meeting_test_data()
|
|
||||||
schedule = meeting.schedule
|
|
||||||
|
|
||||||
url = urlreverse("ietf.meeting.ajax.meeting_json",
|
|
||||||
kwargs=dict(num=meeting.number))
|
|
||||||
post_data = {
|
|
||||||
"schedule": "",
|
|
||||||
}
|
|
||||||
# unauthorized post
|
|
||||||
self.client.login(username="ad", password="ad+password")
|
|
||||||
r = self.client.post(url, post_data)
|
|
||||||
self.assertEqual(r.status_code, 403)
|
|
||||||
|
|
||||||
# clear
|
|
||||||
self.client.login(username="secretary", password="secretary+password")
|
|
||||||
r = self.client.post(url, post_data)
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
self.assertTrue(not Meeting.objects.get(pk=meeting.pk).schedule)
|
|
||||||
|
|
||||||
# set schedule - first fail with non-public
|
|
||||||
post_data = {
|
|
||||||
"schedule": schedule.name,
|
|
||||||
}
|
|
||||||
schedule.public = False
|
|
||||||
schedule.save()
|
|
||||||
|
|
||||||
r = self.client.post(url, post_data)
|
|
||||||
self.assertTrue(r.status_code != 200)
|
|
||||||
self.assertTrue(not Meeting.objects.get(pk=meeting.pk).schedule)
|
|
||||||
|
|
||||||
# then go through with public
|
|
||||||
schedule.public = True
|
|
||||||
schedule.save()
|
|
||||||
|
|
||||||
# Setting a meeting as official no longer sends mail immediately
|
|
||||||
prior_length= len(outbox)
|
|
||||||
r = self.client.post(url, post_data)
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
self.assertEqual(Meeting.objects.get(pk=meeting.pk).schedule, schedule)
|
|
||||||
self.assertEqual(len(outbox),prior_length)
|
|
||||||
|
|
||||||
def test_read_only(self):
|
|
||||||
meeting = make_meeting_test_data()
|
|
||||||
|
|
||||||
# Secretariat
|
|
||||||
self.client.login(username="secretary", password="secretary+password")
|
|
||||||
url = '/meeting/%s/agenda/%s/%s/permissions' % (meeting.number, meeting.schedule.owner.email_address(), meeting.schedule.name);
|
|
||||||
r = self.client.get(url)
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
|
|
||||||
info = r.json()
|
|
||||||
self.assertEqual(info['secretariat'], True)
|
|
||||||
self.assertEqual(urlsplit(info['owner_href'])[2], "/person/%s.json" % meeting.schedule.owner_id)
|
|
||||||
self.assertEqual(info['read_only'], True)
|
|
||||||
self.assertEqual(info['save_perm'], True)
|
|
||||||
|
|
||||||
# owner
|
|
||||||
self.client.login(username=meeting.schedule.owner.user.username,
|
|
||||||
password=meeting.schedule.owner.user.username+"+password")
|
|
||||||
url = '/meeting/%s/agenda/%s/%s/permissions' % (meeting.number, meeting.schedule.owner.email_address(), meeting.schedule.name);
|
|
||||||
r = self.client.get(url)
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
|
|
||||||
info = r.json()
|
|
||||||
self.assertEqual(info['secretariat'], False)
|
|
||||||
self.assertEqual(info['read_only'], False)
|
|
||||||
self.assertEqual(info['save_perm'], False)
|
|
||||||
|
|
||||||
def test_update_timeslot_pinned(self):
|
|
||||||
meeting = make_meeting_test_data()
|
|
||||||
scheduled = SchedTimeSessAssignment.objects.filter(
|
|
||||||
session__meeting=meeting, session__group__acronym="mars").first()
|
|
||||||
|
|
||||||
url = '/meeting/%s/agenda/%s/%s/session/%u.json' % (meeting.number, meeting.schedule.owner_email(), meeting.schedule.name, scheduled.pk)
|
|
||||||
|
|
||||||
post_data = {
|
|
||||||
"pinned": True
|
|
||||||
}
|
|
||||||
|
|
||||||
# unauthorized post gets failure (no redirect)
|
|
||||||
r = self.client.put(url, post_data)
|
|
||||||
self.assertEqual(r.status_code, 403,
|
|
||||||
"post to %s should have failed, no permission, got: %u/%s" %
|
|
||||||
(url, r.status_code, r.content))
|
|
||||||
self.assertTrue(not SchedTimeSessAssignment.objects.get(pk=scheduled.pk).pinned)
|
|
||||||
|
|
||||||
# set pinned
|
|
||||||
meeting.schedule.owner = Person.objects.get(user__username="secretary")
|
|
||||||
meeting.schedule.save()
|
|
||||||
|
|
||||||
# need to rebuild URL, since the schedule owner has changed.
|
|
||||||
url = '/meeting/%s/agenda/%s/%s/session/%u.json' % (meeting.number, meeting.schedule.owner_email(), meeting.schedule.name, scheduled.pk)
|
|
||||||
|
|
||||||
self.client.login(username="secretary", password="secretary+password")
|
|
||||||
r = self.client.put(url, post_data)
|
|
||||||
self.assertEqual(r.status_code, 200,
|
|
||||||
"post to %s should have worked, but got: %u/%s" %
|
|
||||||
(url, r.status_code, r.content))
|
|
||||||
self.assertTrue(SchedTimeSessAssignment.objects.get(pk=scheduled.pk).pinned)
|
|
||||||
|
|
||||||
class TimeSlotEditingApiTests(TestCase):
|
|
||||||
|
|
||||||
def test_manipulate_timeslot(self):
|
|
||||||
meeting = make_meeting_test_data()
|
|
||||||
slot = meeting.timeslot_set.filter(type_id='regular')[0]
|
|
||||||
|
|
||||||
url = urlreverse("ietf.meeting.ajax.timeslot_sloturl",
|
|
||||||
kwargs=dict(num=meeting.number, slotid=slot.pk))
|
|
||||||
|
|
||||||
modify_post_data = {
|
|
||||||
"purpose" : "plenary"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Fail as non-secretariat
|
|
||||||
self.client.login(username="plain", password="plain+password")
|
|
||||||
r = self.client.post(url, modify_post_data)
|
|
||||||
self.assertEqual(r.status_code, 403)
|
|
||||||
slot.refresh_from_db()
|
|
||||||
self.assertEqual(slot.type_id, 'regular')
|
|
||||||
|
|
||||||
# Successful change of purpose
|
|
||||||
self.client.login(username="secretary", password="secretary+password")
|
|
||||||
r = self.client.post(url, modify_post_data)
|
|
||||||
self.assertEqual(r.status_code, 200)
|
|
||||||
slot.refresh_from_db()
|
|
||||||
self.assertEqual(slot.type_id, 'plenary')
|
|
|
@ -1,100 +1,335 @@
|
||||||
# Copyright The IETF Trust 2020, All Rights Reserved
|
# Copyright The IETF Trust 2020, All Rights Reserved
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
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.models import SchedTimeSessAssignment
|
||||||
|
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']
|
# 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
|
||||||
|
|
||||||
for assignment in assignments:
|
# create sessions, etc
|
||||||
expected_filter_keywords = {assignment.timeslot.type.slug, assignment.session.type.slug}
|
session_data = [
|
||||||
|
{
|
||||||
if assignment.session == office_hours:
|
'description': 'regular wg session',
|
||||||
expected_filter_keywords.update([
|
'session': SessionFactory(
|
||||||
group.parent.acronym,
|
group=group, meeting=meeting, add_to_schedule=False,
|
||||||
'officehours',
|
purpose_id='none' if legacy_keywords else 'regular',
|
||||||
'someofficehours',
|
type_id='regular',
|
||||||
])
|
),
|
||||||
else:
|
'expected_keywords': {
|
||||||
expected_filter_keywords.update([
|
|
||||||
expected_group.acronym,
|
expected_group.acronym,
|
||||||
expected_area.acronym
|
expected_area.acronym,
|
||||||
])
|
# if legacy_keywords, next line repeats a previous entry to avoid adding anything to the set
|
||||||
if bof:
|
expected_group.acronym if legacy_keywords else 'regular',
|
||||||
expected_filter_keywords.add('bof')
|
f'{expected_group.acronym}-sessa',
|
||||||
token = assignment.session.docname_token_only_for_multiple()
|
},
|
||||||
if token is not None:
|
},
|
||||||
expected_filter_keywords.update([expected_group.acronym + "-" + token])
|
{
|
||||||
|
'description': 'plenary session',
|
||||||
|
'session': SessionFactory(
|
||||||
|
group=group, meeting=meeting, add_to_schedule=False,
|
||||||
|
name=f'{group.acronym} plenary',
|
||||||
|
purpose_id='none' if legacy_keywords else 'plenary',
|
||||||
|
type_id='plenary',
|
||||||
|
),
|
||||||
|
'expected_keywords': {
|
||||||
|
expected_group.acronym,
|
||||||
|
expected_area.acronym,
|
||||||
|
f'{expected_group.acronym}-sessb',
|
||||||
|
'plenary',
|
||||||
|
f'{group.acronym}-plenary',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'description': 'office hours session',
|
||||||
|
'session': SessionFactory(
|
||||||
|
group=group, meeting=meeting, add_to_schedule=False,
|
||||||
|
name=f'{group.acronym} office hours',
|
||||||
|
purpose_id='none' if legacy_keywords else 'officehours',
|
||||||
|
type_id='other',
|
||||||
|
),
|
||||||
|
'expected_keywords': {
|
||||||
|
expected_group.acronym,
|
||||||
|
expected_area.acronym,
|
||||||
|
f'{expected_group.acronym}-sessc',
|
||||||
|
'officehours',
|
||||||
|
f'{group.acronym}-officehours' if legacy_keywords else 'officehours',
|
||||||
|
# officehours in prev line is a repeated value - since this is a set, it will be ignored
|
||||||
|
f'{group.acronym}-office-hours',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
for sd in session_data:
|
||||||
|
sd['session'].timeslotassignments.create(
|
||||||
|
timeslot=TimeSlotFactory(meeting=meeting, type=sd['session'].type),
|
||||||
|
schedule=meeting.schedule,
|
||||||
|
)
|
||||||
|
|
||||||
|
assignments = meeting.schedule.assignments.all()
|
||||||
|
|
||||||
|
# Set up historic groups if needed.
|
||||||
|
if historic:
|
||||||
|
for a in assignments:
|
||||||
|
a.session.historic_group = expected_group
|
||||||
|
|
||||||
|
# Execute the method under test
|
||||||
|
AgendaKeywordTagger(assignments=assignments).apply()
|
||||||
|
|
||||||
|
# Assert expected results
|
||||||
|
|
||||||
|
# check the assignment count - paranoid, but the method mutates its input so let's be careful
|
||||||
|
self.assertEqual(len(assignments), len(session_data), 'Should not change number of assignments')
|
||||||
|
|
||||||
|
assignment_by_session_pk = {a.session.pk: a for a in assignments}
|
||||||
|
for sd in session_data:
|
||||||
|
assignment = assignment_by_session_pk[sd['session'].pk]
|
||||||
|
expected_filter_keywords = sd['expected_keywords']
|
||||||
|
if bof:
|
||||||
|
expected_filter_keywords.add('bof')
|
||||||
self.assertCountEqual(
|
self.assertCountEqual(
|
||||||
assignment.filter_keywords,
|
assignment.filter_keywords,
|
||||||
expected_filter_keywords,
|
expected_filter_keywords,
|
||||||
'Assignment has incorrect filter keywords'
|
f'Assignment for "{sd["description"]}" has incorrect filter keywords'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@override_settings(MEETING_LEGACY_OFFICE_HOURS_END=111)
|
||||||
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')
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(MEETING_LEGACY_OFFICE_HOURS_END=111)
|
||||||
|
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):
|
||||||
|
self.do_get_filter_categories_test(False)
|
||||||
|
|
||||||
|
def test_get_legacy_filter_categories(self):
|
||||||
|
self.do_get_filter_categories_test(True)
|
||||||
|
|
||||||
|
def do_get_filter_categories_test(self, legacy):
|
||||||
|
# set up
|
||||||
|
meeting = make_meeting_test_data()
|
||||||
|
if legacy:
|
||||||
|
meeting.session_set.all().update(purpose_id='none') # legacy meetings did not have purposes
|
||||||
|
else:
|
||||||
|
meeting.number = str(settings.MEETING_LEGACY_OFFICE_HOURS_END + 1)
|
||||||
|
meeting.save()
|
||||||
|
|
||||||
|
# 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'),
|
||||||
|
purpose_id='officehours' if not legacy else 'none',
|
||||||
|
type_id='other',
|
||||||
|
name='FARFUT office hours',
|
||||||
|
meeting=meeting
|
||||||
|
)
|
||||||
|
|
||||||
|
if legacy:
|
||||||
|
expected = [
|
||||||
|
[
|
||||||
|
# area category
|
||||||
|
{'label': 'FARFUT', 'keyword': 'farfut', 'is_bof': False, 'toggled_by': [],
|
||||||
|
'children': [
|
||||||
|
{'label': 'ames', 'keyword': 'ames', 'is_bof': False, 'toggled_by': ['farfut']},
|
||||||
|
{'label': 'mars', 'keyword': 'mars', 'is_bof': False, 'toggled_by': ['farfut']},
|
||||||
|
]},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
# non-area category
|
||||||
|
{'label': 'IAB', 'keyword': 'iab', 'is_bof': False, 'toggled_by': [],
|
||||||
|
'children': [
|
||||||
|
{'label': iab_child.acronym, 'keyword': iab_child.acronym, 'is_bof': False, 'toggled_by': ['iab']},
|
||||||
|
]},
|
||||||
|
{'label': 'IRTF', 'keyword': 'irtf', 'is_bof': False, 'toggled_by': [],
|
||||||
|
'children': [
|
||||||
|
{'label': irtf_child.acronym, 'keyword': irtf_child.acronym, 'is_bof': True, 'toggled_by': ['bof', 'irtf']},
|
||||||
|
]},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
# non-group category
|
||||||
|
{'label': 'Office Hours', 'keyword': 'officehours', 'is_bof': False, 'toggled_by': [],
|
||||||
|
'children': [
|
||||||
|
{'label': 'FARFUT', 'keyword': 'farfut-officehours', 'is_bof': False, 'toggled_by': ['officehours', 'farfut']}
|
||||||
|
]},
|
||||||
|
{'label': None, 'keyword': None,'is_bof': False, 'toggled_by': [],
|
||||||
|
'children': [
|
||||||
|
{'label': 'BoF', 'keyword': 'bof', 'is_bof': False, 'toggled_by': []},
|
||||||
|
{'label': 'Plenary', 'keyword': 'plenary', 'is_bof': False, 'toggled_by': []},
|
||||||
|
]},
|
||||||
|
],
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
expected = [
|
||||||
|
[
|
||||||
|
# area category
|
||||||
|
{'label': 'FARFUT', 'keyword': 'farfut', 'is_bof': False, 'toggled_by': [],
|
||||||
|
'children': [
|
||||||
|
{'label': 'ames', 'keyword': 'ames', 'is_bof': False, 'toggled_by': ['farfut']},
|
||||||
|
{'label': 'mars', 'keyword': 'mars', 'is_bof': False, 'toggled_by': ['farfut']},
|
||||||
|
]},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
# non-area category
|
||||||
|
{'label': 'IAB', 'keyword': 'iab', 'is_bof': False, 'toggled_by': [],
|
||||||
|
'children': [
|
||||||
|
{'label': iab_child.acronym, 'keyword': iab_child.acronym, 'is_bof': False, 'toggled_by': ['iab']},
|
||||||
|
]},
|
||||||
|
{'label': 'IRTF', 'keyword': 'irtf', 'is_bof': False, 'toggled_by': [],
|
||||||
|
'children': [
|
||||||
|
{'label': irtf_child.acronym, 'keyword': irtf_child.acronym, 'is_bof': True, 'toggled_by': ['bof', 'irtf']},
|
||||||
|
]},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
# non-group category
|
||||||
|
{'label': 'Administrative', 'keyword': 'admin', 'is_bof': False, 'toggled_by': [],
|
||||||
|
'children': [
|
||||||
|
{'label': 'Registration', 'keyword': 'registration', 'is_bof': False, 'toggled_by': ['admin', 'secretariat']},
|
||||||
|
]},
|
||||||
|
{'label': 'Closed meeting', 'keyword': 'closed_meeting', 'is_bof': False, 'toggled_by': [],
|
||||||
|
'children': [
|
||||||
|
{'label': 'IESG Breakfast', 'keyword': 'iesg-breakfast', 'is_bof': False, 'toggled_by': ['closed_meeting', 'iesg']},
|
||||||
|
]},
|
||||||
|
{'label': 'Office hours', 'keyword': 'officehours', 'is_bof': False, 'toggled_by': [],
|
||||||
|
'children': [
|
||||||
|
{'label': 'FARFUT office hours', 'keyword': 'farfut-office-hours', 'is_bof': False, 'toggled_by': ['officehours', 'farfut']}
|
||||||
|
]},
|
||||||
|
{'label': 'Plenary', 'keyword': 'plenary', 'is_bof': False, 'toggled_by': [],
|
||||||
|
'children': [
|
||||||
|
{'label': 'IETF Plenary', 'keyword': 'ietf-plenary', 'is_bof': False, 'toggled_by': ['plenary', 'ietf']},
|
||||||
|
]},
|
||||||
|
{'label': 'Social', 'keyword': 'social', 'is_bof': False, 'toggled_by': [],
|
||||||
|
'children': [
|
||||||
|
{'label': 'Morning Break', 'keyword': 'morning-break', 'is_bof': False, 'toggled_by': ['social', 'secretariat']},
|
||||||
|
]},
|
||||||
|
{'label': None, 'keyword': None,'is_bof': False, 'toggled_by': [],
|
||||||
|
'children': [
|
||||||
|
{'label': 'BoF', 'keyword': 'bof', 'is_bof': False, 'toggled_by': []},
|
||||||
|
]},
|
||||||
|
],
|
||||||
|
]
|
||||||
|
# put all the above together for single-column tests
|
||||||
|
expected_single_category = [sum(expected, [])]
|
||||||
|
|
||||||
|
###
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# single-column
|
||||||
|
filter_organizer = AgendaFilterOrganizer(sessions=sessions, single_category=True)
|
||||||
|
self.assertEqual(filter_organizer.get_filter_categories(), expected_single_category)
|
||||||
|
|
||||||
|
###
|
||||||
|
# test again using assignments
|
||||||
|
assignments = SchedTimeSessAssignment.objects.filter(
|
||||||
|
schedule__in=(meeting.schedule, meeting.schedule.base)
|
||||||
|
)
|
||||||
|
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)
|
|
@ -89,7 +89,13 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
|
||||||
s2.save()
|
s2.save()
|
||||||
SchedTimeSessAssignment.objects.filter(session=s1).delete()
|
SchedTimeSessAssignment.objects.filter(session=s1).delete()
|
||||||
|
|
||||||
s2b = Session.objects.create(meeting=meeting, group=s2.group, attendees=10, requested_duration=datetime.timedelta(minutes=60), type_id='regular')
|
s2b = SessionFactory(
|
||||||
|
meeting=meeting,
|
||||||
|
group=s2.group,
|
||||||
|
attendees=10,
|
||||||
|
requested_duration=datetime.timedelta(minutes=60),
|
||||||
|
add_to_schedule=False,
|
||||||
|
)
|
||||||
|
|
||||||
SchedulingEvent.objects.create(
|
SchedulingEvent.objects.create(
|
||||||
session=s2b,
|
session=s2b,
|
||||||
|
@ -110,7 +116,7 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
|
||||||
|
|
||||||
WebDriverWait(self.driver, 2).until(expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '.edit-meeting-schedule')))
|
WebDriverWait(self.driver, 2).until(expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '.edit-meeting-schedule')))
|
||||||
|
|
||||||
self.assertEqual(len(self.driver.find_elements(By.CSS_SELECTOR, '.session')), 3)
|
self.assertEqual(len(self.driver.find_elements(By.CSS_SELECTOR, '.session.purpose-regular')), 3)
|
||||||
|
|
||||||
# select - show session info
|
# select - show session info
|
||||||
s2_element = self.driver.find_element(By.CSS_SELECTOR, '#session{}'.format(s2.pk))
|
s2_element = self.driver.find_element(By.CSS_SELECTOR, '#session{}'.format(s2.pk))
|
||||||
|
@ -167,7 +173,7 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
|
||||||
|
|
||||||
sorted_pks = [s.pk for s in sorted([s1, s2, s2b], key=lambda s: (s.requested_duration, s.group.parent.acronym, s.group.acronym, s.pk))]
|
sorted_pks = [s.pk for s in sorted([s1, s2, s2b], key=lambda s: (s.requested_duration, s.group.parent.acronym, s.group.acronym, s.pk))]
|
||||||
self.driver.find_element(By.CSS_SELECTOR, '[name=sort_unassigned] option[value=duration]').click()
|
self.driver.find_element(By.CSS_SELECTOR, '[name=sort_unassigned] option[value=duration]').click()
|
||||||
self.assertTrue(self.driver.find_element(By.CSS_SELECTOR, '.unassigned-sessions .drop-target #session{} + #session{}'.format(*sorted_pks)))
|
self.assertTrue(self.driver.find_element(By.CSS_SELECTOR, '.unassigned-sessions .drop-target #session{} ~ #session{}'.format(*sorted_pks)))
|
||||||
|
|
||||||
sorted_pks = [s.pk for s in sorted([s1, s2, s2b], key=lambda s: (int(bool(s.comments)), s.group.parent.acronym, s.group.acronym, s.requested_duration, s.pk))]
|
sorted_pks = [s.pk for s in sorted([s1, s2, s2b], key=lambda s: (int(bool(s.comments)), s.group.parent.acronym, s.group.acronym, s.requested_duration, s.pk))]
|
||||||
self.driver.find_element(By.CSS_SELECTOR, '[name=sort_unassigned] option[value=comments]').click()
|
self.driver.find_element(By.CSS_SELECTOR, '[name=sort_unassigned] option[value=comments]').click()
|
||||||
|
@ -258,7 +264,7 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
|
||||||
'Session should be selectable when parent enabled')
|
'Session should be selectable when parent enabled')
|
||||||
|
|
||||||
# hide timeslots
|
# hide timeslots
|
||||||
self.driver.find_element(By.CSS_SELECTOR, ".timeslot-group-toggles button").click()
|
self.driver.find_element(By.CSS_SELECTOR, "#timeslot-toggle-modal-open").click()
|
||||||
self.assertTrue(self.driver.find_element(By.CSS_SELECTOR, "#timeslot-group-toggles-modal").is_displayed())
|
self.assertTrue(self.driver.find_element(By.CSS_SELECTOR, "#timeslot-group-toggles-modal").is_displayed())
|
||||||
self.driver.find_element(By.CSS_SELECTOR, "#timeslot-group-toggles-modal [value=\"{}\"]".format("ts-group-{}-{}".format(slot2.time.strftime("%Y%m%d-%H%M"), int(slot2.duration.total_seconds() / 60)))).click()
|
self.driver.find_element(By.CSS_SELECTOR, "#timeslot-group-toggles-modal [value=\"{}\"]".format("ts-group-{}-{}".format(slot2.time.strftime("%Y%m%d-%H%M"), int(slot2.duration.total_seconds() / 60)))).click()
|
||||||
self.driver.find_element(By.CSS_SELECTOR, "#timeslot-group-toggles-modal [data-dismiss=\"modal\"]").click()
|
self.driver.find_element(By.CSS_SELECTOR, "#timeslot-group-toggles-modal [data-dismiss=\"modal\"]").click()
|
||||||
|
@ -760,18 +766,8 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
|
||||||
all of the events needed by the editor.
|
all of the events needed by the editor.
|
||||||
"""
|
"""
|
||||||
# Set up a meeting and a schedule a plain user can edit
|
# Set up a meeting and a schedule a plain user can edit
|
||||||
meeting = make_meeting_test_data()
|
schedule = ScheduleFactory(meeting__type_id='ietf', owner__user__username="plain")
|
||||||
schedule = Schedule.objects.filter(meeting=meeting, owner__user__username="plain").first()
|
meeting = schedule.meeting
|
||||||
sessions = meeting.session_set.filter(type_id='regular')
|
|
||||||
timeslots = meeting.timeslot_set.filter(type_id='regular')
|
|
||||||
self.assertGreaterEqual(timeslots.count(), sessions.count(),
|
|
||||||
'Need a timeslot for each session')
|
|
||||||
for index, session in enumerate(sessions):
|
|
||||||
SchedTimeSessAssignment.objects.create(
|
|
||||||
schedule=schedule,
|
|
||||||
timeslot=timeslots[index],
|
|
||||||
session=session,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Open the editor
|
# Open the editor
|
||||||
self.login()
|
self.login()
|
||||||
|
@ -780,7 +776,6 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
|
||||||
kwargs=dict(num=meeting.number, name=schedule.name, owner=schedule.owner_email())
|
kwargs=dict(num=meeting.number, name=schedule.name, owner=schedule.owner_email())
|
||||||
)
|
)
|
||||||
self.driver.get(url)
|
self.driver.get(url)
|
||||||
|
|
||||||
# Check that the drop target for unassigned sessions is actually empty
|
# Check that the drop target for unassigned sessions is actually empty
|
||||||
drop_target = self.driver.find_element(By.CSS_SELECTOR,
|
drop_target = self.driver.find_element(By.CSS_SELECTOR,
|
||||||
'.unassigned-sessions .drop-target'
|
'.unassigned-sessions .drop-target'
|
||||||
|
@ -859,7 +854,7 @@ class ScheduleEditTests(IetfSeleniumTestCase):
|
||||||
ss = list(SchedTimeSessAssignment.objects.filter(session__meeting__number=72,session__group__acronym='mars',schedule__name='test-schedule')) # pyflakes:ignore
|
ss = list(SchedTimeSessAssignment.objects.filter(session__meeting__number=72,session__group__acronym='mars',schedule__name='test-schedule')) # pyflakes:ignore
|
||||||
|
|
||||||
self.login()
|
self.login()
|
||||||
url = self.absreverse('ietf.meeting.views.edit_schedule',kwargs=dict(num='72',name='test-schedule',owner='plain@example.com'))
|
url = self.absreverse('ietf.meeting.views.edit_meeting_schedule',kwargs=dict(num='72',name='test-schedule',owner='plain@example.com'))
|
||||||
self.driver.get(url)
|
self.driver.get(url)
|
||||||
|
|
||||||
# driver.get() will wait for scripts to finish, but not ajax
|
# driver.get() will wait for scripts to finish, but not ajax
|
||||||
|
@ -1197,7 +1192,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 +1303,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 +1475,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 +1732,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 +1758,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 +1825,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 +1850,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
|
||||||
|
@ -2005,6 +2016,7 @@ class InterimTests(IetfSeleniumTestCase):
|
||||||
sg_interim = make_interim_meeting(somegroup, datetime.date.today() + datetime.timedelta(days=20))
|
sg_interim = make_interim_meeting(somegroup, datetime.date.today() + datetime.timedelta(days=20))
|
||||||
sg_sess = sg_interim.session_set.first()
|
sg_sess = sg_interim.session_set.first()
|
||||||
sg_slot = sg_sess.timeslotassignments.first().timeslot
|
sg_slot = sg_sess.timeslotassignments.first().timeslot
|
||||||
|
sg_sess.purpose_id = 'plenary'
|
||||||
sg_sess.type_id = 'plenary'
|
sg_sess.type_id = 'plenary'
|
||||||
sg_slot.type_id = 'plenary'
|
sg_slot.type_id = 'plenary'
|
||||||
sg_sess.save()
|
sg_sess.save()
|
||||||
|
@ -2580,6 +2592,166 @@ class ProceedingsMaterialTests(IetfSeleniumTestCase):
|
||||||
'URL field should be shown by default')
|
'URL field should be shown by default')
|
||||||
|
|
||||||
|
|
||||||
|
@ifSeleniumEnabled
|
||||||
|
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
|
||||||
|
@ -2599,5 +2771,5 @@ class ProceedingsMaterialTests(IetfSeleniumTestCase):
|
||||||
# make_meeting_test_data()
|
# make_meeting_test_data()
|
||||||
#
|
#
|
||||||
# def testOpenSchedule(self):
|
# def testOpenSchedule(self):
|
||||||
# url = urlreverse('ietf.meeting.views.edit_schedule', kwargs=dict(num='72',name='test-schedule'))
|
# url = urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs=dict(num='72',name='test-schedule'))
|
||||||
# r = self.client.get(url)
|
# r = self.client.get(url)
|
File diff suppressed because it is too large
Load diff
|
@ -4,7 +4,7 @@ from django.conf.urls import include
|
||||||
from django.views.generic import RedirectView
|
from django.views.generic import RedirectView
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from ietf.meeting import views, ajax, views_proceedings
|
from ietf.meeting import views, views_proceedings
|
||||||
from ietf.utils.urls import url
|
from ietf.utils.urls import url
|
||||||
|
|
||||||
safe_for_all_meeting_types = [
|
safe_for_all_meeting_types = [
|
||||||
|
@ -26,8 +26,7 @@ safe_for_all_meeting_types = [
|
||||||
|
|
||||||
|
|
||||||
type_ietf_only_patterns = [
|
type_ietf_only_patterns = [
|
||||||
url(r'^agenda/%(owner)s/%(schedule_name)s/edit$' % settings.URL_REGEXPS, views.edit_schedule),
|
url(r'^agenda/%(owner)s/%(schedule_name)s/edit/?$' % settings.URL_REGEXPS, views.edit_meeting_schedule),
|
||||||
url(r'^agenda/%(owner)s/%(schedule_name)s/edit/$' % settings.URL_REGEXPS, views.edit_meeting_schedule),
|
|
||||||
url(r'^agenda/%(owner)s/%(schedule_name)s/timeslots/$' % settings.URL_REGEXPS, views.edit_meeting_timeslots_and_misc_sessions),
|
url(r'^agenda/%(owner)s/%(schedule_name)s/timeslots/$' % settings.URL_REGEXPS, views.edit_meeting_timeslots_and_misc_sessions),
|
||||||
url(r'^agenda/%(owner)s/%(schedule_name)s/details$' % settings.URL_REGEXPS, views.edit_schedule_properties),
|
url(r'^agenda/%(owner)s/%(schedule_name)s/details$' % settings.URL_REGEXPS, views.edit_schedule_properties),
|
||||||
url(r'^agenda/%(owner)s/%(schedule_name)s/delete$' % settings.URL_REGEXPS, views.delete_schedule),
|
url(r'^agenda/%(owner)s/%(schedule_name)s/delete$' % settings.URL_REGEXPS, views.delete_schedule),
|
||||||
|
@ -38,10 +37,6 @@ type_ietf_only_patterns = [
|
||||||
url(r'^agenda/%(owner)s/%(schedule_name)s/by-room/?$' % settings.URL_REGEXPS, views.agenda_by_room),
|
url(r'^agenda/%(owner)s/%(schedule_name)s/by-room/?$' % settings.URL_REGEXPS, views.agenda_by_room),
|
||||||
url(r'^agenda/%(owner)s/%(schedule_name)s/by-type/?$' % settings.URL_REGEXPS, views.agenda_by_type),
|
url(r'^agenda/%(owner)s/%(schedule_name)s/by-type/?$' % settings.URL_REGEXPS, views.agenda_by_type),
|
||||||
url(r'^agenda/%(owner)s/%(schedule_name)s/by-type/(?P<type>[a-z]+)$' % settings.URL_REGEXPS, views.agenda_by_type),
|
url(r'^agenda/%(owner)s/%(schedule_name)s/by-type/(?P<type>[a-z]+)$' % settings.URL_REGEXPS, views.agenda_by_type),
|
||||||
url(r'^agenda/%(owner)s/%(schedule_name)s/permissions$' % settings.URL_REGEXPS, ajax.schedule_permission_api),
|
|
||||||
url(r'^agenda/%(owner)s/%(schedule_name)s/session/(?P<assignment_id>\d+).json$' % settings.URL_REGEXPS, ajax.assignment_json),
|
|
||||||
url(r'^agenda/%(owner)s/%(schedule_name)s/sessions.json$' % settings.URL_REGEXPS, ajax.assignments_json),
|
|
||||||
url(r'^agenda/%(owner)s/%(schedule_name)s.json$' % settings.URL_REGEXPS, ajax.schedule_infourl),
|
|
||||||
url(r'^agenda/%(owner)s/%(schedule_name)s/new/$' % settings.URL_REGEXPS, views.new_meeting_schedule),
|
url(r'^agenda/%(owner)s/%(schedule_name)s/new/$' % settings.URL_REGEXPS, views.new_meeting_schedule),
|
||||||
url(r'^agenda/by-room$', views.agenda_by_room),
|
url(r'^agenda/by-room$', views.agenda_by_room),
|
||||||
url(r'^agenda/by-type$', views.agenda_by_type),
|
url(r'^agenda/by-type$', views.agenda_by_type),
|
||||||
|
@ -52,22 +47,12 @@ type_ietf_only_patterns = [
|
||||||
url(r'^agendas/edit$', RedirectView.as_view(pattern_name='ietf.meeting.views.list_schedules', permanent=True)),
|
url(r'^agendas/edit$', RedirectView.as_view(pattern_name='ietf.meeting.views.list_schedules', permanent=True)),
|
||||||
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'^room/(?P<roomid>\d+).json$', ajax.timeslot_roomurl),
|
|
||||||
url(r'^timeslots$', ajax.timeslot_slotsurl),
|
|
||||||
url(r'^timeslots.json$', ajax.timeslot_slotsurl),
|
|
||||||
url(r'^timeslot/(?P<slotid>\d+).json$', ajax.timeslot_sloturl),
|
|
||||||
url(r'^agendas$', ajax.schedule_infosurl),
|
|
||||||
url(r'^agendas.json$', ajax.schedule_infosurl),
|
|
||||||
url(r'^agenda/(?P<acronym>[-a-z0-9]+)-drafts.pdf$', views.session_draft_pdf),
|
url(r'^agenda/(?P<acronym>[-a-z0-9]+)-drafts.pdf$', views.session_draft_pdf),
|
||||||
url(r'^agenda/(?P<acronym>[-a-z0-9]+)-drafts.tgz$', views.session_draft_tarfile),
|
url(r'^agenda/(?P<acronym>[-a-z0-9]+)-drafts.tgz$', views.session_draft_tarfile),
|
||||||
url(r'^sessions\.json$', ajax.sessions_json),
|
|
||||||
url(r'^session/(?P<sessionid>\d+).json', ajax.session_json),
|
|
||||||
url(r'^session/(?P<sessionid>\d+)/constraints.json', ajax.session_constraints),
|
|
||||||
url(r'^constraint/(?P<constraintid>\d+).json', ajax.constraint_json),
|
|
||||||
url(r'^json$', ajax.meeting_json),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# This is a limited subset of the list above -- many of the views above won't work for interim meetings
|
# This is a limited subset of the list above -- many of the views above won't work for interim meetings
|
||||||
|
@ -82,7 +67,9 @@ type_ietf_only_patterns_id_optional = [
|
||||||
url(r'^agenda(?P<utc>-utc)?(?P<ext>.html)?/?$', views.agenda),
|
url(r'^agenda(?P<utc>-utc)?(?P<ext>.html)?/?$', views.agenda),
|
||||||
url(r'^agenda(?P<ext>.txt)$', views.agenda),
|
url(r'^agenda(?P<ext>.txt)$', views.agenda),
|
||||||
url(r'^agenda(?P<ext>.csv)$', views.agenda),
|
url(r'^agenda(?P<ext>.csv)$', views.agenda),
|
||||||
url(r'^agenda/edit$', views.edit_schedule),
|
url(r'^agenda/edit$',
|
||||||
|
RedirectView.as_view(pattern_name='ietf.meeting.views.edit_meeting_schedule', permanent=True),
|
||||||
|
name='ietf.meeting.views.edit_meeting_schedule'),
|
||||||
url(r'^agenda/edit/$', views.edit_meeting_schedule),
|
url(r'^agenda/edit/$', views.edit_meeting_schedule),
|
||||||
url(r'^requests$', views.meeting_requests),
|
url(r'^requests$', views.meeting_requests),
|
||||||
url(r'^agenda/agenda\.ics$', views.agenda_ical),
|
url(r'^agenda/agenda\.ics$', views.agenda_ical),
|
||||||
|
@ -140,6 +127,7 @@ urlpatterns = [
|
||||||
url(r'^upcoming\.ics/?$', views.upcoming_ical),
|
url(r'^upcoming\.ics/?$', views.upcoming_ical),
|
||||||
url(r'^upcoming\.json/?$', views.upcoming_json),
|
url(r'^upcoming\.json/?$', views.upcoming_json),
|
||||||
url(r'^session/(?P<session_id>\d+)/agenda_materials$', views.session_materials),
|
url(r'^session/(?P<session_id>\d+)/agenda_materials$', views.session_materials),
|
||||||
|
url(r'^session/(?P<session_id>\d+)/edit/?', views.edit_session),
|
||||||
# Then patterns from more specific to less
|
# Then patterns from more specific to less
|
||||||
url(r'^(?P<num>interim-[a-z0-9-]+)/', include(type_interim_patterns)),
|
url(r'^(?P<num>interim-[a-z0-9-]+)/', include(type_interim_patterns)),
|
||||||
url(r'^(?P<num>\d+)/requests.html$', RedirectView.as_view(url='/meeting/%(num)s/requests', permanent=True)),
|
url(r'^(?P<num>\d+)/requests.html$', RedirectView.as_view(url='/meeting/%(num)s/requests', permanent=True)),
|
||||||
|
|
|
@ -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,13 @@ 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,
|
||||||
from ietf.meeting.helpers import get_areas, get_person_by_email, get_schedule_by_name
|
TimeSlotCreateForm, TimeSlotEditForm, SessionEditForm )
|
||||||
from ietf.meeting.helpers import build_all_agenda_slices, get_wg_name_list
|
from ietf.meeting.helpers import get_person_by_email, get_schedule_by_name
|
||||||
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_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,10 +81,10 @@ 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, SessionPurposeName
|
||||||
from ietf.secr.proceedings.utils import handle_upload_file
|
from ietf.secr.proceedings.utils import handle_upload_file
|
||||||
from ietf.secr.proceedings.proc_utils import (get_progress_stats, post_process, import_audio_files,
|
from ietf.secr.proceedings.proc_utils import (get_progress_stats, post_process, import_audio_files,
|
||||||
create_recording)
|
create_recording)
|
||||||
|
@ -279,102 +276,55 @@ def materials_editable_groups(request, num=None):
|
||||||
return render(request, "meeting/materials_editable_groups.html", {
|
return render(request, "meeting/materials_editable_groups.html", {
|
||||||
'meeting_num': meeting.number})
|
'meeting_num': meeting.number})
|
||||||
|
|
||||||
def ascii_alphanumeric(string):
|
|
||||||
return re.match(r'^[a-zA-Z0-9]*$', string)
|
|
||||||
|
|
||||||
class SaveAsForm(forms.Form):
|
|
||||||
savename = forms.CharField(max_length=16)
|
|
||||||
|
|
||||||
@role_required('Area Director','Secretariat')
|
|
||||||
def schedule_create(request, num=None, owner=None, name=None):
|
|
||||||
meeting = get_meeting(num)
|
|
||||||
person = get_person_by_email(owner)
|
|
||||||
schedule = get_schedule_by_name(meeting, person, name)
|
|
||||||
|
|
||||||
if schedule is None:
|
|
||||||
# here we have to return some ajax to display an error.
|
|
||||||
messages.error("Error: No meeting information for meeting %s owner %s schedule %s available" % (num, owner, name)) # pylint: disable=no-value-for-parameter
|
|
||||||
return redirect(edit_schedule, num=num, owner=owner, name=name)
|
|
||||||
|
|
||||||
# authorization was enforced by the @group_require decorator above.
|
|
||||||
|
|
||||||
saveasform = SaveAsForm(request.POST)
|
|
||||||
if not saveasform.is_valid():
|
|
||||||
messages.info(request, "This name is not valid. Please choose another one.")
|
|
||||||
return redirect(edit_schedule, num=num, owner=owner, name=name)
|
|
||||||
|
|
||||||
savedname = saveasform.cleaned_data['savename']
|
|
||||||
|
|
||||||
if not ascii_alphanumeric(savedname):
|
|
||||||
messages.info(request, "This name contains illegal characters. Please choose another one.")
|
|
||||||
return redirect(edit_schedule, num=num, owner=owner, name=name)
|
|
||||||
|
|
||||||
# create the new schedule, and copy the assignments
|
|
||||||
try:
|
|
||||||
sched = meeting.schedule_set.get(name=savedname, owner=request.user.person)
|
|
||||||
if sched:
|
|
||||||
return redirect(edit_schedule, num=meeting.number, owner=sched.owner_email(), name=sched.name)
|
|
||||||
else:
|
|
||||||
messages.info(request, "Schedule creation failed. Please try again.")
|
|
||||||
return redirect(edit_schedule, num=num, owner=owner, name=name)
|
|
||||||
|
|
||||||
except Schedule.DoesNotExist:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# must be done
|
|
||||||
newschedule = Schedule(name=savedname,
|
|
||||||
owner=request.user.person,
|
|
||||||
meeting=meeting,
|
|
||||||
base=schedule.base,
|
|
||||||
origin=schedule,
|
|
||||||
visible=False,
|
|
||||||
public=False)
|
|
||||||
|
|
||||||
newschedule.save()
|
|
||||||
if newschedule is None:
|
|
||||||
return HttpResponse(status=500)
|
|
||||||
|
|
||||||
# keep a mapping so that extendedfrom references can be chased.
|
|
||||||
mapping = {};
|
|
||||||
for ss in schedule.assignments.all():
|
|
||||||
# hack to copy the object, creating a new one
|
|
||||||
# just reset the key, and save it again.
|
|
||||||
oldid = ss.pk
|
|
||||||
ss.pk = None
|
|
||||||
ss.schedule=newschedule
|
|
||||||
ss.save()
|
|
||||||
mapping[oldid] = ss.pk
|
|
||||||
#print "Copying %u to %u" % (oldid, ss.pk)
|
|
||||||
|
|
||||||
# now fix up any extendedfrom references to new set.
|
|
||||||
for ss in newschedule.assignments.all():
|
|
||||||
if ss.extendedfrom is not None:
|
|
||||||
oldid = ss.extendedfrom.id
|
|
||||||
newid = mapping[oldid]
|
|
||||||
#print "Fixing %u to %u" % (oldid, newid)
|
|
||||||
ss.extendedfrom = newschedule.assignments.get(pk = newid)
|
|
||||||
ss.save()
|
|
||||||
|
|
||||||
|
|
||||||
# now redirect to this new schedule.
|
|
||||||
return redirect(edit_schedule, meeting.number, newschedule.owner_email(), newschedule.name)
|
|
||||||
|
|
||||||
|
|
||||||
@role_required('Secretariat')
|
@role_required('Secretariat')
|
||||||
def edit_timeslots(request, num=None):
|
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 +332,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):
|
||||||
|
@ -464,6 +416,19 @@ def new_meeting_schedule(request, num, owner=None, name=None):
|
||||||
|
|
||||||
@ensure_csrf_cookie
|
@ensure_csrf_cookie
|
||||||
def edit_meeting_schedule(request, num=None, owner=None, name=None):
|
def edit_meeting_schedule(request, num=None, owner=None, name=None):
|
||||||
|
"""Schedule editor
|
||||||
|
|
||||||
|
In addition to the URL parameters, accepts a query string parameter 'type'.
|
||||||
|
If present, only sessions/timeslots with a TimeSlotTypeName with that slug
|
||||||
|
will be included in the editor. More than one type can be enabled by passing
|
||||||
|
multiple type parameters.
|
||||||
|
|
||||||
|
?type=regular - shows only regular sessions/timeslots (i.e., old editor behavior)
|
||||||
|
?type=regular&type=other - shows both regular and other sessions/timeslots
|
||||||
|
"""
|
||||||
|
# Need to coordinate this list with types of session requests
|
||||||
|
# that can be created (see, e.g., SessionQuerySet.requests())
|
||||||
|
IGNORE_TIMESLOT_TYPES = ('offagenda', 'reserved', 'unavail')
|
||||||
meeting = get_meeting(num)
|
meeting = get_meeting(num)
|
||||||
if name is None:
|
if name is None:
|
||||||
schedule = meeting.schedule
|
schedule = meeting.schedule
|
||||||
|
@ -493,11 +458,19 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
|
||||||
"hide_menu": True
|
"hide_menu": True
|
||||||
}, status=403, content_type="text/html")
|
}, status=403, content_type="text/html")
|
||||||
|
|
||||||
|
# See if we were given one or more 'type' query string parameters. If so, filter to that timeslot type.
|
||||||
|
if 'type' in request.GET:
|
||||||
|
include_timeslot_types = request.GET.getlist('type')
|
||||||
|
else:
|
||||||
|
include_timeslot_types = None # disables filtering by type (other than IGNORE_TIMESLOT_TYPES)
|
||||||
|
|
||||||
assignments = SchedTimeSessAssignment.objects.filter(
|
assignments = SchedTimeSessAssignment.objects.filter(
|
||||||
schedule__in=[schedule, schedule.base],
|
schedule__in=[schedule, schedule.base],
|
||||||
timeslot__location__isnull=False,
|
timeslot__location__isnull=False,
|
||||||
session__type='regular',
|
)
|
||||||
).order_by('timeslot__time','timeslot__name')
|
if include_timeslot_types is not None:
|
||||||
|
assignments = assignments.filter(session__type__in=include_timeslot_types)
|
||||||
|
assignments = assignments.order_by('timeslot__time','timeslot__name')
|
||||||
|
|
||||||
assignments_by_session = defaultdict(list)
|
assignments_by_session = defaultdict(list)
|
||||||
for a in assignments:
|
for a in assignments:
|
||||||
|
@ -505,10 +478,12 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
|
||||||
|
|
||||||
tombstone_states = ['canceled', 'canceledpa', 'resched']
|
tombstone_states = ['canceled', 'canceledpa', 'resched']
|
||||||
|
|
||||||
|
sessions = Session.objects.filter(meeting=meeting)
|
||||||
|
if include_timeslot_types is not None:
|
||||||
|
sessions = sessions.filter(type__in=include_timeslot_types)
|
||||||
sessions = add_event_info_to_session_qs(
|
sessions = add_event_info_to_session_qs(
|
||||||
Session.objects.filter(
|
sessions.exclude(
|
||||||
meeting=meeting,
|
type__in=IGNORE_TIMESLOT_TYPES,
|
||||||
type='regular',
|
|
||||||
).order_by('pk'),
|
).order_by('pk'),
|
||||||
requested_time=True,
|
requested_time=True,
|
||||||
requested_by=True,
|
requested_by=True,
|
||||||
|
@ -516,13 +491,22 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
|
||||||
Q(current_status__in=['appr', 'schedw', 'scheda', 'sched'])
|
Q(current_status__in=['appr', 'schedw', 'scheda', 'sched'])
|
||||||
| Q(current_status__in=tombstone_states, pk__in={a.session_id for a in assignments})
|
| Q(current_status__in=tombstone_states, pk__in={a.session_id for a in assignments})
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'resources', 'group', 'group__parent', 'group__type', 'joint_with_groups',
|
'resources', 'group', 'group__parent', 'group__type', 'joint_with_groups', 'purpose',
|
||||||
)
|
)
|
||||||
|
|
||||||
timeslots_qs = TimeSlot.objects.filter(meeting=meeting, type='regular').prefetch_related('type').order_by('location', 'time', 'name')
|
timeslots_qs = TimeSlot.objects.filter(meeting=meeting)
|
||||||
|
if include_timeslot_types is not None:
|
||||||
|
timeslots_qs = timeslots_qs.filter(type__in=include_timeslot_types)
|
||||||
|
timeslots_qs = timeslots_qs.exclude(
|
||||||
|
type__in=IGNORE_TIMESLOT_TYPES,
|
||||||
|
).prefetch_related('type').order_by('location', 'time', 'name')
|
||||||
|
|
||||||
min_duration = min(t.duration for t in timeslots_qs)
|
if timeslots_qs.count() > 0:
|
||||||
max_duration = max(t.duration for t in timeslots_qs)
|
min_duration = min(t.duration for t in timeslots_qs)
|
||||||
|
max_duration = max(t.duration for t in timeslots_qs)
|
||||||
|
else:
|
||||||
|
min_duration = 1
|
||||||
|
max_duration = 2
|
||||||
|
|
||||||
def timedelta_to_css_ems(timedelta):
|
def timedelta_to_css_ems(timedelta):
|
||||||
# we scale the session and slots a bit according to their
|
# we scale the session and slots a bit according to their
|
||||||
|
@ -552,10 +536,14 @@ 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:
|
s.purpose_label = None
|
||||||
|
if (s.purpose.slug in ('none', 'regular')) and s.group:
|
||||||
s.scheduling_label = s.group.acronym
|
s.scheduling_label = s.group.acronym
|
||||||
elif s.name:
|
s.purpose_label = 'BoF' if s.group.is_bof() else s.group.type.name
|
||||||
s.scheduling_label = s.name
|
else:
|
||||||
|
s.purpose_label = s.purpose.name
|
||||||
|
if s.name:
|
||||||
|
s.scheduling_label = s.name
|
||||||
|
|
||||||
s.requested_duration_in_hours = round(s.requested_duration.seconds / 60.0 / 60.0, 1)
|
s.requested_duration_in_hours = round(s.requested_duration.seconds / 60.0 / 60.0, 1)
|
||||||
|
|
||||||
|
@ -659,7 +647,10 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
|
||||||
|
|
||||||
all_days = sorted(all_days) # changes set to a list
|
all_days = sorted(all_days) # changes set to a list
|
||||||
# Note the maximum timeslot count for any room
|
# Note the maximum timeslot count for any room
|
||||||
max_timeslots = max(rd['timeslot_count'] for rd in room_data.values())
|
if len(room_data) > 0:
|
||||||
|
max_timeslots = max(rd['timeslot_count'] for rd in room_data.values())
|
||||||
|
else:
|
||||||
|
max_timeslots = 0
|
||||||
|
|
||||||
# Partition rooms into groups with identical timeslot arrangements.
|
# Partition rooms into groups with identical timeslot arrangements.
|
||||||
# Start by discarding any roos that have no timeslots.
|
# Start by discarding any roos that have no timeslots.
|
||||||
|
@ -872,7 +863,10 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
|
||||||
return _json_response(False, error="Invalid parameters")
|
return _json_response(False, error="Invalid parameters")
|
||||||
|
|
||||||
# Show only rooms that have regular sessions
|
# Show only rooms that have regular sessions
|
||||||
rooms = meeting.room_set.filter(session_types__slug='regular')
|
if include_timeslot_types is None:
|
||||||
|
rooms = meeting.room_set.all()
|
||||||
|
else:
|
||||||
|
rooms = meeting.room_set.filter(session_types__slug__in=include_timeslot_types)
|
||||||
|
|
||||||
# Construct timeslot data for the template to render
|
# Construct timeslot data for the template to render
|
||||||
days = prepare_timeslots_for_display(timeslots_qs, rooms)
|
days = prepare_timeslots_for_display(timeslots_qs, rooms)
|
||||||
|
@ -942,6 +936,16 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
|
||||||
p.scheduling_color = "rgb({}, {}, {})".format(*tuple(int(round(x * 255)) for x in rgb_color))
|
p.scheduling_color = "rgb({}, {}, {})".format(*tuple(int(round(x * 255)) for x in rgb_color))
|
||||||
p.light_scheduling_color = "rgb({}, {}, {})".format(*tuple(int(round((0.9 + 0.1 * x) * 255)) for x in rgb_color))
|
p.light_scheduling_color = "rgb({}, {}, {})".format(*tuple(int(round((0.9 + 0.1 * x) * 255)) for x in rgb_color))
|
||||||
|
|
||||||
|
session_purposes = sorted(set(s.purpose for s in sessions if s.purpose), key=lambda p: p.name)
|
||||||
|
timeslot_types = sorted(
|
||||||
|
set(
|
||||||
|
s.type for s in sessions if s.type
|
||||||
|
).union(
|
||||||
|
t.type for t in timeslots_qs.all()
|
||||||
|
),
|
||||||
|
key=lambda tstype: tstype.name,
|
||||||
|
)
|
||||||
|
|
||||||
return render(request, "meeting/edit_meeting_schedule.html", {
|
return render(request, "meeting/edit_meeting_schedule.html", {
|
||||||
'meeting': meeting,
|
'meeting': meeting,
|
||||||
'schedule': schedule,
|
'schedule': schedule,
|
||||||
|
@ -952,6 +956,8 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
|
||||||
'timeslot_groups': sorted((d, list(sorted(t_groups))) for d, t_groups in timeslot_groups.items()),
|
'timeslot_groups': sorted((d, list(sorted(t_groups))) for d, t_groups in timeslot_groups.items()),
|
||||||
'unassigned_sessions': unassigned_sessions,
|
'unassigned_sessions': unassigned_sessions,
|
||||||
'session_parents': session_parents,
|
'session_parents': session_parents,
|
||||||
|
'session_purposes': session_purposes,
|
||||||
|
'timeslot_types': timeslot_types,
|
||||||
'hide_menu': True,
|
'hide_menu': True,
|
||||||
'lock_time': lock_time,
|
'lock_time': lock_time,
|
||||||
})
|
})
|
||||||
|
@ -968,6 +974,7 @@ class TimeSlotForm(forms.Form):
|
||||||
location = RoomNameModelChoiceField(queryset=Room.objects.all(), required=False, empty_label="(No location)")
|
location = RoomNameModelChoiceField(queryset=Room.objects.all(), required=False, empty_label="(No location)")
|
||||||
show_location = forms.BooleanField(initial=True, required=False)
|
show_location = forms.BooleanField(initial=True, required=False)
|
||||||
type = forms.ModelChoiceField(queryset=TimeSlotTypeName.objects.filter(used=True), empty_label=None, required=False)
|
type = forms.ModelChoiceField(queryset=TimeSlotTypeName.objects.filter(used=True), empty_label=None, required=False)
|
||||||
|
purpose = forms.ModelChoiceField(queryset=SessionPurposeName.objects.filter(used=True), required=False, widget=forms.HiddenInput)
|
||||||
name = forms.CharField(help_text='Name that appears on the agenda', required=False)
|
name = forms.CharField(help_text='Name that appears on the agenda', required=False)
|
||||||
short = forms.CharField(max_length=32,label='Short name', help_text='Abbreviated session name used for material file names', required=False)
|
short = forms.CharField(max_length=32,label='Short name', help_text='Abbreviated session name used for material file names', required=False)
|
||||||
group = forms.ModelChoiceField(queryset=Group.objects.filter(type__in=['ietf', 'team'], state='active'),
|
group = forms.ModelChoiceField(queryset=Group.objects.filter(type__in=['ietf', 'team'], state='active'),
|
||||||
|
@ -993,6 +1000,12 @@ class TimeSlotForm(forms.Form):
|
||||||
|
|
||||||
self.active_assignment = None
|
self.active_assignment = None
|
||||||
|
|
||||||
|
# only allow timeslots with at least one purpose
|
||||||
|
timeslot_types_with_purpose = set()
|
||||||
|
for spn in SessionPurposeName.objects.filter(used=True):
|
||||||
|
timeslot_types_with_purpose.update(spn.timeslot_types)
|
||||||
|
self.fields['type'].queryset = self.fields['type'].queryset.filter(pk__in=timeslot_types_with_purpose)
|
||||||
|
|
||||||
if timeslot:
|
if timeslot:
|
||||||
self.initial = {
|
self.initial = {
|
||||||
'day': timeslot.time.date(),
|
'day': timeslot.time.date(),
|
||||||
|
@ -1025,7 +1038,10 @@ class TimeSlotForm(forms.Form):
|
||||||
ts_type = self.cleaned_data.get('type')
|
ts_type = self.cleaned_data.get('type')
|
||||||
short = self.cleaned_data.get('short')
|
short = self.cleaned_data.get('short')
|
||||||
|
|
||||||
if ts_type:
|
if not ts_type:
|
||||||
|
# assign a generic purpose if no type has been set
|
||||||
|
self.cleaned_data['purpose'] = SessionPurposeName.objects.get(slug='open_meeting')
|
||||||
|
else:
|
||||||
if ts_type.slug in ['break', 'reg', 'reserved', 'unavail', 'regular']:
|
if ts_type.slug in ['break', 'reg', 'reserved', 'unavail', 'regular']:
|
||||||
if ts_type.slug != 'regular':
|
if ts_type.slug != 'regular':
|
||||||
self.cleaned_data['group'] = self.fields['group'].queryset.get(acronym='secretariat')
|
self.cleaned_data['group'] = self.fields['group'].queryset.get(acronym='secretariat')
|
||||||
|
@ -1035,10 +1051,22 @@ 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':
|
# find an allowed session purpose (guaranteed by TimeSlotForm)
|
||||||
|
for purpose in SessionPurposeName.objects.filter(used=True):
|
||||||
|
if ts_type.pk in purpose.timeslot_types:
|
||||||
|
self.cleaned_data['purpose'] = purpose
|
||||||
|
break
|
||||||
|
if self.cleaned_data['purpose'] is None:
|
||||||
|
self.add_error('type', f'{ts_type} has no allowed purposes')
|
||||||
|
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
|
||||||
|
@ -1118,6 +1146,7 @@ def edit_meeting_timeslots_and_misc_sessions(request, num=None, owner=None, name
|
||||||
short=c['short'],
|
short=c['short'],
|
||||||
group=c['group'],
|
group=c['group'],
|
||||||
type=c['type'],
|
type=c['type'],
|
||||||
|
purpose=c['purpose'],
|
||||||
agenda_note=c.get('agenda_note') or "",
|
agenda_note=c.get('agenda_note') or "",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1284,80 +1313,6 @@ def edit_meeting_timeslots_and_misc_sessions(request, num=None, owner=None, name
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
#@role_required('Area Director','Secretariat')
|
|
||||||
# disable the above security for now, check it below.
|
|
||||||
@ensure_csrf_cookie
|
|
||||||
def edit_schedule(request, num=None, owner=None, name=None):
|
|
||||||
|
|
||||||
if request.method == 'POST':
|
|
||||||
return schedule_create(request, num, owner, name)
|
|
||||||
|
|
||||||
user = request.user
|
|
||||||
meeting = get_meeting(num)
|
|
||||||
person = get_person_by_email(owner)
|
|
||||||
if name is None:
|
|
||||||
schedule = meeting.schedule
|
|
||||||
else:
|
|
||||||
schedule = get_schedule_by_name(meeting, person, name)
|
|
||||||
if schedule is None:
|
|
||||||
raise Http404("No meeting information for meeting %s owner %s schedule %s available" % (num, owner, name))
|
|
||||||
|
|
||||||
meeting_base_url = request.build_absolute_uri(meeting.base_url())
|
|
||||||
site_base_url = request.build_absolute_uri('/')[:-1] # skip the trailing slash
|
|
||||||
|
|
||||||
rooms = meeting.room_set.filter(session_types__slug='regular').distinct().order_by("capacity")
|
|
||||||
saveas = SaveAsForm()
|
|
||||||
saveasurl=reverse(edit_schedule,
|
|
||||||
args=[meeting.number, schedule.owner_email(), schedule.name])
|
|
||||||
|
|
||||||
can_see, can_edit,secretariat = schedule_permissions(meeting, schedule, user)
|
|
||||||
|
|
||||||
if not can_see:
|
|
||||||
return render(request, "meeting/private_schedule.html",
|
|
||||||
{"schedule":schedule,
|
|
||||||
"meeting": meeting,
|
|
||||||
"meeting_base_url":meeting_base_url,
|
|
||||||
"hide_menu": True
|
|
||||||
}, status=403, content_type="text/html")
|
|
||||||
|
|
||||||
assignments = get_all_assignments_from_schedule(schedule)
|
|
||||||
|
|
||||||
# get_modified_from needs the query set, not the list
|
|
||||||
modified = get_modified_from_assignments(assignments)
|
|
||||||
|
|
||||||
area_list = get_areas()
|
|
||||||
wg_name_list = get_wg_name_list(assignments)
|
|
||||||
wg_list = get_wg_list(wg_name_list)
|
|
||||||
ads = find_ads_for_meeting(meeting)
|
|
||||||
for ad in ads:
|
|
||||||
# set the default to avoid needing extra arguments in templates
|
|
||||||
# django 1.3+
|
|
||||||
ad.default_hostscheme = site_base_url
|
|
||||||
|
|
||||||
time_slices,date_slices = build_all_agenda_slices(meeting)
|
|
||||||
|
|
||||||
return render(request, "meeting/landscape_edit.html",
|
|
||||||
{"schedule":schedule,
|
|
||||||
"saveas": saveas,
|
|
||||||
"saveasurl": saveasurl,
|
|
||||||
"meeting_base_url": meeting_base_url,
|
|
||||||
"site_base_url": site_base_url,
|
|
||||||
"rooms":rooms,
|
|
||||||
"time_slices":time_slices,
|
|
||||||
"date_slices":date_slices,
|
|
||||||
"modified": modified,
|
|
||||||
"meeting":meeting,
|
|
||||||
"area_list": area_list,
|
|
||||||
"area_directors" : ads,
|
|
||||||
"wg_list": wg_list ,
|
|
||||||
"assignments": assignments,
|
|
||||||
"show_inline": set(["txt","htm","html"]),
|
|
||||||
"hide_menu": True,
|
|
||||||
"can_edit_properties": can_edit or secretariat,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
class SchedulePropertiesForm(forms.ModelForm):
|
class SchedulePropertiesForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Schedule
|
model = Schedule
|
||||||
|
@ -1391,7 +1346,7 @@ def edit_schedule_properties(request, num, owner, name):
|
||||||
form.save()
|
form.save()
|
||||||
if request.GET.get('next'):
|
if request.GET.get('next'):
|
||||||
return HttpResponseRedirect(request.GET.get('next'))
|
return HttpResponseRedirect(request.GET.get('next'))
|
||||||
return redirect('ietf.meeting.views.edit_schedule', num=num, owner=owner, name=name)
|
return redirect('ietf.meeting.views.edit_meeting_schedule', num=num, owner=owner, name=name)
|
||||||
else:
|
else:
|
||||||
form = SchedulePropertiesForm(meeting, instance=schedule)
|
form = SchedulePropertiesForm(meeting, instance=schedule)
|
||||||
|
|
||||||
|
@ -1450,6 +1405,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):
|
||||||
|
@ -1519,114 +1475,10 @@ def get_assignments_for_agenda(schedule):
|
||||||
"""Get queryset containing assignments to show on the agenda"""
|
"""Get queryset containing assignments to show on the agenda"""
|
||||||
return SchedTimeSessAssignment.objects.filter(
|
return SchedTimeSessAssignment.objects.filter(
|
||||||
schedule__in=[schedule, schedule.base],
|
schedule__in=[schedule, schedule.base],
|
||||||
timeslot__type__private=False,
|
session__on_agenda=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
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 +1491,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 +1519,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 +1533,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 +1576,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 +1600,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 +1610,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 +1690,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 +1706,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,
|
||||||
|
@ -1986,10 +1830,10 @@ def week_view(request, num=None, name=None, owner=None):
|
||||||
|
|
||||||
filtered_assignments = SchedTimeSessAssignment.objects.filter(
|
filtered_assignments = SchedTimeSessAssignment.objects.filter(
|
||||||
schedule__in=[schedule, schedule.base],
|
schedule__in=[schedule, schedule.base],
|
||||||
timeslot__type__private=False,
|
session__on_agenda=True,
|
||||||
)
|
)
|
||||||
filtered_assignments = preprocess_assignments_for_agenda(filtered_assignments, meeting)
|
filtered_assignments = preprocess_assignments_for_agenda(filtered_assignments, meeting)
|
||||||
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 +1842,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 +1852,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":
|
||||||
|
@ -2169,10 +2013,10 @@ def agenda_ical(request, num=None, name=None, acronym=None, session_id=None):
|
||||||
|
|
||||||
assignments = SchedTimeSessAssignment.objects.filter(
|
assignments = SchedTimeSessAssignment.objects.filter(
|
||||||
schedule__in=[schedule, schedule.base],
|
schedule__in=[schedule, schedule.base],
|
||||||
timeslot__type__private=False,
|
session__on_agenda=True,
|
||||||
)
|
)
|
||||||
assignments = preprocess_assignments_for_agenda(assignments, meeting)
|
assignments = preprocess_assignments_for_agenda(assignments, meeting)
|
||||||
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)
|
||||||
|
@ -2207,7 +2051,7 @@ def agenda_json(request, num=None):
|
||||||
parent_acronyms = set()
|
parent_acronyms = set()
|
||||||
assignments = SchedTimeSessAssignment.objects.filter(
|
assignments = SchedTimeSessAssignment.objects.filter(
|
||||||
schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None],
|
schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None],
|
||||||
timeslot__type__private=False,
|
session__on_agenda=True,
|
||||||
).exclude(
|
).exclude(
|
||||||
session__type__in=['break', 'reg']
|
session__type__in=['break', 'reg']
|
||||||
)
|
)
|
||||||
|
@ -2330,14 +2174,10 @@ def agenda_json(request, num=None):
|
||||||
|
|
||||||
def meeting_requests(request, num=None):
|
def meeting_requests(request, num=None):
|
||||||
meeting = get_meeting(num)
|
meeting = get_meeting(num)
|
||||||
sessions = add_event_info_to_session_qs(
|
sessions = Session.objects.requests().filter(
|
||||||
Session.objects.filter(
|
meeting__number=meeting.number,
|
||||||
meeting__number=meeting.number,
|
group__parent__isnull=False
|
||||||
type__slug='regular',
|
).with_current_status().with_requested_by().exclude(
|
||||||
group__parent__isnull=False
|
|
||||||
),
|
|
||||||
requested_by=True,
|
|
||||||
).exclude(
|
|
||||||
requested_by=0
|
requested_by=0
|
||||||
).order_by(
|
).order_by(
|
||||||
"group__parent__acronym", "current_status", "group__acronym"
|
"group__parent__acronym", "current_status", "group__acronym"
|
||||||
|
@ -3625,37 +3465,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 +3509,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 +3543,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 +3635,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 +3930,83 @@ 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')
|
||||||
|
def edit_session(request, session_id):
|
||||||
|
session = get_object_or_404(Session, pk=session_id)
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = SessionEditForm(instance=session, data=request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
return HttpResponseRedirect(
|
||||||
|
reverse('ietf.meeting.views.edit_meeting_schedule',
|
||||||
|
kwargs={'num': form.instance.meeting.number}))
|
||||||
|
else:
|
||||||
|
form = SessionEditForm(instance=session)
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
'meeting/edit_session.html',
|
||||||
|
{'session': session, 'form': form},
|
||||||
|
)
|
||||||
|
|
||||||
@role_required('Secretariat')
|
@role_required('Secretariat')
|
||||||
def request_minutes(request, num=None):
|
def request_minutes(request, num=None):
|
||||||
|
|
|
@ -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,
|
||||||
|
@ -2619,6 +2620,7 @@
|
||||||
],
|
],
|
||||||
"req_subm_approval": true,
|
"req_subm_approval": true,
|
||||||
"role_order": "[\n \"chair\",\n \"lead\",\n \"delegate\",\n \"matman\"\n]",
|
"role_order": "[\n \"chair\",\n \"lead\",\n \"delegate\",\n \"matman\"\n]",
|
||||||
|
"session_purposes": "[\n \"presentation\"\n]",
|
||||||
"show_on_agenda": true
|
"show_on_agenda": true
|
||||||
},
|
},
|
||||||
"model": "group.groupfeatures",
|
"model": "group.groupfeatures",
|
||||||
|
@ -2629,6 +2631,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,
|
||||||
|
@ -2654,6 +2657,7 @@
|
||||||
"parent_types": [],
|
"parent_types": [],
|
||||||
"req_subm_approval": false,
|
"req_subm_approval": false,
|
||||||
"role_order": "[\n \"chair\"\n]",
|
"role_order": "[\n \"chair\"\n]",
|
||||||
|
"session_purposes": "[\n \"closed_meeting\",\n \"officehours\"\n]",
|
||||||
"show_on_agenda": false
|
"show_on_agenda": false
|
||||||
},
|
},
|
||||||
"model": "group.groupfeatures",
|
"model": "group.groupfeatures",
|
||||||
|
@ -2664,6 +2668,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,
|
||||||
|
@ -2692,6 +2697,7 @@
|
||||||
],
|
],
|
||||||
"req_subm_approval": true,
|
"req_subm_approval": true,
|
||||||
"role_order": "[\n \"chair\",\n \"secr\"\n]",
|
"role_order": "[\n \"chair\",\n \"secr\"\n]",
|
||||||
|
"session_purposes": "[\n \"regular\"\n]",
|
||||||
"show_on_agenda": true
|
"show_on_agenda": true
|
||||||
},
|
},
|
||||||
"model": "group.groupfeatures",
|
"model": "group.groupfeatures",
|
||||||
|
@ -2702,6 +2708,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,
|
||||||
|
@ -2729,6 +2736,7 @@
|
||||||
],
|
],
|
||||||
"req_subm_approval": true,
|
"req_subm_approval": true,
|
||||||
"role_order": "[\n \"chair\",\n \"secr\"\n]",
|
"role_order": "[\n \"chair\",\n \"secr\"\n]",
|
||||||
|
"session_purposes": "[\n \"regular\"\n]",
|
||||||
"show_on_agenda": false
|
"show_on_agenda": false
|
||||||
},
|
},
|
||||||
"model": "group.groupfeatures",
|
"model": "group.groupfeatures",
|
||||||
|
@ -2739,6 +2747,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,
|
||||||
|
@ -2766,6 +2775,7 @@
|
||||||
],
|
],
|
||||||
"req_subm_approval": true,
|
"req_subm_approval": true,
|
||||||
"role_order": "[\n \"chair\",\n \"secr\"\n]",
|
"role_order": "[\n \"chair\",\n \"secr\"\n]",
|
||||||
|
"session_purposes": "[\n \"open_meeting\",\n \"presentation\",\n \"regular\",\n \"social\",\n \"tutorial\"\n]",
|
||||||
"show_on_agenda": false
|
"show_on_agenda": false
|
||||||
},
|
},
|
||||||
"model": "group.groupfeatures",
|
"model": "group.groupfeatures",
|
||||||
|
@ -2776,6 +2786,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,
|
||||||
|
@ -2803,6 +2814,7 @@
|
||||||
],
|
],
|
||||||
"req_subm_approval": true,
|
"req_subm_approval": true,
|
||||||
"role_order": "[\n \"chair\",\n \"secr\"\n]",
|
"role_order": "[\n \"chair\",\n \"secr\"\n]",
|
||||||
|
"session_purposes": "[\n \"closed_meeting\",\n \"regular\"\n]",
|
||||||
"show_on_agenda": true
|
"show_on_agenda": true
|
||||||
},
|
},
|
||||||
"model": "group.groupfeatures",
|
"model": "group.groupfeatures",
|
||||||
|
@ -2813,6 +2825,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": "none",
|
||||||
"agenda_type": "ad",
|
"agenda_type": "ad",
|
||||||
"create_wiki": false,
|
"create_wiki": false,
|
||||||
"custom_group_roles": true,
|
"custom_group_roles": true,
|
||||||
|
@ -2840,6 +2853,7 @@
|
||||||
],
|
],
|
||||||
"req_subm_approval": false,
|
"req_subm_approval": false,
|
||||||
"role_order": "[\n \"lead\",\n \"chair\",\n \"secr\"\n]",
|
"role_order": "[\n \"lead\",\n \"chair\",\n \"secr\"\n]",
|
||||||
|
"session_purposes": "[\n \"closed_meeting\",\n \"officehours\",\n \"open_meeting\"\n]",
|
||||||
"show_on_agenda": false
|
"show_on_agenda": false
|
||||||
},
|
},
|
||||||
"model": "group.groupfeatures",
|
"model": "group.groupfeatures",
|
||||||
|
@ -2850,6 +2864,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,
|
||||||
|
@ -2875,6 +2890,7 @@
|
||||||
"parent_types": [],
|
"parent_types": [],
|
||||||
"req_subm_approval": false,
|
"req_subm_approval": false,
|
||||||
"role_order": "[\n \"chair\"\n]",
|
"role_order": "[\n \"chair\"\n]",
|
||||||
|
"session_purposes": "[\n \"officehours\"\n]",
|
||||||
"show_on_agenda": false
|
"show_on_agenda": false
|
||||||
},
|
},
|
||||||
"model": "group.groupfeatures",
|
"model": "group.groupfeatures",
|
||||||
|
@ -2885,6 +2901,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,
|
||||||
|
@ -2910,6 +2927,7 @@
|
||||||
"parent_types": [],
|
"parent_types": [],
|
||||||
"req_subm_approval": true,
|
"req_subm_approval": true,
|
||||||
"role_order": "[\n \"chair\",\n \"delegate\",\n \"member\"\n]",
|
"role_order": "[\n \"chair\",\n \"delegate\",\n \"member\"\n]",
|
||||||
|
"session_purposes": "[\n \"closed_meeting\",\n \"open_meeting\"\n]",
|
||||||
"show_on_agenda": false
|
"show_on_agenda": false
|
||||||
},
|
},
|
||||||
"model": "group.groupfeatures",
|
"model": "group.groupfeatures",
|
||||||
|
@ -2920,6 +2938,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,
|
||||||
|
@ -2947,6 +2966,7 @@
|
||||||
],
|
],
|
||||||
"req_subm_approval": true,
|
"req_subm_approval": true,
|
||||||
"role_order": "[\n \"chair\",\n \"secr\"\n]",
|
"role_order": "[\n \"chair\",\n \"secr\"\n]",
|
||||||
|
"session_purposes": "[\n \"admin\",\n \"plenary\",\n \"presentation\",\n \"social\"\n]",
|
||||||
"show_on_agenda": false
|
"show_on_agenda": false
|
||||||
},
|
},
|
||||||
"model": "group.groupfeatures",
|
"model": "group.groupfeatures",
|
||||||
|
@ -2957,6 +2977,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,
|
||||||
|
@ -2984,6 +3005,7 @@
|
||||||
],
|
],
|
||||||
"req_subm_approval": false,
|
"req_subm_approval": false,
|
||||||
"role_order": "[\n \"chair\",\n \"secr\"\n]",
|
"role_order": "[\n \"chair\",\n \"secr\"\n]",
|
||||||
|
"session_purposes": "[]",
|
||||||
"show_on_agenda": false
|
"show_on_agenda": false
|
||||||
},
|
},
|
||||||
"model": "group.groupfeatures",
|
"model": "group.groupfeatures",
|
||||||
|
@ -2994,6 +3016,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,
|
||||||
|
@ -3021,6 +3044,7 @@
|
||||||
],
|
],
|
||||||
"req_subm_approval": true,
|
"req_subm_approval": true,
|
||||||
"role_order": "[\n \"chair\",\n \"secr\"\n]",
|
"role_order": "[\n \"chair\",\n \"secr\"\n]",
|
||||||
|
"session_purposes": "[]",
|
||||||
"show_on_agenda": false
|
"show_on_agenda": false
|
||||||
},
|
},
|
||||||
"model": "group.groupfeatures",
|
"model": "group.groupfeatures",
|
||||||
|
@ -3031,6 +3055,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,
|
||||||
|
@ -3056,6 +3081,7 @@
|
||||||
"parent_types": [],
|
"parent_types": [],
|
||||||
"req_subm_approval": true,
|
"req_subm_approval": true,
|
||||||
"role_order": "[\n \"chair\",\n \"delegate\"\n]",
|
"role_order": "[\n \"chair\",\n \"delegate\"\n]",
|
||||||
|
"session_purposes": "[\n \"officehours\"\n]",
|
||||||
"show_on_agenda": false
|
"show_on_agenda": false
|
||||||
},
|
},
|
||||||
"model": "group.groupfeatures",
|
"model": "group.groupfeatures",
|
||||||
|
@ -3066,6 +3092,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,
|
||||||
|
@ -3093,6 +3120,7 @@
|
||||||
],
|
],
|
||||||
"req_subm_approval": true,
|
"req_subm_approval": true,
|
||||||
"role_order": "[\n \"chair\",\n \"secr\"\n]",
|
"role_order": "[\n \"chair\",\n \"secr\"\n]",
|
||||||
|
"session_purposes": "[\n \"officehours\",\n \"open_meeting\",\n \"presentation\"\n]",
|
||||||
"show_on_agenda": false
|
"show_on_agenda": false
|
||||||
},
|
},
|
||||||
"model": "group.groupfeatures",
|
"model": "group.groupfeatures",
|
||||||
|
@ -3103,6 +3131,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,
|
||||||
|
@ -3130,6 +3159,7 @@
|
||||||
],
|
],
|
||||||
"req_subm_approval": true,
|
"req_subm_approval": true,
|
||||||
"role_order": "[\n \"chair\",\n \"member\",\n \"advisor\"\n]",
|
"role_order": "[\n \"chair\",\n \"member\",\n \"advisor\"\n]",
|
||||||
|
"session_purposes": "[\n \"closed_meeting\",\n \"officehours\"\n]",
|
||||||
"show_on_agenda": false
|
"show_on_agenda": false
|
||||||
},
|
},
|
||||||
"model": "group.groupfeatures",
|
"model": "group.groupfeatures",
|
||||||
|
@ -3140,6 +3170,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,
|
||||||
|
@ -3167,6 +3198,7 @@
|
||||||
],
|
],
|
||||||
"req_subm_approval": false,
|
"req_subm_approval": false,
|
||||||
"role_order": "[\n \"lead\",\n \"chair\",\n \"secr\"\n]",
|
"role_order": "[\n \"lead\",\n \"chair\",\n \"secr\"\n]",
|
||||||
|
"session_purposes": "[\n \"regular\",\n \"tutorial\"\n]",
|
||||||
"show_on_agenda": false
|
"show_on_agenda": false
|
||||||
},
|
},
|
||||||
"model": "group.groupfeatures",
|
"model": "group.groupfeatures",
|
||||||
|
@ -3177,6 +3209,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,
|
||||||
|
@ -3204,6 +3237,7 @@
|
||||||
],
|
],
|
||||||
"req_subm_approval": true,
|
"req_subm_approval": true,
|
||||||
"role_order": "[\n \"chair\",\n \"secr\"\n]",
|
"role_order": "[\n \"chair\",\n \"secr\"\n]",
|
||||||
|
"session_purposes": "[\n \"regular\"\n]",
|
||||||
"show_on_agenda": true
|
"show_on_agenda": true
|
||||||
},
|
},
|
||||||
"model": "group.groupfeatures",
|
"model": "group.groupfeatures",
|
||||||
|
@ -3214,6 +3248,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,
|
||||||
|
@ -3241,6 +3276,7 @@
|
||||||
],
|
],
|
||||||
"req_subm_approval": true,
|
"req_subm_approval": true,
|
||||||
"role_order": "[\n \"chair\",\n \"secr\"\n]",
|
"role_order": "[\n \"chair\",\n \"secr\"\n]",
|
||||||
|
"session_purposes": "[\n \"open_meeting\",\n \"social\"\n]",
|
||||||
"show_on_agenda": false
|
"show_on_agenda": false
|
||||||
},
|
},
|
||||||
"model": "group.groupfeatures",
|
"model": "group.groupfeatures",
|
||||||
|
@ -3251,6 +3287,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,
|
||||||
|
@ -3276,6 +3313,7 @@
|
||||||
"parent_types": [],
|
"parent_types": [],
|
||||||
"req_subm_approval": true,
|
"req_subm_approval": true,
|
||||||
"role_order": "[\n \"chair\",\n \"secr\"\n]",
|
"role_order": "[\n \"chair\",\n \"secr\"\n]",
|
||||||
|
"session_purposes": "[\n \"officehours\"\n]",
|
||||||
"show_on_agenda": false
|
"show_on_agenda": false
|
||||||
},
|
},
|
||||||
"model": "group.groupfeatures",
|
"model": "group.groupfeatures",
|
||||||
|
@ -3286,6 +3324,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,
|
||||||
|
@ -3313,6 +3352,7 @@
|
||||||
],
|
],
|
||||||
"req_subm_approval": true,
|
"req_subm_approval": true,
|
||||||
"role_order": "[\n \"chair\",\n \"delegate\",\n \"secr\"\n]",
|
"role_order": "[\n \"chair\",\n \"delegate\",\n \"secr\"\n]",
|
||||||
|
"session_purposes": "[\n \"regular\"\n]",
|
||||||
"show_on_agenda": true
|
"show_on_agenda": true
|
||||||
},
|
},
|
||||||
"model": "group.groupfeatures",
|
"model": "group.groupfeatures",
|
||||||
|
@ -3323,6 +3363,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,
|
||||||
|
@ -3351,6 +3392,7 @@
|
||||||
],
|
],
|
||||||
"req_subm_approval": true,
|
"req_subm_approval": true,
|
||||||
"role_order": "[\n \"liaiman\"\n]",
|
"role_order": "[\n \"liaiman\"\n]",
|
||||||
|
"session_purposes": "[]",
|
||||||
"show_on_agenda": false
|
"show_on_agenda": false
|
||||||
},
|
},
|
||||||
"model": "group.groupfeatures",
|
"model": "group.groupfeatures",
|
||||||
|
@ -3361,6 +3403,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,
|
||||||
|
@ -3388,6 +3431,7 @@
|
||||||
],
|
],
|
||||||
"req_subm_approval": false,
|
"req_subm_approval": false,
|
||||||
"role_order": "[\n \"chair\",\n \"member\",\n \"matman\"\n]",
|
"role_order": "[\n \"chair\",\n \"member\",\n \"matman\"\n]",
|
||||||
|
"session_purposes": "[\n \"coding\",\n \"presentation\",\n \"social\",\n \"tutorial\"\n]",
|
||||||
"show_on_agenda": false
|
"show_on_agenda": false
|
||||||
},
|
},
|
||||||
"model": "group.groupfeatures",
|
"model": "group.groupfeatures",
|
||||||
|
@ -3398,6 +3442,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,
|
||||||
|
@ -3425,6 +3470,7 @@
|
||||||
],
|
],
|
||||||
"req_subm_approval": true,
|
"req_subm_approval": true,
|
||||||
"role_order": "[\n \"chair\",\n \"secr\",\n \"delegate\"\n]",
|
"role_order": "[\n \"chair\",\n \"secr\",\n \"delegate\"\n]",
|
||||||
|
"session_purposes": "[\n \"regular\"\n]",
|
||||||
"show_on_agenda": true
|
"show_on_agenda": true
|
||||||
},
|
},
|
||||||
"model": "group.groupfeatures",
|
"model": "group.groupfeatures",
|
||||||
|
@ -6095,6 +6141,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": "",
|
||||||
|
@ -12676,6 +12762,138 @@
|
||||||
"model": "name.roomresourcename",
|
"model": "name.roomresourcename",
|
||||||
"pk": "webex"
|
"pk": "webex"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"desc": "Meeting administration",
|
||||||
|
"name": "Administrative",
|
||||||
|
"on_agenda": true,
|
||||||
|
"order": 5,
|
||||||
|
"timeslot_types": "[\n \"other\",\n \"reg\"\n]",
|
||||||
|
"used": true
|
||||||
|
},
|
||||||
|
"model": "name.sessionpurposename",
|
||||||
|
"pk": "admin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"desc": "Closed meeting",
|
||||||
|
"name": "Closed meeting",
|
||||||
|
"on_agenda": false,
|
||||||
|
"order": 10,
|
||||||
|
"timeslot_types": "[\n \"other\",\n \"regular\"\n]",
|
||||||
|
"used": true
|
||||||
|
},
|
||||||
|
"model": "name.sessionpurposename",
|
||||||
|
"pk": "closed_meeting"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"desc": "Coding session",
|
||||||
|
"name": "Coding",
|
||||||
|
"on_agenda": true,
|
||||||
|
"order": 4,
|
||||||
|
"timeslot_types": "[\n \"other\"\n]",
|
||||||
|
"used": true
|
||||||
|
},
|
||||||
|
"model": "name.sessionpurposename",
|
||||||
|
"pk": "coding"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"desc": "Value not set (do not use for new sessions)",
|
||||||
|
"name": "None",
|
||||||
|
"on_agenda": true,
|
||||||
|
"order": 0,
|
||||||
|
"timeslot_types": "[]",
|
||||||
|
"used": false
|
||||||
|
},
|
||||||
|
"model": "name.sessionpurposename",
|
||||||
|
"pk": "none"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"desc": "Office hours session",
|
||||||
|
"name": "Office hours",
|
||||||
|
"on_agenda": true,
|
||||||
|
"order": 3,
|
||||||
|
"timeslot_types": "[\n \"other\"\n]",
|
||||||
|
"used": true
|
||||||
|
},
|
||||||
|
"model": "name.sessionpurposename",
|
||||||
|
"pk": "officehours"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"desc": "Open meeting",
|
||||||
|
"name": "Open meeting",
|
||||||
|
"on_agenda": true,
|
||||||
|
"order": 9,
|
||||||
|
"timeslot_types": "[\n \"other\"\n]",
|
||||||
|
"used": true
|
||||||
|
},
|
||||||
|
"model": "name.sessionpurposename",
|
||||||
|
"pk": "open_meeting"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"desc": "Plenary session",
|
||||||
|
"name": "Plenary",
|
||||||
|
"on_agenda": true,
|
||||||
|
"order": 7,
|
||||||
|
"timeslot_types": "[\n \"plenary\"\n]",
|
||||||
|
"used": true
|
||||||
|
},
|
||||||
|
"model": "name.sessionpurposename",
|
||||||
|
"pk": "plenary"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"desc": "Presentation session",
|
||||||
|
"name": "Presentation",
|
||||||
|
"on_agenda": true,
|
||||||
|
"order": 8,
|
||||||
|
"timeslot_types": "[\n \"other\",\n \"regular\"\n]",
|
||||||
|
"used": true
|
||||||
|
},
|
||||||
|
"model": "name.sessionpurposename",
|
||||||
|
"pk": "presentation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"desc": "Regular group session",
|
||||||
|
"name": "Regular",
|
||||||
|
"on_agenda": true,
|
||||||
|
"order": 1,
|
||||||
|
"timeslot_types": "[\n \"regular\"\n]",
|
||||||
|
"used": true
|
||||||
|
},
|
||||||
|
"model": "name.sessionpurposename",
|
||||||
|
"pk": "regular"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"desc": "Social event or activity",
|
||||||
|
"name": "Social",
|
||||||
|
"on_agenda": true,
|
||||||
|
"order": 6,
|
||||||
|
"timeslot_types": "[\n \"break\",\n \"other\"\n]",
|
||||||
|
"used": true
|
||||||
|
},
|
||||||
|
"model": "name.sessionpurposename",
|
||||||
|
"pk": "social"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"desc": "Tutorial or training session",
|
||||||
|
"name": "Tutorial",
|
||||||
|
"on_agenda": true,
|
||||||
|
"order": 2,
|
||||||
|
"timeslot_types": "[\n \"other\"\n]",
|
||||||
|
"used": true
|
||||||
|
},
|
||||||
|
"model": "name.sessionpurposename",
|
||||||
|
"pk": "tutorial"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fields": {
|
"fields": {
|
||||||
"desc": "",
|
"desc": "",
|
||||||
|
@ -13101,7 +13319,6 @@
|
||||||
"desc": "",
|
"desc": "",
|
||||||
"name": "Break",
|
"name": "Break",
|
||||||
"order": 0,
|
"order": 0,
|
||||||
"private": false,
|
|
||||||
"used": true
|
"used": true
|
||||||
},
|
},
|
||||||
"model": "name.timeslottypename",
|
"model": "name.timeslottypename",
|
||||||
|
@ -13112,7 +13329,6 @@
|
||||||
"desc": "Leadership Meetings",
|
"desc": "Leadership Meetings",
|
||||||
"name": "Leadership",
|
"name": "Leadership",
|
||||||
"order": 0,
|
"order": 0,
|
||||||
"private": true,
|
|
||||||
"used": true
|
"used": true
|
||||||
},
|
},
|
||||||
"model": "name.timeslottypename",
|
"model": "name.timeslottypename",
|
||||||
|
@ -13123,8 +13339,7 @@
|
||||||
"desc": "Other Meetings Not Published on Agenda",
|
"desc": "Other Meetings Not Published on Agenda",
|
||||||
"name": "Off Agenda",
|
"name": "Off Agenda",
|
||||||
"order": 0,
|
"order": 0,
|
||||||
"private": true,
|
"used": false
|
||||||
"used": true
|
|
||||||
},
|
},
|
||||||
"model": "name.timeslottypename",
|
"model": "name.timeslottypename",
|
||||||
"pk": "offagenda"
|
"pk": "offagenda"
|
||||||
|
@ -13134,7 +13349,6 @@
|
||||||
"desc": "",
|
"desc": "",
|
||||||
"name": "Other",
|
"name": "Other",
|
||||||
"order": 0,
|
"order": 0,
|
||||||
"private": false,
|
|
||||||
"used": true
|
"used": true
|
||||||
},
|
},
|
||||||
"model": "name.timeslottypename",
|
"model": "name.timeslottypename",
|
||||||
|
@ -13145,7 +13359,6 @@
|
||||||
"desc": "",
|
"desc": "",
|
||||||
"name": "Plenary",
|
"name": "Plenary",
|
||||||
"order": 0,
|
"order": 0,
|
||||||
"private": false,
|
|
||||||
"used": true
|
"used": true
|
||||||
},
|
},
|
||||||
"model": "name.timeslottypename",
|
"model": "name.timeslottypename",
|
||||||
|
@ -13156,7 +13369,6 @@
|
||||||
"desc": "",
|
"desc": "",
|
||||||
"name": "Registration",
|
"name": "Registration",
|
||||||
"order": 0,
|
"order": 0,
|
||||||
"private": false,
|
|
||||||
"used": true
|
"used": true
|
||||||
},
|
},
|
||||||
"model": "name.timeslottypename",
|
"model": "name.timeslottypename",
|
||||||
|
@ -13167,7 +13379,6 @@
|
||||||
"desc": "",
|
"desc": "",
|
||||||
"name": "Regular",
|
"name": "Regular",
|
||||||
"order": 0,
|
"order": 0,
|
||||||
"private": false,
|
|
||||||
"used": true
|
"used": true
|
||||||
},
|
},
|
||||||
"model": "name.timeslottypename",
|
"model": "name.timeslottypename",
|
||||||
|
@ -13178,8 +13389,7 @@
|
||||||
"desc": "A room has been reserved for use by another body the timeslot indicated",
|
"desc": "A room has been reserved for use by another body the timeslot indicated",
|
||||||
"name": "Room Reserved",
|
"name": "Room Reserved",
|
||||||
"order": 0,
|
"order": 0,
|
||||||
"private": false,
|
"used": false
|
||||||
"used": true
|
|
||||||
},
|
},
|
||||||
"model": "name.timeslottypename",
|
"model": "name.timeslottypename",
|
||||||
"pk": "reserved"
|
"pk": "reserved"
|
||||||
|
@ -13189,7 +13399,6 @@
|
||||||
"desc": "A room was not booked for the timeslot indicated",
|
"desc": "A room was not booked for the timeslot indicated",
|
||||||
"name": "Room Unavailable",
|
"name": "Room Unavailable",
|
||||||
"order": 0,
|
"order": 0,
|
||||||
"private": false,
|
|
||||||
"used": true
|
"used": true
|
||||||
},
|
},
|
||||||
"model": "name.timeslottypename",
|
"model": "name.timeslottypename",
|
||||||
|
|
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),
|
||||||
|
]
|
33
ietf/name/migrations/0034_sessionpurposename.py
Normal file
33
ietf/name/migrations/0034_sessionpurposename.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# 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', '0033_populate_agendafiltertypename'),
|
||||||
|
]
|
||||||
|
|
||||||
|
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')])),
|
||||||
|
('on_agenda', models.BooleanField(default=True, help_text='Are sessions of this purpose visible on the agenda by default?')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['order', 'name'],
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
53
ietf/name/migrations/0035_populate_sessionpurposename.py
Normal file
53
ietf/name/migrations/0035_populate_sessionpurposename.py
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
# 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, on_agenda, used) in enumerate((
|
||||||
|
('none', 'None', 'Value not set (do not use for new sessions)', [], True, False),
|
||||||
|
('regular', 'Regular', 'Regular group session', ['regular'], True, True),
|
||||||
|
('tutorial', 'Tutorial', 'Tutorial or training session', ['other'], True, True),
|
||||||
|
('officehours', 'Office hours', 'Office hours session', ['other'], True, True),
|
||||||
|
('coding', 'Coding', 'Coding session', ['other'], True, True),
|
||||||
|
('admin', 'Administrative', 'Meeting administration', ['other', 'reg'], True, True),
|
||||||
|
('social', 'Social', 'Social event or activity', ['break', 'other'], True, True),
|
||||||
|
('plenary', 'Plenary', 'Plenary session', ['plenary'], True, True),
|
||||||
|
('presentation', 'Presentation', 'Presentation session', ['other', 'regular'], True, True),
|
||||||
|
('open_meeting', 'Open meeting', 'Open meeting', ['other'], True, True),
|
||||||
|
('closed_meeting', 'Closed meeting', 'Closed meeting', ['other', 'regular'], False, True),
|
||||||
|
)):
|
||||||
|
# verify that we're not about to use an invalid type
|
||||||
|
for ts_type in tstypes:
|
||||||
|
TimeSlotTypeName.objects.get(pk=ts_type) # throws an exception unless exists
|
||||||
|
|
||||||
|
SessionPurposeName.objects.create(
|
||||||
|
slug=slug,
|
||||||
|
name=name,
|
||||||
|
desc=desc,
|
||||||
|
used=used,
|
||||||
|
order=order,
|
||||||
|
timeslot_types = tstypes,
|
||||||
|
on_agenda=on_agenda,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def reverse(apps, schema_editor):
|
||||||
|
SessionPurposeName = apps.get_model('name', 'SessionPurposeName')
|
||||||
|
SessionPurposeName.objects.all().delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('name', '0034_sessionpurposename'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(forward, reverse)
|
||||||
|
]
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Generated by Django 2.2.24 on 2021-10-25 16:58
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
PRIVATE_TIMESLOT_SLUGS = {'lead', 'offagenda'} # from DB 2021 Oct
|
||||||
|
|
||||||
|
|
||||||
|
def forward(apps, schema_editor):
|
||||||
|
TimeSlotTypeName = apps.get_model('name', 'TimeSlotTypeName')
|
||||||
|
slugs = TimeSlotTypeName.objects.filter(private=True).values_list('slug', flat=True)
|
||||||
|
if set(slugs) != PRIVATE_TIMESLOT_SLUGS:
|
||||||
|
# the reverse migration will not restore the database, refuse to migrate
|
||||||
|
raise ValueError('Disagreement between migration data and database')
|
||||||
|
|
||||||
|
|
||||||
|
def reverse(apps, schema_editor):
|
||||||
|
TimeSlotTypeName = apps.get_model('name', 'TimeSlotTypeName')
|
||||||
|
TimeSlotTypeName.objects.filter(slug__in=PRIVATE_TIMESLOT_SLUGS).update(private=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('name', '0035_populate_sessionpurposename'),
|
||||||
|
('meeting', '0051_populate_session_on_agenda'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(forward, reverse),
|
||||||
|
]
|
17
ietf/name/migrations/0037_remove_timeslottypename_private.py
Normal file
17
ietf/name/migrations/0037_remove_timeslottypename_private.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# Generated by Django 2.2.24 on 2021-10-25 17:23
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('name', '0036_depopulate_timeslottypename_private'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='timeslottypename',
|
||||||
|
name='private',
|
||||||
|
),
|
||||||
|
]
|
24
ietf/name/migrations/0038_disuse_offagenda_and_reserved.py
Normal file
24
ietf/name/migrations/0038_disuse_offagenda_and_reserved.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# Generated by Django 2.2.24 on 2021-10-29 06:44
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def forward(apps, schema_editor):
|
||||||
|
TimeSlotTypeName = apps.get_model('name', 'TimeSlotTypeName')
|
||||||
|
TimeSlotTypeName.objects.filter(slug__in=('offagenda', 'reserved')).update(used=False)
|
||||||
|
|
||||||
|
|
||||||
|
def reverse(apps, schema_editor):
|
||||||
|
TimeSlotTypeName = apps.get_model('name', 'TimeSlotTypeName')
|
||||||
|
TimeSlotTypeName.objects.filter(slug__in=('offagenda', 'reserved')).update(used=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('name', '0037_remove_timeslottypename_private'),
|
||||||
|
]
|
||||||
|
|
||||||
|
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,11 +67,21 @@ 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')],
|
||||||
|
)
|
||||||
|
on_agenda = models.BooleanField(default=True, help_text='Are sessions of this purpose visible on the agenda by default?')
|
||||||
|
|
||||||
class TimeSlotTypeName(NameModel):
|
class TimeSlotTypeName(NameModel):
|
||||||
"""Session, Break, Registration, Other, Reserved, unavail"""
|
"""Session, Break, Registration, Other, Reserved, unavail"""
|
||||||
private = models.BooleanField(default=False, help_text="Whether sessions of this type should be kept off the public agenda")
|
|
||||||
class ConstraintName(NameModel):
|
class ConstraintName(NameModel):
|
||||||
"""conflict, conflic2, conflic3, bethere, timerange, time_relation, wg_adjacent"""
|
"""conflict, conflic2, conflic3, bethere, timerange, time_relation, wg_adjacent"""
|
||||||
penalty = models.IntegerField(default=0, help_text="The penalty for violating this kind of constraint; for instance 10 (small penalty) or 10000 (large penalty)")
|
penalty = models.IntegerField(default=0, help_text="The penalty for violating this kind of constraint; for instance 10 (small penalty) or 10000 (large penalty)")
|
||||||
|
|
|
@ -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,
|
||||||
|
@ -18,7 +18,7 @@ from ietf.name.models import ( AgendaTypeName, BallotPositionName, ConstraintNam
|
||||||
ReviewAssignmentStateName, ReviewRequestStateName, ReviewResultName, ReviewTypeName,
|
ReviewAssignmentStateName, ReviewRequestStateName, ReviewResultName, ReviewTypeName,
|
||||||
RoleName, RoomResourceName, SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName,
|
RoleName, RoomResourceName, SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName,
|
||||||
TopicAudienceName, ReviewerQueuePolicyName, TimerangeName, ExtResourceTypeName, ExtResourceName,
|
TopicAudienceName, ReviewerQueuePolicyName, TimerangeName, ExtResourceTypeName, ExtResourceName,
|
||||||
SlideSubmissionStatusName, ProceedingsMaterialTypeName)
|
SlideSubmissionStatusName, ProceedingsMaterialTypeName, SessionPurposeName )
|
||||||
|
|
||||||
class TimeSlotTypeNameResource(ModelResource):
|
class TimeSlotTypeNameResource(ModelResource):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -684,3 +684,39 @@ 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())
|
||||||
|
|
||||||
|
|
||||||
|
class SessionPurposeNameResource(ModelResource):
|
||||||
|
class Meta:
|
||||||
|
queryset = SessionPurposeName.objects.all()
|
||||||
|
serializer = api.Serializer()
|
||||||
|
cache = SimpleCache()
|
||||||
|
#resource_name = 'sessionpurposename'
|
||||||
|
ordering = ['slug', ]
|
||||||
|
filtering = {
|
||||||
|
"slug": ALL,
|
||||||
|
"name": ALL,
|
||||||
|
"desc": ALL,
|
||||||
|
"used": ALL,
|
||||||
|
"order": ALL,
|
||||||
|
"timeslot_types": ALL,
|
||||||
|
"on_agenda": ALL,
|
||||||
|
}
|
||||||
|
api.name.register(SessionPurposeNameResource())
|
||||||
|
|
|
@ -6,14 +6,6 @@ from django.http import HttpResponse
|
||||||
from ietf.ietfauth.utils import role_required
|
from ietf.ietfauth.utils import role_required
|
||||||
from ietf.person.models import Person
|
from ietf.person.models import Person
|
||||||
|
|
||||||
def person_json(request, personid):
|
|
||||||
person = get_object_or_404(Person, pk=personid)
|
|
||||||
|
|
||||||
return HttpResponse(json.dumps(person.json_dict(request.build_absolute_uri("/")),
|
|
||||||
sort_keys=True, indent=2),
|
|
||||||
content_type="application/json")
|
|
||||||
|
|
||||||
|
|
||||||
@role_required('Secretariat')
|
@role_required('Secretariat')
|
||||||
def person_email_json(request, personid):
|
def person_email_json(request, personid):
|
||||||
person = get_object_or_404(Person, pk=personid)
|
person = get_object_or_404(Person, pk=personid)
|
||||||
|
|
|
@ -228,18 +228,6 @@ class Person(models.Model):
|
||||||
def defurl(self):
|
def defurl(self):
|
||||||
return urljoin(self.default_hostscheme,self.json_url())
|
return urljoin(self.default_hostscheme,self.json_url())
|
||||||
|
|
||||||
def json_url(self):
|
|
||||||
return "/person/%s.json" % (self.id, )
|
|
||||||
|
|
||||||
# return info about the person
|
|
||||||
def json_dict(self, hostscheme):
|
|
||||||
ct1 = dict()
|
|
||||||
ct1['person_id'] = self.id
|
|
||||||
ct1['href'] = urljoin(hostscheme, self.json_url())
|
|
||||||
ct1['name'] = self.name
|
|
||||||
ct1['ascii'] = self.ascii
|
|
||||||
return ct1
|
|
||||||
|
|
||||||
def available_api_endpoints(self):
|
def available_api_endpoints(self):
|
||||||
from ietf.ietfauth.utils import has_role
|
from ietf.ietfauth.utils import has_role
|
||||||
return list(set([ (v, n) for (v, n, r) in PERSON_API_KEY_VALUES if r==None or has_role(self.user, r) ]))
|
return list(set([ (v, n) for (v, n, r) in PERSON_API_KEY_VALUES if r==None or has_role(self.user, r) ]))
|
||||||
|
|
|
@ -4,7 +4,6 @@ from ietf.utils.urls import url
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^merge/$', views.merge),
|
url(r'^merge/$', views.merge),
|
||||||
url(r'^search/(?P<model_name>(person|email))/$', views.ajax_select2_search),
|
url(r'^search/(?P<model_name>(person|email))/$', views.ajax_select2_search),
|
||||||
url(r'^(?P<personid>[0-9]+).json$', ajax.person_json),
|
|
||||||
url(r'^(?P<personid>[0-9]+)/email.json$', ajax.person_email_json),
|
url(r'^(?P<personid>[0-9]+)/email.json$', ajax.person_email_json),
|
||||||
url(r'^(?P<email_or_name>[^/]+)$', views.profile),
|
url(r'^(?P<email_or_name>[^/]+)$', views.profile),
|
||||||
url(r'^(?P<email_or_name>[^/]+)/photo/?$', views.photo),
|
url(r'^(?P<email_or_name>[^/]+)/photo/?$', views.photo),
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -336,7 +336,8 @@ class SecrMeetingTestCase(TestCase):
|
||||||
'duration':'02:00',
|
'duration':'02:00',
|
||||||
'name':'Testing',
|
'name':'Testing',
|
||||||
'short':'test',
|
'short':'test',
|
||||||
'type':'reg',
|
'purpose_0': 'admin', # purpose
|
||||||
|
'purpose_1':'reg', # type
|
||||||
'group':group.pk,
|
'group':group.pk,
|
||||||
'location': room.pk,
|
'location': room.pk,
|
||||||
'remote_instructions': 'http://webex.com/foobar',
|
'remote_instructions': 'http://webex.com/foobar',
|
||||||
|
@ -382,7 +383,8 @@ class SecrMeetingTestCase(TestCase):
|
||||||
'time':new_time.strftime('%H:%M'),
|
'time':new_time.strftime('%H:%M'),
|
||||||
'duration':'01:00',
|
'duration':'01:00',
|
||||||
'day':'2',
|
'day':'2',
|
||||||
'type':'other',
|
'purpose_0': 'coding', # purpose
|
||||||
|
'purpose_1': 'other', # type
|
||||||
'remote_instructions': 'http://webex.com/foobar',
|
'remote_instructions': 'http://webex.com/foobar',
|
||||||
})
|
})
|
||||||
self.assertRedirects(response, redirect_url)
|
self.assertRedirects(response, redirect_url)
|
||||||
|
|
|
@ -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_factory
|
||||||
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,21 @@ 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
|
||||||
|
formset_class = sessiondetailsformset_factory(max_num=3 if group.features.acts_like_wg else 12)
|
||||||
|
self.session_forms = formset_class(group=self.group, meeting=meeting, data=data)
|
||||||
|
super(SessionForm, self).__init__(data=data, *args, **kwargs)
|
||||||
|
|
||||||
|
# Allow additional sessions for non-wg-like groups
|
||||||
|
if not self.group.features.acts_like_wg:
|
||||||
|
self.fields['num_session'].choices = ((n, str(n)) for n in range(1, 13))
|
||||||
|
|
||||||
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 +149,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 +229,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 +253,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 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 + forms.Media(js=('secr/js/session_form.js',))
|
||||||
|
|
||||||
|
|
||||||
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,19 @@ 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',
|
if value in (None, ''):
|
||||||
'1800':'30 Minutes',
|
return 'unspecified'
|
||||||
'3000':'50 Minutes',
|
value = int(value)
|
||||||
'3600':'1 Hour',
|
map = {0: 'None',
|
||||||
'5400':'1.5 Hours',
|
1800: '30 Minutes',
|
||||||
'6000':'100 Minutes',
|
3600: '1 Hour',
|
||||||
'7200':'2 Hours',
|
5400: '1.5 Hours',
|
||||||
'9000':'2.5 Hours'}
|
7200: '2 Hours',
|
||||||
return map[value]
|
9000: '2.5 Hours'}
|
||||||
|
if value in map:
|
||||||
|
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):
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
from django.test import override_settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
import debug # pyflakes:ignore
|
import debug # pyflakes:ignore
|
||||||
|
@ -76,6 +77,7 @@ class SessionRequestTestCase(TestCase):
|
||||||
self.assertRedirects(r,reverse('ietf.secr.sreq.views.main'))
|
self.assertRedirects(r,reverse('ietf.secr.sreq.views.main'))
|
||||||
self.assertEqual(SchedulingEvent.objects.filter(session=session).order_by('-id')[0].status_id, 'deleted')
|
self.assertEqual(SchedulingEvent.objects.filter(session=session).order_by('-id')[0].status_id, 'deleted')
|
||||||
|
|
||||||
|
@override_settings(SECR_VIRTUAL_MEETINGS=tuple()) # ensure not unexpectedly testing a virtual meeting session
|
||||||
def test_edit(self):
|
def test_edit(self):
|
||||||
meeting = MeetingFactory(type_id='ietf', date=datetime.date.today())
|
meeting = MeetingFactory(type_id='ietf', date=datetime.date.today())
|
||||||
mars = RoleFactory(name_id='chair', person__user__username='marschairman', group__acronym='mars').group
|
mars = RoleFactory(name_id='chair', person__user__username='marschairman', group__acronym='mars').group
|
||||||
|
@ -90,17 +92,43 @@ class SessionRequestTestCase(TestCase):
|
||||||
self.client.login(username="marschairman", password="marschairman+password")
|
self.client.login(username="marschairman", password="marschairman+password")
|
||||||
r = self.client.get(url)
|
r = self.client.get(url)
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
|
attendees = 10
|
||||||
|
comments = 'need lights'
|
||||||
|
mars_sessions = meeting.session_set.filter(group__acronym='mars')
|
||||||
post_data = {'num_session':'2',
|
post_data = {'num_session':'2',
|
||||||
'length_session1':'3600',
|
'attendees': attendees,
|
||||||
'length_session2':'3600',
|
|
||||||
'attendees':'10',
|
|
||||||
'constraint_chair_conflict':iabprog.acronym,
|
'constraint_chair_conflict':iabprog.acronym,
|
||||||
'comments':'need lights',
|
|
||||||
'session_time_relation': 'subsequent-days',
|
'session_time_relation': 'subsequent-days',
|
||||||
'adjacent_with_wg': group2.acronym,
|
'adjacent_with_wg': group2.acronym,
|
||||||
'joint_with_groups': group3.acronym + ' ' + group4.acronym,
|
'joint_with_groups': group3.acronym + ' ' + group4.acronym,
|
||||||
'joint_for_session': '2',
|
'joint_for_session': '2',
|
||||||
'timeranges': ['thursday-afternoon-early', 'thursday-afternoon-late'],
|
'timeranges': ['thursday-afternoon-early', 'thursday-afternoon-late'],
|
||||||
|
'session_set-TOTAL_FORMS': '2',
|
||||||
|
'session_set-INITIAL_FORMS': '1',
|
||||||
|
'session_set-MIN_NUM_FORMS': '1',
|
||||||
|
'session_set-MAX_NUM_FORMS': '3',
|
||||||
|
'session_set-0-id':mars_sessions[0].pk,
|
||||||
|
'session_set-0-name': mars_sessions[0].name,
|
||||||
|
'session_set-0-short': mars_sessions[0].short,
|
||||||
|
'session_set-0-purpose': mars_sessions[0].purpose_id,
|
||||||
|
'session_set-0-type': mars_sessions[0].type_id,
|
||||||
|
'session_set-0-requested_duration': '3600',
|
||||||
|
'session_set-0-on_agenda': mars_sessions[0].on_agenda,
|
||||||
|
'session_set-0-remote_instructions': mars_sessions[0].remote_instructions,
|
||||||
|
'session_set-0-attendees': attendees,
|
||||||
|
'session_set-0-comments': comments,
|
||||||
|
'session_set-0-DELETE': '',
|
||||||
|
# no session_set-1-id because it's a new request
|
||||||
|
'session_set-1-name': '',
|
||||||
|
'session_set-1-short': '',
|
||||||
|
'session_set-1-purpose': 'regular',
|
||||||
|
'session_set-1-type': 'regular',
|
||||||
|
'session_set-1-requested_duration': '3600',
|
||||||
|
'session_set-1-on_agenda': True,
|
||||||
|
'session_set-1-remote_instructions': mars_sessions[0].remote_instructions,
|
||||||
|
'session_set-1-attendees': attendees,
|
||||||
|
'session_set-1-comments': comments,
|
||||||
|
'session_set-1-DELETE': '',
|
||||||
'submit': 'Continue'}
|
'submit': 'Continue'}
|
||||||
r = self.client.post(url, post_data, HTTP_HOST='example.com')
|
r = self.client.post(url, post_data, HTTP_HOST='example.com')
|
||||||
redirect_url = reverse('ietf.secr.sreq.views.view', kwargs={'acronym': 'mars'})
|
redirect_url = reverse('ietf.secr.sreq.views.view', kwargs={'acronym': 'mars'})
|
||||||
|
@ -133,11 +161,37 @@ class SessionRequestTestCase(TestCase):
|
||||||
post_data = {'num_session':'2',
|
post_data = {'num_session':'2',
|
||||||
'length_session1':'3600',
|
'length_session1':'3600',
|
||||||
'length_session2':'3600',
|
'length_session2':'3600',
|
||||||
'attendees':'10',
|
'attendees':attendees,
|
||||||
'constraint_chair_conflict':'',
|
'constraint_chair_conflict':'',
|
||||||
'comments':'need lights',
|
'comments':'need lights',
|
||||||
'joint_with_groups': group2.acronym,
|
'joint_with_groups': group2.acronym,
|
||||||
'joint_for_session': '1',
|
'joint_for_session': '1',
|
||||||
|
'session_set-TOTAL_FORMS': '2',
|
||||||
|
'session_set-INITIAL_FORMS': '2',
|
||||||
|
'session_set-MIN_NUM_FORMS': '1',
|
||||||
|
'session_set-MAX_NUM_FORMS': '3',
|
||||||
|
'session_set-0-id':sessions[0].pk,
|
||||||
|
'session_set-0-name': sessions[0].name,
|
||||||
|
'session_set-0-short': sessions[0].short,
|
||||||
|
'session_set-0-purpose': sessions[0].purpose_id,
|
||||||
|
'session_set-0-type': sessions[0].type_id,
|
||||||
|
'session_set-0-requested_duration': '3600',
|
||||||
|
'session_set-0-on_agenda': sessions[0].on_agenda,
|
||||||
|
'session_set-0-remote_instructions': sessions[0].remote_instructions,
|
||||||
|
'session_set-0-attendees': sessions[0].attendees,
|
||||||
|
'session_set-0-comments': sessions[1].comments,
|
||||||
|
'session_set-0-DELETE': '',
|
||||||
|
'session_set-1-id': sessions[1].pk,
|
||||||
|
'session_set-1-name': sessions[1].name,
|
||||||
|
'session_set-1-short': sessions[1].short,
|
||||||
|
'session_set-1-purpose': sessions[1].purpose_id,
|
||||||
|
'session_set-1-type': sessions[1].type_id,
|
||||||
|
'session_set-1-requested_duration': '3600',
|
||||||
|
'session_set-1-on_agenda': sessions[1].on_agenda,
|
||||||
|
'session_set-1-remote_instructions': sessions[1].remote_instructions,
|
||||||
|
'session_set-1-attendees': sessions[1].attendees,
|
||||||
|
'session_set-1-comments': sessions[1].comments,
|
||||||
|
'session_set-1-DELETE': '',
|
||||||
'submit': 'Continue'}
|
'submit': 'Continue'}
|
||||||
r = self.client.post(url, post_data, HTTP_HOST='example.com')
|
r = self.client.post(url, post_data, HTTP_HOST='example.com')
|
||||||
self.assertRedirects(r, redirect_url)
|
self.assertRedirects(r, redirect_url)
|
||||||
|
@ -160,7 +214,7 @@ class SessionRequestTestCase(TestCase):
|
||||||
"""Inactive conflicts should be displayed and removable"""
|
"""Inactive conflicts should be displayed and removable"""
|
||||||
meeting = MeetingFactory(type_id='ietf', date=datetime.date.today(), group_conflicts=['chair_conflict'])
|
meeting = MeetingFactory(type_id='ietf', date=datetime.date.today(), group_conflicts=['chair_conflict'])
|
||||||
mars = RoleFactory(name_id='chair', person__user__username='marschairman', group__acronym='mars').group
|
mars = RoleFactory(name_id='chair', person__user__username='marschairman', group__acronym='mars').group
|
||||||
SessionFactory(meeting=meeting, group=mars, status_id='sched')
|
session = SessionFactory(meeting=meeting, group=mars, status_id='sched')
|
||||||
other_group = GroupFactory()
|
other_group = GroupFactory()
|
||||||
Constraint.objects.create(
|
Constraint.objects.create(
|
||||||
meeting=meeting,
|
meeting=meeting,
|
||||||
|
@ -184,16 +238,31 @@ class SessionRequestTestCase(TestCase):
|
||||||
# check that the target is displayed correctly in the UI
|
# check that the target is displayed correctly in the UI
|
||||||
self.assertIn(other_group.acronym, delete_checkbox.find('../input[@type="text"]').value)
|
self.assertIn(other_group.acronym, delete_checkbox.find('../input[@type="text"]').value)
|
||||||
|
|
||||||
|
attendees = '10'
|
||||||
post_data = {
|
post_data = {
|
||||||
'num_session': '1',
|
'num_session': '1',
|
||||||
'length_session1': '3600',
|
'attendees': attendees,
|
||||||
'attendees': '10',
|
|
||||||
'constraint_chair_conflict':'',
|
'constraint_chair_conflict':'',
|
||||||
'comments':'',
|
'comments':'',
|
||||||
'joint_with_groups': '',
|
'joint_with_groups': '',
|
||||||
'joint_for_session': '',
|
'joint_for_session': '',
|
||||||
'submit': 'Save',
|
|
||||||
'delete_conflict': 'on',
|
'delete_conflict': 'on',
|
||||||
|
'session_set-TOTAL_FORMS': '1',
|
||||||
|
'session_set-INITIAL_FORMS': '1',
|
||||||
|
'session_set-MIN_NUM_FORMS': '1',
|
||||||
|
'session_set-MAX_NUM_FORMS': '3',
|
||||||
|
'session_set-0-id':session.pk,
|
||||||
|
'session_set-0-name': session.name,
|
||||||
|
'session_set-0-short': session.short,
|
||||||
|
'session_set-0-purpose': session.purpose_id,
|
||||||
|
'session_set-0-type': session.type_id,
|
||||||
|
'session_set-0-requested_duration': '3600',
|
||||||
|
'session_set-0-on_agenda': session.on_agenda,
|
||||||
|
'session_set-0-remote_instructions': session.remote_instructions,
|
||||||
|
'session_set-0-attendees': attendees,
|
||||||
|
'session_set-0-comments': '',
|
||||||
|
'session_set-0-DELETE': '',
|
||||||
|
'submit': 'Save',
|
||||||
}
|
}
|
||||||
r = self.client.post(url, post_data, HTTP_HOST='example.com')
|
r = self.client.post(url, post_data, HTTP_HOST='example.com')
|
||||||
redirect_url = reverse('ietf.secr.sreq.views.view', kwargs={'acronym': 'mars'})
|
redirect_url = reverse('ietf.secr.sreq.views.view', kwargs={'acronym': 'mars'})
|
||||||
|
@ -283,15 +352,31 @@ class SubmitRequestCase(TestCase):
|
||||||
url = reverse('ietf.secr.sreq.views.new',kwargs={'acronym':group.acronym})
|
url = reverse('ietf.secr.sreq.views.new',kwargs={'acronym':group.acronym})
|
||||||
confirm_url = reverse('ietf.secr.sreq.views.confirm',kwargs={'acronym':group.acronym})
|
confirm_url = reverse('ietf.secr.sreq.views.confirm',kwargs={'acronym':group.acronym})
|
||||||
main_url = reverse('ietf.secr.sreq.views.main')
|
main_url = reverse('ietf.secr.sreq.views.main')
|
||||||
|
attendees = '10'
|
||||||
|
comments = 'need projector'
|
||||||
post_data = {'num_session':'1',
|
post_data = {'num_session':'1',
|
||||||
'length_session1':'3600',
|
'attendees':attendees,
|
||||||
'attendees':'10',
|
|
||||||
'constraint_chair_conflict':'',
|
'constraint_chair_conflict':'',
|
||||||
'comments':'need projector',
|
'comments':comments,
|
||||||
'adjacent_with_wg': group2.acronym,
|
'adjacent_with_wg': group2.acronym,
|
||||||
'timeranges': ['thursday-afternoon-early', 'thursday-afternoon-late'],
|
'timeranges': ['thursday-afternoon-early', 'thursday-afternoon-late'],
|
||||||
'joint_with_groups': group3.acronym + ' ' + group4.acronym,
|
'joint_with_groups': group3.acronym + ' ' + group4.acronym,
|
||||||
'joint_for_session': '1',
|
'joint_for_session': '1',
|
||||||
|
'session_set-TOTAL_FORMS': '1',
|
||||||
|
'session_set-INITIAL_FORMS': '0',
|
||||||
|
'session_set-MIN_NUM_FORMS': '1',
|
||||||
|
'session_set-MAX_NUM_FORMS': '3',
|
||||||
|
# no 'session_set-0-id' to create a new session
|
||||||
|
'session_set-0-name': '',
|
||||||
|
'session_set-0-short': '',
|
||||||
|
'session_set-0-purpose': 'regular',
|
||||||
|
'session_set-0-type': 'regular',
|
||||||
|
'session_set-0-requested_duration': '3600',
|
||||||
|
'session_set-0-on_agenda': True,
|
||||||
|
'session_set-0-remote_instructions': '',
|
||||||
|
'session_set-0-attendees': attendees,
|
||||||
|
'session_set-0-comments': comments,
|
||||||
|
'session_set-0-DELETE': '',
|
||||||
'submit': 'Continue'}
|
'submit': 'Continue'}
|
||||||
self.client.login(username="secretary", password="secretary+password")
|
self.client.login(username="secretary", password="secretary+password")
|
||||||
r = self.client.post(url,post_data)
|
r = self.client.post(url,post_data)
|
||||||
|
@ -313,7 +398,7 @@ class SubmitRequestCase(TestCase):
|
||||||
self.assertRedirects(r, main_url)
|
self.assertRedirects(r, main_url)
|
||||||
session_count_after = Session.objects.filter(meeting=meeting, group=group, type='regular').count()
|
session_count_after = Session.objects.filter(meeting=meeting, group=group, type='regular').count()
|
||||||
self.assertEqual(session_count_after, session_count_before + 1)
|
self.assertEqual(session_count_after, session_count_before + 1)
|
||||||
|
|
||||||
# Verify database content
|
# Verify database content
|
||||||
session = Session.objects.get(meeting=meeting, group=group)
|
session = Session.objects.get(meeting=meeting, group=group)
|
||||||
self.assertEqual(session.constraints().get(name='wg_adjacent').target.acronym, group2.acronym)
|
self.assertEqual(session.constraints().get(name='wg_adjacent').target.acronym, group2.acronym)
|
||||||
|
@ -329,17 +414,35 @@ class SubmitRequestCase(TestCase):
|
||||||
area = RoleFactory(name_id='ad', person=ad, group__type_id='area').group
|
area = RoleFactory(name_id='ad', person=ad, group__type_id='area').group
|
||||||
group = GroupFactory(parent=area)
|
group = GroupFactory(parent=area)
|
||||||
url = reverse('ietf.secr.sreq.views.new',kwargs={'acronym':group.acronym})
|
url = reverse('ietf.secr.sreq.views.new',kwargs={'acronym':group.acronym})
|
||||||
post_data = {'num_session':'2',
|
attendees = '10'
|
||||||
'length_session1':'3600',
|
comments = 'need projector'
|
||||||
'attendees':'10',
|
post_data = {
|
||||||
'constraint_chair_conflict':'',
|
'num_session':'2',
|
||||||
'comments':'need projector'}
|
'attendees':attendees,
|
||||||
|
'constraint_chair_conflict':'',
|
||||||
|
'comments':comments,
|
||||||
|
'session_set-TOTAL_FORMS': '1',
|
||||||
|
'session_set-INITIAL_FORMS': '1',
|
||||||
|
'session_set-MIN_NUM_FORMS': '1',
|
||||||
|
'session_set-MAX_NUM_FORMS': '3',
|
||||||
|
# no 'session_set-0-id' to create a new session
|
||||||
|
'session_set-0-name': '',
|
||||||
|
'session_set-0-short': '',
|
||||||
|
'session_set-0-purpose': 'regular',
|
||||||
|
'session_set-0-type': 'regular',
|
||||||
|
'session_set-0-requested_duration': '3600',
|
||||||
|
'session_set-0-on_agenda': True,
|
||||||
|
'session_set-0-remote_instructions': '',
|
||||||
|
'session_set-0-attendees': attendees,
|
||||||
|
'session_set-0-comments': comments,
|
||||||
|
'session_set-0-DELETE': '',
|
||||||
|
}
|
||||||
self.client.login(username="secretary", password="secretary+password")
|
self.client.login(username="secretary", password="secretary+password")
|
||||||
r = self.client.post(url,post_data)
|
r = self.client.post(url,post_data)
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
q = PyQuery(r.content)
|
q = PyQuery(r.content)
|
||||||
self.assertEqual(len(q('#session-request-form')),1)
|
self.assertEqual(len(q('#session-request-form')),1)
|
||||||
self.assertContains(r, 'You must enter a length for all sessions')
|
self.assertContains(r, 'Must provide data for all sessions')
|
||||||
|
|
||||||
def test_submit_request_check_constraints(self):
|
def test_submit_request_check_constraints(self):
|
||||||
m1 = MeetingFactory(type_id='ietf', date=datetime.date.today() - datetime.timedelta(days=100))
|
m1 = MeetingFactory(type_id='ietf', date=datetime.date.today() - datetime.timedelta(days=100))
|
||||||
|
@ -363,7 +466,7 @@ class SubmitRequestCase(TestCase):
|
||||||
target=inactive_group,
|
target=inactive_group,
|
||||||
name_id='chair_conflict',
|
name_id='chair_conflict',
|
||||||
)
|
)
|
||||||
SessionFactory(group=group, meeting=m1)
|
session = SessionFactory(group=group, meeting=m1)
|
||||||
|
|
||||||
self.client.login(username="secretary", password="secretary+password")
|
self.client.login(username="secretary", password="secretary+password")
|
||||||
|
|
||||||
|
@ -375,11 +478,27 @@ class SubmitRequestCase(TestCase):
|
||||||
self.assertIn(still_active_group.acronym, conflict1)
|
self.assertIn(still_active_group.acronym, conflict1)
|
||||||
self.assertNotIn(inactive_group.acronym, conflict1)
|
self.assertNotIn(inactive_group.acronym, conflict1)
|
||||||
|
|
||||||
|
attendees = '10'
|
||||||
|
comments = 'need projector'
|
||||||
post_data = {'num_session':'1',
|
post_data = {'num_session':'1',
|
||||||
'length_session1':'3600',
|
'attendees':attendees,
|
||||||
'attendees':'10',
|
|
||||||
'constraint_chair_conflict': group.acronym,
|
'constraint_chair_conflict': group.acronym,
|
||||||
'comments':'need projector',
|
'comments':comments,
|
||||||
|
'session_set-TOTAL_FORMS': '1',
|
||||||
|
'session_set-INITIAL_FORMS': '1',
|
||||||
|
'session_set-MIN_NUM_FORMS': '1',
|
||||||
|
'session_set-MAX_NUM_FORMS': '3',
|
||||||
|
# no 'session_set-0-id' to create a new session
|
||||||
|
'session_set-0-name': '',
|
||||||
|
'session_set-0-short': '',
|
||||||
|
'session_set-0-purpose': session.purpose_id,
|
||||||
|
'session_set-0-type': session.type_id,
|
||||||
|
'session_set-0-requested_duration': '3600',
|
||||||
|
'session_set-0-on_agenda': session.on_agenda,
|
||||||
|
'session_set-0-remote_instructions': session.remote_instructions,
|
||||||
|
'session_set-0-attendees': attendees,
|
||||||
|
'session_set-0-comments': comments,
|
||||||
|
'session_set-0-DELETE': '',
|
||||||
'submit': 'Continue'}
|
'submit': 'Continue'}
|
||||||
r = self.client.post(url,post_data)
|
r = self.client.post(url,post_data)
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
|
@ -405,10 +524,9 @@ class SubmitRequestCase(TestCase):
|
||||||
url = reverse('ietf.secr.sreq.views.new',kwargs={'acronym':group.acronym})
|
url = reverse('ietf.secr.sreq.views.new',kwargs={'acronym':group.acronym})
|
||||||
confirm_url = reverse('ietf.secr.sreq.views.confirm',kwargs={'acronym':group.acronym})
|
confirm_url = reverse('ietf.secr.sreq.views.confirm',kwargs={'acronym':group.acronym})
|
||||||
len_before = len(outbox)
|
len_before = len(outbox)
|
||||||
|
attendees = '10'
|
||||||
post_data = {'num_session':'2',
|
post_data = {'num_session':'2',
|
||||||
'length_session1':'3600',
|
'attendees':attendees,
|
||||||
'length_session2':'3600',
|
|
||||||
'attendees':'10',
|
|
||||||
'bethere':str(ad.pk),
|
'bethere':str(ad.pk),
|
||||||
'constraint_chair_conflict':group4.acronym,
|
'constraint_chair_conflict':group4.acronym,
|
||||||
'comments':'',
|
'comments':'',
|
||||||
|
@ -418,6 +536,32 @@ class SubmitRequestCase(TestCase):
|
||||||
'joint_with_groups': group3.acronym,
|
'joint_with_groups': group3.acronym,
|
||||||
'joint_for_session': '2',
|
'joint_for_session': '2',
|
||||||
'timeranges': ['thursday-afternoon-early', 'thursday-afternoon-late'],
|
'timeranges': ['thursday-afternoon-early', 'thursday-afternoon-late'],
|
||||||
|
'session_set-TOTAL_FORMS': '2',
|
||||||
|
'session_set-INITIAL_FORMS': '0',
|
||||||
|
'session_set-MIN_NUM_FORMS': '1',
|
||||||
|
'session_set-MAX_NUM_FORMS': '3',
|
||||||
|
# no 'session_set-0-id' for new session
|
||||||
|
'session_set-0-name': '',
|
||||||
|
'session_set-0-short': '',
|
||||||
|
'session_set-0-purpose': 'regular',
|
||||||
|
'session_set-0-type': 'regular',
|
||||||
|
'session_set-0-requested_duration': '3600',
|
||||||
|
'session_set-0-on_agenda': True,
|
||||||
|
'session_set-0-remote_instructions': '',
|
||||||
|
'session_set-0-attendees': attendees,
|
||||||
|
'session_set-0-comments': '',
|
||||||
|
'session_set-0-DELETE': '',
|
||||||
|
# no 'session_set-1-id' for new session
|
||||||
|
'session_set-1-name': '',
|
||||||
|
'session_set-1-short': '',
|
||||||
|
'session_set-1-purpose': 'regular',
|
||||||
|
'session_set-1-type': 'regular',
|
||||||
|
'session_set-1-requested_duration': '3600',
|
||||||
|
'session_set-1-on_agenda': True,
|
||||||
|
'session_set-1-remote_instructions': '',
|
||||||
|
'session_set-1-attendees': attendees,
|
||||||
|
'session_set-1-comments': '',
|
||||||
|
'session_set-1-DELETE': '',
|
||||||
'submit': 'Continue'}
|
'submit': 'Continue'}
|
||||||
self.client.login(username="ameschairman", password="ameschairman+password")
|
self.client.login(username="ameschairman", password="ameschairman+password")
|
||||||
# submit
|
# submit
|
||||||
|
@ -541,23 +685,59 @@ class SessionFormTest(TestCase):
|
||||||
self.group5 = GroupFactory()
|
self.group5 = GroupFactory()
|
||||||
self.group6 = GroupFactory()
|
self.group6 = GroupFactory()
|
||||||
|
|
||||||
|
attendees = '10'
|
||||||
|
comments = 'need lights'
|
||||||
self.valid_form_data = {
|
self.valid_form_data = {
|
||||||
'num_session': '2',
|
'num_session': '2',
|
||||||
'third_session': 'true',
|
'third_session': 'true',
|
||||||
'length_session1': '3600',
|
'attendees': attendees,
|
||||||
'length_session2': '3600',
|
|
||||||
'length_session3': '3600',
|
|
||||||
'attendees': '10',
|
|
||||||
'constraint_chair_conflict': self.group2.acronym,
|
'constraint_chair_conflict': self.group2.acronym,
|
||||||
'constraint_tech_overlap': self.group3.acronym,
|
'constraint_tech_overlap': self.group3.acronym,
|
||||||
'constraint_key_participant': self.group4.acronym,
|
'constraint_key_participant': self.group4.acronym,
|
||||||
'comments': 'need lights',
|
'comments': comments,
|
||||||
'session_time_relation': 'subsequent-days',
|
'session_time_relation': 'subsequent-days',
|
||||||
'adjacent_with_wg': self.group5.acronym,
|
'adjacent_with_wg': self.group5.acronym,
|
||||||
'joint_with_groups': self.group6.acronym,
|
'joint_with_groups': self.group6.acronym,
|
||||||
'joint_for_session': '3',
|
'joint_for_session': '3',
|
||||||
'timeranges': ['thursday-afternoon-early', 'thursday-afternoon-late'],
|
'timeranges': ['thursday-afternoon-early', 'thursday-afternoon-late'],
|
||||||
'submit': 'Continue'
|
'submit': 'Continue',
|
||||||
|
'session_set-TOTAL_FORMS': '3',
|
||||||
|
'session_set-INITIAL_FORMS': '0',
|
||||||
|
'session_set-MIN_NUM_FORMS': '1',
|
||||||
|
'session_set-MAX_NUM_FORMS': '3',
|
||||||
|
# no 'session_set-0-id' for new session
|
||||||
|
'session_set-0-name': '',
|
||||||
|
'session_set-0-short': '',
|
||||||
|
'session_set-0-purpose': 'regular',
|
||||||
|
'session_set-0-type': 'regular',
|
||||||
|
'session_set-0-requested_duration': '3600',
|
||||||
|
'session_set-0-on_agenda': True,
|
||||||
|
'session_set-0-remote_instructions': '',
|
||||||
|
'session_set-0-attendees': attendees,
|
||||||
|
'session_set-0-comments': '',
|
||||||
|
'session_set-0-DELETE': '',
|
||||||
|
# no 'session_set-1-id' for new session
|
||||||
|
'session_set-1-name': '',
|
||||||
|
'session_set-1-short': '',
|
||||||
|
'session_set-1-purpose': 'regular',
|
||||||
|
'session_set-1-type': 'regular',
|
||||||
|
'session_set-1-requested_duration': '3600',
|
||||||
|
'session_set-1-on_agenda': True,
|
||||||
|
'session_set-1-remote_instructions': '',
|
||||||
|
'session_set-1-attendees': attendees,
|
||||||
|
'session_set-1-comments': '',
|
||||||
|
'session_set-1-DELETE': '',
|
||||||
|
# no 'session_set-2-id' for new session
|
||||||
|
'session_set-2-name': '',
|
||||||
|
'session_set-2-short': '',
|
||||||
|
'session_set-2-purpose': 'regular',
|
||||||
|
'session_set-2-type': 'regular',
|
||||||
|
'session_set-2-requested_duration': '3600',
|
||||||
|
'session_set-2-on_agenda': True,
|
||||||
|
'session_set-2-remote_instructions': '',
|
||||||
|
'session_set-2-attendees': attendees,
|
||||||
|
'session_set-2-comments': '',
|
||||||
|
'session_set-2-DELETE': '',
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_valid(self):
|
def test_valid(self):
|
||||||
|
@ -639,58 +819,65 @@ class SessionFormTest(TestCase):
|
||||||
def test_invalid_joint_for_session(self):
|
def test_invalid_joint_for_session(self):
|
||||||
form = self._invalid_test_helper({
|
form = self._invalid_test_helper({
|
||||||
'third_session': '',
|
'third_session': '',
|
||||||
|
'session_set-TOTAL_FORMS': '2',
|
||||||
'num_session': 2,
|
'num_session': 2,
|
||||||
'joint_for_session': '3',
|
'joint_for_session': '3',
|
||||||
})
|
})
|
||||||
self.assertEqual(form.errors,
|
self.assertEqual(form.errors,
|
||||||
{
|
{
|
||||||
'joint_for_session': ['The third session can not be the joint session, '
|
'joint_for_session': [
|
||||||
'because you have not requested a third session.']
|
'Session 3 can not be the joint session, the session has not been requested.']
|
||||||
})
|
})
|
||||||
|
|
||||||
form = self._invalid_test_helper({
|
form = self._invalid_test_helper({
|
||||||
'third_session': '',
|
'third_session': '',
|
||||||
'length_session2': '',
|
'session_set-TOTAL_FORMS': '1',
|
||||||
'num_session': 1,
|
'num_session': 1,
|
||||||
'joint_for_session': '2',
|
'joint_for_session': '2',
|
||||||
'session_time_relation': '',
|
'session_time_relation': '',
|
||||||
})
|
})
|
||||||
self.assertEqual(form.errors,
|
self.assertEqual(form.errors,
|
||||||
{
|
{
|
||||||
'joint_for_session': ['The second session can not be the joint session, '
|
'joint_for_session': [
|
||||||
'because you have not requested a second session.']
|
'Session 2 can not be the joint session, the session has not been requested.']
|
||||||
})
|
})
|
||||||
|
|
||||||
def test_invalid_missing_session_length(self):
|
def test_invalid_missing_session_length(self):
|
||||||
form = self._invalid_test_helper({
|
form = self._invalid_test_helper({
|
||||||
'length_session2': '',
|
'session_set-TOTAL_FORMS': '2',
|
||||||
|
'session_set-1-requested_duration': '',
|
||||||
'third_session': 'false',
|
'third_session': 'false',
|
||||||
'joint_for_session': None,
|
'joint_for_session': None,
|
||||||
})
|
})
|
||||||
self.assertEqual(form.errors,
|
self.assertEqual(form.session_forms.errors,
|
||||||
{
|
[
|
||||||
'length_session2': ['You must enter a length for all sessions'],
|
{},
|
||||||
})
|
{'requested_duration': ['This field is required.']},
|
||||||
|
])
|
||||||
|
|
||||||
form = self._invalid_test_helper({
|
form = self._invalid_test_helper({
|
||||||
'length_session2': '',
|
'session_set-1-requested_duration': '',
|
||||||
'length_session3': '',
|
'session_set-2-requested_duration': '',
|
||||||
'joint_for_session': None,
|
'joint_for_session': None,
|
||||||
})
|
})
|
||||||
self.assertEqual(form.errors,
|
self.assertEqual(
|
||||||
{
|
form.session_forms.errors,
|
||||||
'length_session2': ['You must enter a length for all sessions'],
|
[
|
||||||
'length_session3': ['You must enter a length for all sessions'],
|
{},
|
||||||
})
|
{'requested_duration': ['This field is required.']},
|
||||||
|
{'requested_duration': ['This field is required.']},
|
||||||
|
])
|
||||||
|
|
||||||
form = self._invalid_test_helper({
|
form = self._invalid_test_helper({
|
||||||
'length_session3': '',
|
'session_set-2-requested_duration': '',
|
||||||
'joint_for_session': None,
|
'joint_for_session': None,
|
||||||
})
|
})
|
||||||
self.assertEqual(form.errors,
|
self.assertEqual(form.session_forms.errors,
|
||||||
{
|
[
|
||||||
'length_session3': ['You must enter a length for all sessions'],
|
{},
|
||||||
})
|
{},
|
||||||
|
{'requested_duration': ['This field is required.']},
|
||||||
|
])
|
||||||
|
|
||||||
def _invalid_test_helper(self, new_form_data):
|
def _invalid_test_helper(self, new_form_data):
|
||||||
form_data = dict(self.valid_form_data, **new_form_data)
|
form_data = dict(self.valid_form_data, **new_form_data)
|
||||||
|
|
|
@ -60,17 +60,13 @@ def get_initial_session(sessions, prune_conflicts=False):
|
||||||
constraints = group.constraint_source_set.filter(meeting=meeting) # all constraints with this group as source
|
constraints = group.constraint_source_set.filter(meeting=meeting) # all constraints with this group as source
|
||||||
conflicts = constraints.filter(name__is_group_conflict=True) # only the group conflict constraints
|
conflicts = constraints.filter(name__is_group_conflict=True) # only the group conflict constraints
|
||||||
|
|
||||||
# even if there are three sessions requested, the old form has 2 in this field
|
if group.features.acts_like_wg:
|
||||||
initial['num_session'] = min(sessions.count(), 2)
|
# even if there are three sessions requested, the old form has 2 in this field
|
||||||
|
initial['num_session'] = min(sessions.count(), 2)
|
||||||
# accessing these foreign key fields throw errors if they are unset so we
|
initial['third_session'] = sessions.count() > 2
|
||||||
# need to catch these
|
else:
|
||||||
initial['length_session1'] = str(sessions[0].requested_duration.seconds)
|
initial['num_session'] = sessions.count()
|
||||||
try:
|
initial['third_session'] = False
|
||||||
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):
|
||||||
|
@ -268,6 +264,13 @@ def cancel(request, acronym):
|
||||||
messages.success(request, 'The %s Session Request has been cancelled' % group.acronym)
|
messages.success(request, 'The %s Session Request has been cancelled' % group.acronym)
|
||||||
return redirect('ietf.secr.sreq.views.main')
|
return redirect('ietf.secr.sreq.views.main')
|
||||||
|
|
||||||
|
|
||||||
|
def status_slug_for_new_session(session, session_number):
|
||||||
|
if session.group.features.acts_like_wg and session_number == 2:
|
||||||
|
return 'apprw'
|
||||||
|
return 'schedw'
|
||||||
|
|
||||||
|
|
||||||
@role_required(*AUTHORIZED_ROLES)
|
@role_required(*AUTHORIZED_ROLES)
|
||||||
def confirm(request, acronym):
|
def confirm(request, acronym):
|
||||||
'''
|
'''
|
||||||
|
@ -276,12 +279,14 @@ def confirm(request, acronym):
|
||||||
'''
|
'''
|
||||||
# FIXME: this should be using form.is_valid/form.cleaned_data - invalid input will make it crash
|
# FIXME: this should be using form.is_valid/form.cleaned_data - invalid input will make it crash
|
||||||
group = get_object_or_404(Group,acronym=acronym)
|
group = get_object_or_404(Group,acronym=acronym)
|
||||||
|
if len(group.features.session_purposes) == 0:
|
||||||
|
raise Http404(f'Cannot request sessions for group "{acronym}"')
|
||||||
meeting = get_meeting(days=14)
|
meeting = get_meeting(days=14)
|
||||||
FormClass = get_session_form_class()
|
FormClass = get_session_form_class()
|
||||||
|
|
||||||
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 +321,27 @@ 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
|
form.session_forms.save()
|
||||||
# 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
|
new_session = sess_form.instance
|
||||||
for duration in (form.data.get('length_session1',None),form.data.get('length_session2',None),form.data.get('length_session3',None)):
|
SchedulingEvent.objects.create(
|
||||||
count += 1
|
session=new_session,
|
||||||
if duration:
|
status=SessionStatusName.objects.get(slug=status_slug_for_new_session(new_session, count)),
|
||||||
slug = 'apprw' if count == 3 else 'schedw'
|
by=login,
|
||||||
new_session = Session.objects.create(
|
)
|
||||||
meeting=meeting,
|
if 'resources' in form.data:
|
||||||
group=group,
|
new_session.resources.set(session_data['resources'])
|
||||||
attendees=form.cleaned_data['attendees'],
|
jfs = form.data.get('joint_for_session', '-1')
|
||||||
requested_duration=datetime.timedelta(0,int(duration)),
|
if not jfs: # jfs might be ''
|
||||||
comments=form.cleaned_data['comments'],
|
jfs = '-1'
|
||||||
type_id='regular',
|
if int(jfs) == count + 1: # count is zero-indexed
|
||||||
)
|
groups_split = form.cleaned_data.get('joint_with_groups').replace(',',' ').split()
|
||||||
SchedulingEvent.objects.create(
|
joint = Group.objects.filter(acronym__in=groups_split)
|
||||||
session=new_session,
|
new_session.joint_with_groups.set(joint)
|
||||||
status=SessionStatusName.objects.get(slug=slug),
|
new_session.save()
|
||||||
by=login,
|
session_changed(new_session)
|
||||||
)
|
|
||||||
if 'resources' in form.data:
|
|
||||||
new_session.resources.set(session_data['resources'])
|
|
||||||
jfs = form.data.get('joint_for_session', '-1')
|
|
||||||
if not jfs: # jfs might be ''
|
|
||||||
jfs = '-1'
|
|
||||||
if int(jfs) == count:
|
|
||||||
groups_split = form.cleaned_data.get('joint_with_groups').replace(',',' ').split()
|
|
||||||
joint = Group.objects.filter(acronym__in=groups_split)
|
|
||||||
new_session.joint_with_groups.set(joint)
|
|
||||||
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,8 +412,13 @@ 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')
|
if len(group.features.session_purposes) == 0:
|
||||||
sessions_count = sessions.count()
|
raise Http404(f'Cannot request sessions for group "{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', 'deleted'])
|
||||||
|
).order_by('id')
|
||||||
initial = get_initial_session(sessions)
|
initial = get_initial_session(sessions)
|
||||||
FormClass = get_session_form_class()
|
FormClass = get_session_form_class()
|
||||||
|
|
||||||
|
@ -449,67 +448,18 @@ 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():
|
||||||
# might be cleaner to simply delete and rewrite all records (but maintain submitter?)
|
changed_session_forms = [sf for sf in form.session_forms.forms_to_keep if sf.has_changed()]
|
||||||
# adjust duration or add sessions
|
form.session_forms.save()
|
||||||
# session 1
|
for n, subform in enumerate(form.session_forms):
|
||||||
if 'length_session1' in form.changed_data:
|
session = subform.instance
|
||||||
session = sessions[0]
|
if session in form.session_forms.created_instances:
|
||||||
session.requested_duration = datetime.timedelta(0,int(form.cleaned_data['length_session1']))
|
|
||||||
session.save()
|
|
||||||
session_changed(session)
|
|
||||||
|
|
||||||
# session 2
|
|
||||||
if 'length_session2' in form.changed_data:
|
|
||||||
length_session2 = form.cleaned_data['length_session2']
|
|
||||||
if length_session2 == '':
|
|
||||||
sessions[1].delete()
|
|
||||||
elif sessions_count < 2:
|
|
||||||
duration = datetime.timedelta(0,int(form.cleaned_data['length_session2']))
|
|
||||||
new_session = Session.objects.create(
|
|
||||||
meeting=meeting,
|
|
||||||
group=group,
|
|
||||||
attendees=form.cleaned_data['attendees'],
|
|
||||||
requested_duration=duration,
|
|
||||||
comments=form.cleaned_data['comments'],
|
|
||||||
type_id='regular',
|
|
||||||
)
|
|
||||||
SchedulingEvent.objects.create(
|
SchedulingEvent.objects.create(
|
||||||
session=new_session,
|
session=session,
|
||||||
status=SessionStatusName.objects.get(slug='schedw'),
|
status_id=status_slug_for_new_session(session, n),
|
||||||
by=request.user.person,
|
by=request.user.person,
|
||||||
)
|
)
|
||||||
else:
|
for sf in changed_session_forms:
|
||||||
duration = datetime.timedelta(0,int(form.cleaned_data['length_session2']))
|
session_changed(sf.instance)
|
||||||
session = sessions[1]
|
|
||||||
session.requested_duration = duration
|
|
||||||
session.save()
|
|
||||||
|
|
||||||
# session 3
|
|
||||||
if 'length_session3' in form.changed_data:
|
|
||||||
length_session3 = form.cleaned_data['length_session3']
|
|
||||||
if length_session3 == '':
|
|
||||||
sessions[2].delete()
|
|
||||||
elif sessions_count < 3:
|
|
||||||
duration = datetime.timedelta(0,int(form.cleaned_data['length_session3']))
|
|
||||||
new_session = Session.objects.create(
|
|
||||||
meeting=meeting,
|
|
||||||
group=group,
|
|
||||||
attendees=form.cleaned_data['attendees'],
|
|
||||||
requested_duration=duration,
|
|
||||||
comments=form.cleaned_data['comments'],
|
|
||||||
type_id='regular',
|
|
||||||
)
|
|
||||||
SchedulingEvent.objects.create(
|
|
||||||
session=new_session,
|
|
||||||
status=SessionStatusName.objects.get(slug='apprw'),
|
|
||||||
by=request.user.person,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
duration = datetime.timedelta(0,int(form.cleaned_data['length_session3']))
|
|
||||||
session = sessions[2]
|
|
||||||
session.requested_duration = duration
|
|
||||||
session.save()
|
|
||||||
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(
|
||||||
|
@ -534,7 +484,8 @@ def edit(request, acronym, num=None):
|
||||||
session_changed(sessions[current_joint_for_session_idx])
|
session_changed(sessions[current_joint_for_session_idx])
|
||||||
sessions[new_joint_for_session_idx].joint_with_groups.set(new_joint_with_groups)
|
sessions[new_joint_for_session_idx].joint_with_groups.set(new_joint_with_groups)
|
||||||
session_changed(sessions[new_joint_for_session_idx])
|
session_changed(sessions[new_joint_for_session_idx])
|
||||||
|
|
||||||
|
# Update sessions to match changes to shared form fields
|
||||||
if 'attendees' in form.changed_data:
|
if 'attendees' in form.changed_data:
|
||||||
sessions.update(attendees=form.cleaned_data['attendees'])
|
sessions.update(attendees=form.cleaned_data['attendees'])
|
||||||
if 'comments' in form.changed_data:
|
if 'comments' in form.changed_data:
|
||||||
|
@ -588,10 +539,6 @@ def edit(request, acronym, num=None):
|
||||||
# send notification
|
# send notification
|
||||||
send_notification(group,meeting,login,form.cleaned_data,'update')
|
send_notification(group,meeting,login,form.cleaned_data,'update')
|
||||||
|
|
||||||
# nuke any cache that might be lingering around.
|
|
||||||
from ietf.meeting.helpers import session_constraint_expire
|
|
||||||
session_constraint_expire(request,session)
|
|
||||||
|
|
||||||
messages.success(request, 'Session Request updated')
|
messages.success(request, 'Session Request updated')
|
||||||
return redirect('ietf.secr.sreq.views.view', acronym=acronym)
|
return redirect('ietf.secr.sreq.views.view', acronym=acronym)
|
||||||
|
|
||||||
|
@ -666,7 +613,7 @@ def main(request):
|
||||||
|
|
||||||
# add session status messages for use in template
|
# add session status messages for use in template
|
||||||
for group in scheduled_groups:
|
for group in scheduled_groups:
|
||||||
if len(group.meeting_sessions) < 3:
|
if not group.features.acts_like_wg or (len(group.meeting_sessions) < 3):
|
||||||
group.status_message = group.meeting_sessions[0].current_status
|
group.status_message = group.meeting_sessions[0].current_status
|
||||||
else:
|
else:
|
||||||
group.status_message = 'First two sessions: %s, Third session: %s' % (group.meeting_sessions[0].current_status, group.meeting_sessions[2].current_status)
|
group.status_message = 'First two sessions: %s, Third session: %s' % (group.meeting_sessions[0].current_status, group.meeting_sessions[2].current_status)
|
||||||
|
@ -690,9 +637,11 @@ def new(request, acronym):
|
||||||
to create the request.
|
to create the request.
|
||||||
'''
|
'''
|
||||||
group = get_object_or_404(Group, acronym=acronym)
|
group = get_object_or_404(Group, acronym=acronym)
|
||||||
|
if len(group.features.session_purposes) == 0:
|
||||||
|
raise Http404(f'Cannot request sessions for group "{acronym}"')
|
||||||
meeting = get_meeting(days=14)
|
meeting = get_meeting(days=14)
|
||||||
session_conflicts = dict(inbound=inbound_session_conflicts_as_string(group, meeting))
|
session_conflicts = dict(inbound=inbound_session_conflicts_as_string(group, meeting))
|
||||||
is_virtual = meeting.number in settings.SECR_VIRTUAL_MEETINGS,
|
is_virtual = meeting.number in settings.SECR_VIRTUAL_MEETINGS
|
||||||
FormClass = get_session_form_class()
|
FormClass = get_session_form_class()
|
||||||
|
|
||||||
# check if app is locked
|
# check if app is locked
|
||||||
|
@ -770,6 +719,7 @@ def no_session(request, acronym):
|
||||||
meeting=meeting,
|
meeting=meeting,
|
||||||
requested_duration=datetime.timedelta(0),
|
requested_duration=datetime.timedelta(0),
|
||||||
type_id='regular',
|
type_id='regular',
|
||||||
|
purpose_id='regular',
|
||||||
)
|
)
|
||||||
SchedulingEvent.objects.create(
|
SchedulingEvent.objects.create(
|
||||||
session=session,
|
session=session,
|
||||||
|
@ -892,7 +842,8 @@ def view(request, acronym, num = None):
|
||||||
return render(request, 'sreq/view.html', {
|
return render(request, 'sreq/view.html', {
|
||||||
'is_locked': is_locked,
|
'is_locked': is_locked,
|
||||||
'is_virtual': meeting.number in settings.SECR_VIRTUAL_MEETINGS,
|
'is_virtual': meeting.number in settings.SECR_VIRTUAL_MEETINGS,
|
||||||
'session': session,
|
'session': session, # legacy processed data
|
||||||
|
'sessions': sessions, # actual session instances
|
||||||
'activities': activities,
|
'activities': activities,
|
||||||
'meeting': meeting,
|
'meeting': meeting,
|
||||||
'group': group,
|
'group': group,
|
||||||
|
|
28
ietf/secr/static/secr/js/session_form.js
Normal file
28
ietf/secr/static/secr/js/session_form.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
/* Copyright The IETF Trust 2021, All Rights Reserved
|
||||||
|
*
|
||||||
|
* JS support for the SessionForm
|
||||||
|
* */
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function track_common_input(input, name_suffix) {
|
||||||
|
const handler = function() {
|
||||||
|
const hidden_inputs = document.querySelectorAll(
|
||||||
|
'.session-details-form input[name$="-' + name_suffix + '"]'
|
||||||
|
);
|
||||||
|
for (let hi of hidden_inputs) {
|
||||||
|
hi.value = input.value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
input.addEventListener('change', handler);
|
||||||
|
handler();
|
||||||
|
}
|
||||||
|
|
||||||
|
function initialize() {
|
||||||
|
// Keep all the hidden inputs in sync with the main form
|
||||||
|
track_common_input(document.getElementById('id_attendees'), 'attendees');
|
||||||
|
track_common_input(document.getElementById('id_comments'), 'comments');
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('load', initialize);
|
||||||
|
})();
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ Session Requester: {{ login }}
|
||||||
{% if session.joint_with_groups %}{{ session.joint_for_session_display }} joint with: {{ session.joint_with_groups }}{% endif %}
|
{% if session.joint_with_groups %}{{ session.joint_for_session_display }} joint with: {{ session.joint_with_groups }}{% endif %}
|
||||||
|
|
||||||
Number of Sessions: {{ session.num_session }}
|
Number of Sessions: {{ session.num_session }}
|
||||||
Length of Session(s): {{ session.length_session1|display_duration }}{% if session.length_session2 %}, {{ session.length_session2|display_duration }}{% endif %}{% if session.length_session3 %}, {{ session.length_session3|display_duration }}{% endif %}
|
Length of Session(s): {{ session.length_session1|display_duration }}{% if session.length_session2 %}, {{ session.length_session2|display_duration }}{% endif %}{% if session.length_session3 %}, {{ session.length_session3|display_duration }}{% endif %}
|
||||||
Number of Attendees: {{ session.attendees }}
|
Number of Attendees: {{ session.attendees }}
|
||||||
Conflicts to Avoid:
|
Conflicts to Avoid:
|
||||||
{% for line in session.outbound_conflicts %} {{line}}
|
{% for line in session.outbound_conflicts %} {{line}}
|
||||||
|
|
|
@ -1,20 +1,28 @@
|
||||||
<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>
|
{% if group.features.acts_like_wg %}<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" %}
|
<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>
|
||||||
<tr class="bg2"><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 not group.features.acts_like_wg #}
|
||||||
|
{% for session_form in form.session_forms %}
|
||||||
|
<tr class="bg2" id="session_row_{{ forloop.counter0 }}"><td>Session {{ forloop.counter }}:<span class="required">*</span></td><td>{% include 'meeting/session_details_form.html' with form=session_form only %}</td></tr>
|
||||||
|
{% endfor %}
|
||||||
{% 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>
|
||||||
|
|
|
@ -3,16 +3,11 @@
|
||||||
<col width="200">
|
<col width="200">
|
||||||
<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.third_session %}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>
|
{% if form %}
|
||||||
{% if session.length_session2 %}
|
{% include 'includes/sessions_request_view_formset.html' with formset=form.session_forms group=group session=session only %}
|
||||||
<tr class="row2"><td>Length of Session 2:</td><td>{{ session.length_session2|display_duration }}</td></tr>
|
{% else %}
|
||||||
{% if not is_virtual %}
|
{% include 'includes/sessions_request_view_session_set.html' with session_set=sessions group=group session=session only %}
|
||||||
<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 %}
|
|
||||||
{% if session.length_session3 %}
|
|
||||||
<tr class="row2"><td>Length of Session 3:</td><td>{{ session.length_session3|display_duration }}</td></tr>
|
|
||||||
{% endif %}
|
{% 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">
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
{% load ams_filters %}{# keep this in sync with sessions_request_view_session_set.html #}
|
||||||
|
{% for sess_form in formset %}{% if sess_form.cleaned_data and not sess_form.cleaned_data.DELETE %}
|
||||||
|
<tr class="row2">
|
||||||
|
<td>Session {{ forloop.counter }}:</td>
|
||||||
|
<td>
|
||||||
|
<dl>
|
||||||
|
<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 != 'regular' %}
|
||||||
|
<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 group.features.acts_like_wg and 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>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}{% endfor %}
|
|
@ -0,0 +1,30 @@
|
||||||
|
{% load ams_filters %}{# keep this in sync with sessions_request_view_formset.html #}
|
||||||
|
{% for sess in session_set %}
|
||||||
|
<tr class="row2">
|
||||||
|
<td>Session {{ forloop.counter }}:</td>
|
||||||
|
<td>
|
||||||
|
<dl>
|
||||||
|
<dt>Length</dt>
|
||||||
|
<dd>{{ sess.requested_duration.total_seconds|display_duration }}</dd>
|
||||||
|
{% if sess.name %}
|
||||||
|
<dt>Name</dt>
|
||||||
|
<dd>{{ sess.name }}</dd>{% endif %}
|
||||||
|
{% if sess.purpose.slug != 'regular' %}
|
||||||
|
<dt>Purpose</dt>
|
||||||
|
<dd>
|
||||||
|
{{ sess.purpose }}
|
||||||
|
{% if sess.purpose.timeslot_types|length > 1 %}({{ sess.type }}
|
||||||
|
){% endif %}
|
||||||
|
</dd>
|
||||||
|
{% endif %}
|
||||||
|
</dl>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% if group.features.acts_like_wg and 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>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
|
@ -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 group.features.acts_like_wg and 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 = 112 # 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
|
||||||
|
|
|
@ -1239,6 +1239,9 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
|
||||||
.edit-meeting-schedule .session .session-label {
|
.edit-meeting-schedule .session .session-label {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
margin-left: 0.1em;
|
margin-left: 0.1em;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-meeting-schedule .session .session-label .bof-tag {
|
.edit-meeting-schedule .session .session-label .bof-tag {
|
||||||
|
@ -1335,7 +1338,8 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
|
||||||
margin: 0.5em 0;
|
margin: 0.5em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-meeting-schedule .scheduling-panel .preferences > span {
|
.edit-meeting-schedule .scheduling-panel .preferences > span {
|
||||||
|
margin-top: 0;
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1344,17 +1348,20 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-meeting-schedule #timeslot-group-toggles-modal .modal-body {
|
.edit-meeting-schedule #timeslot-group-toggles-modal .modal-body > div {
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
}
|
||||||
|
.edit-meeting-schedule #timeslot-group-toggles-modal .modal-body .individual-timeslots {
|
||||||
/*column-count: 3;*/
|
/*column-count: 3;*/
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: row wrap;
|
flex-flow: row wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-meeting-schedule #timeslot-group-toggles-modal .modal-body > * {
|
.edit-meeting-schedule #timeslot-group-toggles-modal .modal-body .individual-timeslots > * {
|
||||||
margin-right: 1.5em;
|
margin-right: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-meeting-schedule #timeslot-group-toggles-modal .modal-body label {
|
.edit-meeting-schedule #timeslot-group-toggles-modal .modal-body .individual-timeslots label {
|
||||||
display: block;
|
display: block;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
@ -1363,7 +1370,7 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-meeting-schedule .session-parent-toggles label {
|
.edit-meeting-schedule .toggle-inputs label {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
padding: 0 1em;
|
padding: 0 1em;
|
||||||
|
@ -1545,6 +1552,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;
|
||||||
|
|
|
@ -1,184 +0,0 @@
|
||||||
/*
|
|
||||||
*
|
|
||||||
* FILE: agenda_edit.js
|
|
||||||
* Copyright (c) 2013, The IETF Trust. See ../../../LICENSE.
|
|
||||||
*
|
|
||||||
* www.credil.org: Project Orlando 2013
|
|
||||||
* Author: Justin Hornosty ( justin@credil.org )
|
|
||||||
* Michael Richardson <mcr@sandelman.ca>
|
|
||||||
*
|
|
||||||
* Description:
|
|
||||||
* This is the main file for the agenda editing page.
|
|
||||||
* It contains the document read function that starts everything
|
|
||||||
* off, and uses functions and objects from agenda_*.js
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//////////////-GLOBALS----////////////////////////////////////////
|
|
||||||
|
|
||||||
// these need to be setup in landscape_edit's setup_slots() inline function:
|
|
||||||
//var meeting_number = 0; // is the meeting name.
|
|
||||||
//var schedule_id = 0; // what is the schedule we are editing.
|
|
||||||
//var schedule_name; // what is the schedule we are editing.
|
|
||||||
//var schedule_owner_href = ''; // who owns this schedule
|
|
||||||
//var assignments_post_href;
|
|
||||||
//var meeting_base_url;
|
|
||||||
//var site_base_url;
|
|
||||||
//var total_rooms = 0; // the number of rooms
|
|
||||||
//var total_days = 0; // the number of days
|
|
||||||
|
|
||||||
var is_secretariat = false;
|
|
||||||
|
|
||||||
var agenda_globals;
|
|
||||||
|
|
||||||
var area_directors = {}; // list of promises of area directors, index by href.
|
|
||||||
|
|
||||||
var read_only = true; // it is true until we learn otherwise.
|
|
||||||
var days = [];
|
|
||||||
var legend_status = {}; // agenda area colors.
|
|
||||||
var load_conflicts = true;
|
|
||||||
var duplicate_sessions = {};
|
|
||||||
/********* colors ************************************/
|
|
||||||
|
|
||||||
var dragging_color = "blue"; // color when draging events.
|
|
||||||
var none_color = ''; // when we reset the color. I believe doing '' will force it back to the stylesheet value.
|
|
||||||
var color_droppable_empty_slot = 'rgb(0, 102, 153)';
|
|
||||||
|
|
||||||
// these are used for debugging only.
|
|
||||||
var last_json_txt = ""; // last txt from a json call.
|
|
||||||
var last_json_reply = []; // last parsed content
|
|
||||||
|
|
||||||
var hidden_rooms = [];
|
|
||||||
var hidden_days = [];
|
|
||||||
|
|
||||||
/****************************************************/
|
|
||||||
|
|
||||||
/////////////-END-GLOBALS-///////////////////////////////////////
|
|
||||||
|
|
||||||
/* refactor this out into the html */
|
|
||||||
$(document).ready(function() {
|
|
||||||
initStuff();
|
|
||||||
|
|
||||||
$("#close_ietf_menubar").click();
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
/* initStuff()
|
|
||||||
This is ran at page load and sets up the entire page.
|
|
||||||
*/
|
|
||||||
function initStuff(){
|
|
||||||
agenda_globals = new AgendaGlobals();
|
|
||||||
//agenda_globals.__debug_session_move = true;
|
|
||||||
|
|
||||||
log("initstuff() running...");
|
|
||||||
var directorpromises = [];
|
|
||||||
|
|
||||||
/* define a slot for unscheduled items */
|
|
||||||
var unassigned = new ScheduledSlot();
|
|
||||||
unassigned.make_unassigned();
|
|
||||||
|
|
||||||
setup_slots(directorpromises);
|
|
||||||
mark_area_directors(directorpromises);
|
|
||||||
log("setup_slots() ran");
|
|
||||||
droppable();
|
|
||||||
log("droppable() ran");
|
|
||||||
|
|
||||||
$.when.apply($,directorpromises).done(function() {
|
|
||||||
/* can not load events until area director info,
|
|
||||||
timeslots, sessions, and assignments
|
|
||||||
have been loaded
|
|
||||||
*/
|
|
||||||
log("loading/linking objects");
|
|
||||||
load_events();
|
|
||||||
log("load_events() ran");
|
|
||||||
find_meeting_no_room();
|
|
||||||
calculate_name_select_box();
|
|
||||||
calculate_room_select_box();
|
|
||||||
listeners();
|
|
||||||
droppable();
|
|
||||||
duplicate_sessions = find_double_timeslots();
|
|
||||||
empty_info_table();
|
|
||||||
count_sessions();
|
|
||||||
|
|
||||||
if(load_conflicts) {
|
|
||||||
recalculate(null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
static_listeners();
|
|
||||||
log("listeners() ran");
|
|
||||||
|
|
||||||
start_spin();
|
|
||||||
|
|
||||||
read_only = true;
|
|
||||||
log("do read only check");
|
|
||||||
read_only_check();
|
|
||||||
stop_spin();
|
|
||||||
|
|
||||||
meeting_objs_length = Object.keys(agenda_globals.meeting_objs).length;
|
|
||||||
|
|
||||||
/* Comment this out for fast loading */
|
|
||||||
//load_conflicts = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var __READ_ONLY;
|
|
||||||
function read_only_result(msg) {
|
|
||||||
__READ_ONLY = msg;
|
|
||||||
is_secretariat = msg.secretariat;
|
|
||||||
|
|
||||||
read_only = msg.read_only;
|
|
||||||
console.log("read only", read_only);
|
|
||||||
|
|
||||||
if(!read_only) {
|
|
||||||
$("#read_only").css("display", "none");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(msg.save_perm) {
|
|
||||||
$(".agenda_save_box").css("display", "block");
|
|
||||||
if(read_only) {
|
|
||||||
$(".agenda_save_box").css("position", "fixed");
|
|
||||||
$(".agenda_save_box").css("top", "20px");
|
|
||||||
$(".agenda_save_box").css("right", "10px");
|
|
||||||
$(".agenda_save_box").css("bottom", "auto");
|
|
||||||
$(".agenda_save_box").css("border", "3px solid blue");
|
|
||||||
$(".agenda_save_box").css("z-index", "2000");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$(".agenda_save_box").html("please login to save");
|
|
||||||
}
|
|
||||||
|
|
||||||
schedule_owner_href = msg.owner_href;
|
|
||||||
// XX go fetch the owner and display it.
|
|
||||||
console.log("owner href:", schedule_owner_href);
|
|
||||||
|
|
||||||
$("#pageloaded").show();
|
|
||||||
|
|
||||||
listeners();
|
|
||||||
droppable();
|
|
||||||
}
|
|
||||||
|
|
||||||
function read_only_check() {
|
|
||||||
var read_only_url = meeting_base_url + "/agenda/" + schedule_owner_email + "/" + schedule_name + "/permissions";
|
|
||||||
console.log("Loading readonly status from: ", read_only_url);
|
|
||||||
var read_only_load = $.ajax(read_only_url);
|
|
||||||
|
|
||||||
read_only_load.success(function(newobj, status, jqXHR) {
|
|
||||||
last_json_reply = newobj;
|
|
||||||
read_only_result(newobj);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function print_all_ss(objs){
|
|
||||||
console.log(objs)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Local Variables:
|
|
||||||
* c-basic-offset:4
|
|
||||||
* End:
|
|
||||||
*/
|
|
||||||
|
|
|
@ -1,626 +0,0 @@
|
||||||
/*
|
|
||||||
* agenda_helpers.js
|
|
||||||
*
|
|
||||||
* Copyright (c) 2013, The IETF Trust. See ../../../LICENSE.
|
|
||||||
*
|
|
||||||
* www.credil.org: Project Orlando 2013
|
|
||||||
* Author: Justin Hornosty ( justin@credil.org )
|
|
||||||
* Michael Richardson <mcr@sandelman.ca>
|
|
||||||
*
|
|
||||||
* Should contain miscellaneous commonly used functions.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* do_work:
|
|
||||||
when we are waiting for something to either resolve to true, or another similar job
|
|
||||||
this function should achieve this.
|
|
||||||
|
|
||||||
result will be a function that when returns true will stop the work and then the callback
|
|
||||||
will be triggered.
|
|
||||||
|
|
||||||
ex:
|
|
||||||
global_x = 0
|
|
||||||
do_work(function(){ global_x++; return global_x > 100 }, function(){ console.log("resolved") })
|
|
||||||
*/
|
|
||||||
function do_work(result,callback){
|
|
||||||
setTimeout(function(){
|
|
||||||
if(!result()){
|
|
||||||
setTimeout(arguments.callee,1);
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function log(text){
|
|
||||||
console.log(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
function print_all(){
|
|
||||||
console.log("all");
|
|
||||||
console.log(agenda_globals.meeting_objs.length);
|
|
||||||
for(var i=0; i<agenda_globals.meeting_objs.length; i++){
|
|
||||||
agenda_globals.meeting_objs[i].print_out();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function find_title(title){
|
|
||||||
$.each(agenda_globals.meeting_objs, function(key){
|
|
||||||
if (agenda_globals.meeting_objs[key].title == title) {
|
|
||||||
console.log(agenda_globals.meeting_objs[key]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function find_session_id(session_id){
|
|
||||||
$.each(agenda_globals.meeting_objs, function(key){
|
|
||||||
if (agenda_globals.meeting_objs[key].session_id == session_id) {
|
|
||||||
console.log(agenda_globals.meeting_objs[key]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function find_same_area(area){
|
|
||||||
var areas = []
|
|
||||||
area = area.toUpperCase();
|
|
||||||
$.each(agenda_globals.meeting_objs, function(index,obj){
|
|
||||||
if(obj.area == area){
|
|
||||||
areas.push({id:index,slot_status_key:obj.slot_status_key})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return areas
|
|
||||||
}
|
|
||||||
|
|
||||||
function style_empty_slots(){
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
var __debug_load_events = false;
|
|
||||||
/* this pushes every event into the agendas */
|
|
||||||
function load_events(){
|
|
||||||
var slot_id;
|
|
||||||
|
|
||||||
console.log("load events...");
|
|
||||||
|
|
||||||
/* first delete all html items that might have gotten saved if
|
|
||||||
* user save-as and went offline.
|
|
||||||
*/
|
|
||||||
if(__debug_load_events) {
|
|
||||||
console.log("processing double slot status relations");
|
|
||||||
}
|
|
||||||
|
|
||||||
/* clear out all the timeslots */
|
|
||||||
$.each(agenda_globals.timeslot_bydomid, function(key) {
|
|
||||||
insert_cell(key, "", true);
|
|
||||||
|
|
||||||
var timeslot = agenda_globals.timeslot_bydomid[key];
|
|
||||||
slot_id = ("#"+key);
|
|
||||||
|
|
||||||
$(slot_id).addClass("agenda_slot_" + timeslot.roomtype);
|
|
||||||
|
|
||||||
if(timeslot.roomtype == "unavail") {
|
|
||||||
$(slot_id).removeClass("ui-droppable");
|
|
||||||
$(slot_id).removeClass("free_slot");
|
|
||||||
$(slot_id).addClass("agenda_slot_unavailable");
|
|
||||||
} else {
|
|
||||||
$(slot_id).removeClass("agenda_slot_unavailable");
|
|
||||||
$(slot_id).addClass("ui-droppable");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$.each(agenda_globals.slot_status, function(key) {
|
|
||||||
ssid_arr = agenda_globals.slot_status[key];
|
|
||||||
|
|
||||||
for(var q = 0; q<ssid_arr.length; q++){
|
|
||||||
ssid = ssid_arr[q];
|
|
||||||
|
|
||||||
ssid.connect_to_timeslot_session();
|
|
||||||
|
|
||||||
// also see if the slots have any declared relationship, and take it forward as
|
|
||||||
// well as backwards.
|
|
||||||
if(ssid.extendedfrom_id != false) {
|
|
||||||
other = agenda_globals.slot_objs[ssid.extendedfrom_id];
|
|
||||||
if(__debug_load_events) {
|
|
||||||
console.log("slot:",ssid.assignment_id, "extended from: ",key,ssid.extendedfrom_id); // ," is: ", other);
|
|
||||||
}
|
|
||||||
if(other != undefined) {
|
|
||||||
ssid.extendedfrom = other;
|
|
||||||
other.extendedto = ssid;
|
|
||||||
} else {
|
|
||||||
if(__debug_load_events) {
|
|
||||||
console.log("extended from: ",ssid.extendedfrom_id," not found");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// go through the slots again, and if one slot has been extended, then
|
|
||||||
// extend any other "sister" slots as well.
|
|
||||||
if(__debug_load_events) {
|
|
||||||
console.log("marking extended slots for slots with multiple sessions");
|
|
||||||
}
|
|
||||||
$.each(agenda_globals.slot_status, function(key) {
|
|
||||||
ssid_arr = agenda_globals.slot_status[key];
|
|
||||||
|
|
||||||
var extendedto = undefined;
|
|
||||||
for(var q = 0; q<ssid_arr.length; q++){
|
|
||||||
ssid = ssid_arr[q];
|
|
||||||
if(extendedto == undefined &&
|
|
||||||
ssid.extendedto != undefined) {
|
|
||||||
if(__debug_load_events) {
|
|
||||||
console.log("ssid",ssid.session_id,"extended 1");
|
|
||||||
}
|
|
||||||
extendedto = ssid.extendedto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for(var q = 0; q<ssid_arr.length; q++){
|
|
||||||
ssid = ssid_arr[q];
|
|
||||||
ssid.extendedto = extendedto;
|
|
||||||
if(__debug_load_events) {
|
|
||||||
console.log("ssid",ssid.session_id,"extended 2");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if(__debug_load_events) {
|
|
||||||
console.log("finding responsible ad");
|
|
||||||
}
|
|
||||||
$.each(agenda_globals.meeting_objs, function(key) {
|
|
||||||
session = agenda_globals.meeting_objs[key];
|
|
||||||
session.find_responsible_ad();
|
|
||||||
});
|
|
||||||
|
|
||||||
$.each(agenda_globals.slot_status, function(key) {
|
|
||||||
ssid_arr = agenda_globals.slot_status[key]
|
|
||||||
if(key == "sortable-list"){
|
|
||||||
console.log("sortable list");
|
|
||||||
}else {
|
|
||||||
for(var q = 0; q<ssid_arr.length; q++){
|
|
||||||
ssid = ssid_arr[q];
|
|
||||||
slot_id = ("#"+ssid.domid());
|
|
||||||
|
|
||||||
if(__debug_load_events) {
|
|
||||||
console.log("populating slot: ",slot_id,key);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(ssid.timeslot.roomtype != "unavail") {
|
|
||||||
session = agenda_globals.meeting_objs[ssid.session_id];
|
|
||||||
if (session != null) {
|
|
||||||
if(ssid.extendedto != undefined) {
|
|
||||||
session.double_wide = true;
|
|
||||||
session.slot2 = ssid.extendedto;
|
|
||||||
}
|
|
||||||
if(ssid.extendedfrom == undefined) {
|
|
||||||
session.slot_status_key = key;
|
|
||||||
}
|
|
||||||
|
|
||||||
$(slot_id).removeClass('free_slot');
|
|
||||||
|
|
||||||
if(ssid.extendedfrom == undefined) {
|
|
||||||
if(__debug_load_events) {
|
|
||||||
console.log(" with session", session.title);
|
|
||||||
}
|
|
||||||
|
|
||||||
session.populate_event(key);
|
|
||||||
}
|
|
||||||
session.placed(ssid.timeslot, false, ssid);
|
|
||||||
} else {
|
|
||||||
$(slot_id).addClass('free_slot');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$.each(agenda_globals.meeting_objs, function(key) {
|
|
||||||
session = agenda_globals.meeting_objs[key];
|
|
||||||
|
|
||||||
// note in the group, what the set of column classes is.
|
|
||||||
// this is an array, as the group might have multiple
|
|
||||||
// sessions!
|
|
||||||
group = session.group;
|
|
||||||
if(group == undefined) {
|
|
||||||
console.log("session: ", session.title, "has no group_href:", session.group_href);
|
|
||||||
} else {
|
|
||||||
group.add_column_classes(session.column_class_list);
|
|
||||||
group.add_session(session);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function check_free(inp){
|
|
||||||
var empty = false;
|
|
||||||
slot = agenda_globals.timeslot_bydomid[inp.id];
|
|
||||||
if(slot == null){
|
|
||||||
//console.log("\t from check_free, slot is null?", inp,inp.id, agenda_globals.slot_status[inp.id]);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (slot.empty == false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* clears any background highlight colors of scheduled sessions */
|
|
||||||
function clear_highlight(inp_arr){ // @args: array from slot_status{}
|
|
||||||
if(inp_arr == null){
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for(var i =0; i<inp_arr.length; i++){
|
|
||||||
$("#session_"+inp_arr[i].session_id).removeClass('free_slot');
|
|
||||||
$("#session_"+inp_arr[i].session_id).css('background-color','');
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/* based on any meeting object, it finds any other objects inside the same timeslot. */
|
|
||||||
function find_friends(inp){
|
|
||||||
var ts = $(inp).parent().attr('id');
|
|
||||||
var ss_arr = agenda_globals.slot_status[ts];
|
|
||||||
if (ss_arr != null){
|
|
||||||
return ss_arr;
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
//console.log("find_friends("+inp+") did not find anything");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function json_to_id(j){
|
|
||||||
return (j.room()+"_"+j.date()+"_"+j.time());
|
|
||||||
}
|
|
||||||
|
|
||||||
function id_to_json(id){
|
|
||||||
if(id != null){
|
|
||||||
var split = id.split('_');
|
|
||||||
return {"room":split[0],"date":split[1],"time":split[2]}
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* returns a the html for a row in a table
|
|
||||||
as: <tr><td>title</td><td>data</td></tr>
|
|
||||||
*/
|
|
||||||
function gen_tr_td(title,data){
|
|
||||||
return "<tr><td>"+title+"</td><td>"+data+"</td></tr>";
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mainly for the case where we didn't get any data back from the server */
|
|
||||||
function empty_info_table(){
|
|
||||||
$("#info_grp").html(name_select_html);
|
|
||||||
$("#info_name").html("");
|
|
||||||
$("#info_area").html("");
|
|
||||||
$("#info_duration").html("");
|
|
||||||
|
|
||||||
$(".agenda_selected_buttons").attr('disabled',true);
|
|
||||||
$(".agenda_double_slot").addClass("button_disabled");
|
|
||||||
$(".agenda_double_slot").removeClass("button_enabled");
|
|
||||||
|
|
||||||
if(!read_only) {
|
|
||||||
$("#info_location").html(generate_select_box()+"<button id='info_location_set'>Set</button>");
|
|
||||||
$("#info_location_select").val("");
|
|
||||||
$("#info_location_select").val($("#info_location_select_option_"+current_timeslot_id).val());
|
|
||||||
}
|
|
||||||
$("#info_responsible").html("");
|
|
||||||
$("#info_requestedby").html("");
|
|
||||||
$("#agenda_requested_features").html("");
|
|
||||||
|
|
||||||
/* need to reset listeners, because we just changed the HTML */
|
|
||||||
listeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var temp_1;
|
|
||||||
/* creates the 'info' table that is located on the right side.
|
|
||||||
takes in a json.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function compare_timeslot(a,b) {
|
|
||||||
//console.log("day: a,b", a.day, b.day);
|
|
||||||
|
|
||||||
// sometimes (a.day==b.say)==false and (a.day===b.day)==false,
|
|
||||||
// for days that appear identical, but built from different strings,
|
|
||||||
// yet (a.day-b.day)==0.
|
|
||||||
if((a.day - b.day) == 0) {
|
|
||||||
//console.log("time: a,b", a.starttime, b.starttime);
|
|
||||||
if(a.starttime == b.starttime) {
|
|
||||||
//console.log("room: a,b", a.room, b.room, a.room < b.room);
|
|
||||||
if(a.room > b.room) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if(a.starttime > b.starttime) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(a.day > b.day) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var room_select_html = "";
|
|
||||||
function calculate_room_select_box() {
|
|
||||||
var html = "<select id='info_location_select'>";
|
|
||||||
var mobj_array = [];
|
|
||||||
|
|
||||||
$.each(agenda_globals.timeslot_byid, function(key, value){
|
|
||||||
mobj_array.push(value)
|
|
||||||
});
|
|
||||||
|
|
||||||
var sorted = mobj_array.sort(compare_timeslot);
|
|
||||||
var lastone_id = undefined;
|
|
||||||
|
|
||||||
$.each(sorted, function(index, value) {
|
|
||||||
// this check removes duplicates from the list, if there are any.
|
|
||||||
if(value.roomtype == "break" || value.roomtype=="reg") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(value.timeslot_id == lastone_id) {
|
|
||||||
return; // from subfunction.
|
|
||||||
}
|
|
||||||
//console.log("room_select_html", index, value, value.short_string);
|
|
||||||
html=html+"<option value='"+value.timeslot_id;
|
|
||||||
html=html+"' id='info_location_select_option_";
|
|
||||||
html=html+value.timeslot_id+"'>";
|
|
||||||
html=html+value.short_string;
|
|
||||||
if(value.roomtype != "regular") {
|
|
||||||
html = html+ "(" + value.roomtype + ")";
|
|
||||||
}
|
|
||||||
html=html+"</option>";
|
|
||||||
lastone_id = value.timeslot_id;
|
|
||||||
});
|
|
||||||
html = html+"</select>";
|
|
||||||
room_select_html = html;
|
|
||||||
return room_select_html;
|
|
||||||
}
|
|
||||||
|
|
||||||
var name_select_html = undefined;
|
|
||||||
var temp_sorted = null;
|
|
||||||
function calculate_name_select_box(){
|
|
||||||
var html = "<select id='info_name_select'>";
|
|
||||||
var mobj_array = [];
|
|
||||||
var mobj_array2;
|
|
||||||
$.each(agenda_globals.meeting_objs, function(key, value){ mobj_array.push(value) });
|
|
||||||
mobj_array2 = mobj_array.sort(function(a,b) { return a.title.localeCompare(b.title); });
|
|
||||||
|
|
||||||
var mlen = mobj_array.length;
|
|
||||||
console.log("calculate name_select box with",mlen,"objects");
|
|
||||||
for(var i = 0; i < mlen; i++){
|
|
||||||
//console.log("select box mobj["+i+"]="+mobj_array[i]);
|
|
||||||
// html=html+"<option value='"+mobj_array[i].slot_status_key;
|
|
||||||
html=html+"<option value='"+mobj_array[i].session_id;
|
|
||||||
html=html+"' id='info_name_select_option_";
|
|
||||||
ts_id = "err";
|
|
||||||
//console.log(mobj_array[i].session_id);
|
|
||||||
try{
|
|
||||||
ts_id = mobj_array[i].session_id;
|
|
||||||
}catch(err){
|
|
||||||
console.log(err); // bucket list items.
|
|
||||||
|
|
||||||
}
|
|
||||||
html=html+ts_id+"'>";
|
|
||||||
|
|
||||||
|
|
||||||
try{
|
|
||||||
html=html+mobj_array[i].title; // + " (" + mobj_array[i].description + ")";
|
|
||||||
} catch(err) {
|
|
||||||
html=html+"ERRROR!!!";
|
|
||||||
}
|
|
||||||
html=html+"</option>";
|
|
||||||
}
|
|
||||||
|
|
||||||
html = html+"</select>";
|
|
||||||
name_select_html = html;
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function generate_select_box(){
|
|
||||||
if(!room_select_html) {
|
|
||||||
calculate_name_select_box();
|
|
||||||
}
|
|
||||||
return room_select_html;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function insert_cell(js_room_id, text, replace){
|
|
||||||
slot_id = ("#"+js_room_id);
|
|
||||||
try{
|
|
||||||
var found;
|
|
||||||
if(replace) {
|
|
||||||
found = $(slot_id).html(text);
|
|
||||||
} else {
|
|
||||||
found = $(slot_id).append($(text));
|
|
||||||
|
|
||||||
}
|
|
||||||
$(slot_id).css('background','');
|
|
||||||
$(slot_id).removeClass('free_slot');
|
|
||||||
if(found.length == 0){
|
|
||||||
// do something here, if length was zero... then?
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
catch(err){
|
|
||||||
log("error");
|
|
||||||
log(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function find_meeting_no_room(){
|
|
||||||
$.each(agenda_globals.meeting_objs, function(key){
|
|
||||||
if(agenda_globals.meeting_objs[key].slot_status_key == null) {
|
|
||||||
session = agenda_globals.meeting_objs[key]
|
|
||||||
session.slot_status_key = null;
|
|
||||||
session.populate_event(bucketlist_id);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* in some cases we have sessions that span over two timeslots.
|
|
||||||
so we end up with two slot_status pointing to the same meeting_obj.
|
|
||||||
this this occures when someone requests a session that is extra long
|
|
||||||
which will then fill up the next timeslot.
|
|
||||||
|
|
||||||
this functions finds those cases.
|
|
||||||
|
|
||||||
returns a json{ 'ts': arr[time_slot_ids] }
|
|
||||||
|
|
||||||
*/
|
|
||||||
function find_double_timeslots(){
|
|
||||||
var duplicate = {};
|
|
||||||
|
|
||||||
$.each(agenda_globals.slot_status, function(key){
|
|
||||||
for(var i =0; i<agenda_globals.slot_status[key].length; i++){
|
|
||||||
// goes threw all the slots
|
|
||||||
var ss_id = agenda_globals.slot_status[key][i].session_id;
|
|
||||||
if(duplicate[ss_id]){
|
|
||||||
duplicate[ss_id]['count']++;
|
|
||||||
duplicate[ss_id]['ts'].push(key);
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
duplicate[ss_id] = {'count': 1, 'ts':[key]};
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var dup = {};
|
|
||||||
// console.log(duplicate);
|
|
||||||
$.each(duplicate, function(key){
|
|
||||||
if(duplicate[key]['count'] > 1){
|
|
||||||
dup[key] = duplicate[key]['ts'];
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return dup;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var child = null;
|
|
||||||
/* removes a duplicate timeslot. completely. it's gone. */
|
|
||||||
function remove_duplicate(timeslot_id, ss_id){
|
|
||||||
children = $("#"+timeslot_id).children();
|
|
||||||
child = children;
|
|
||||||
for(var i = 0; i< children.length; i++){ // loop to
|
|
||||||
if($(children[i]).attr('session_id') == ss_id) { // make sure we only remove duplicate.
|
|
||||||
try{
|
|
||||||
$(children[i]).remove();
|
|
||||||
}catch(exception){
|
|
||||||
console.log("exception from remove_duplicate",exception);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function auto_remove(){
|
|
||||||
dup = find_double_timeslots();
|
|
||||||
$.each(dup, function(key){
|
|
||||||
remove_duplicate(dup[key][1], key);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* for the spinnner */
|
|
||||||
|
|
||||||
/* spinner code from:
|
|
||||||
http://fgnass.github.com/spin.js/
|
|
||||||
|
|
||||||
ex: $("#spinner").spin() < start the spin
|
|
||||||
$("#spinner").spin(false) < stop the spin
|
|
||||||
|
|
||||||
http://gist.github.com/itsflorida < jquery functionality.
|
|
||||||
|
|
||||||
lines: 30, // The number of lines to draw
|
|
||||||
length: 7, // The length of each line
|
|
||||||
width: 1, // The line thickness
|
|
||||||
radius: 20, // The radius of the inner circle
|
|
||||||
corners: 1, // Corner roundness (0..1)
|
|
||||||
rotate: 0, // The rotation offset
|
|
||||||
color: '#000', // #rgb or #rrggbb
|
|
||||||
speed: 1, // Rounds per second
|
|
||||||
trail: 60, // Afterglow percentage
|
|
||||||
shadow: false, // Whether to render a shadow
|
|
||||||
hwaccel: true, // Whether to use hardware acceleration
|
|
||||||
className: 'spinner', // The CSS class to assign to the spinner
|
|
||||||
zIndex: 2e9, // The zindex (defaults to 2000000000)
|
|
||||||
top: 'auto', // Top position relative to parent in px
|
|
||||||
left: 'auto' // Left position relative to parent in px
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
(function($) {
|
|
||||||
$.fn.spin = function(opts, color) {
|
|
||||||
if (Spinner) {
|
|
||||||
return this.each(function() {
|
|
||||||
var $this = $(this),
|
|
||||||
data = $this.data();
|
|
||||||
|
|
||||||
if (data.spinner) {
|
|
||||||
data.spinner.stop();
|
|
||||||
delete data.spinner;
|
|
||||||
}
|
|
||||||
if (opts !== false) {
|
|
||||||
if (typeof opts === "string") {
|
|
||||||
if (opts in presets) {
|
|
||||||
opts = presets[opts];
|
|
||||||
} else {
|
|
||||||
opts = {};
|
|
||||||
}
|
|
||||||
if (color) {
|
|
||||||
opts.color = color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data.spinner = new Spinner($.extend({color: $this.css('color')}, opts)).spin(this);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw "Spinner class not available.";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})(jQuery);
|
|
||||||
|
|
||||||
|
|
||||||
function start_spin(opts){
|
|
||||||
//spinner
|
|
||||||
// $("#schedule_name").hide();
|
|
||||||
$("#spinner").show();
|
|
||||||
$("#spinner").spin({lines:16, radius:8, length:16, width:4});
|
|
||||||
$("#pageloaded").hide();
|
|
||||||
}
|
|
||||||
function stop_spin(){
|
|
||||||
//spinner
|
|
||||||
$("#schedule_name").show();
|
|
||||||
$("#spinner").hide();
|
|
||||||
$("#spinner").spin(false);
|
|
||||||
$("#pageloaded").show();
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Local Variables:
|
|
||||||
* c-basic-offset:4
|
|
||||||
* End:
|
|
||||||
*/
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -82,6 +82,7 @@ jQuery(document).ready(function () {
|
||||||
jQuery(element).addClass("selected");
|
jQuery(element).addClass("selected");
|
||||||
|
|
||||||
showConstraintHints(element);
|
showConstraintHints(element);
|
||||||
|
showTimeSlotTypeIndicators(element.dataset.type);
|
||||||
|
|
||||||
let sessionInfoContainer = content.find(".scheduling-panel .session-info-container");
|
let sessionInfoContainer = content.find(".scheduling-panel .session-info-container");
|
||||||
sessionInfoContainer.html(jQuery(element).find(".session-info").html());
|
sessionInfoContainer.html(jQuery(element).find(".session-info").html());
|
||||||
|
@ -105,6 +106,7 @@ jQuery(document).ready(function () {
|
||||||
else {
|
else {
|
||||||
sessions.removeClass("selected");
|
sessions.removeClass("selected");
|
||||||
showConstraintHints();
|
showConstraintHints();
|
||||||
|
resetTimeSlotTypeIndicators();
|
||||||
content.find(".scheduling-panel .session-info-container").html("");
|
content.find(".scheduling-panel .session-info-container").html("");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -203,6 +205,23 @@ jQuery(document).ready(function () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove timeslot classes indicating timeslot type disagreement
|
||||||
|
*/
|
||||||
|
function resetTimeSlotTypeIndicators() {
|
||||||
|
timeslots.removeClass('wrong-timeslot-type');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add timeslot classes indicating timeslot type disagreement
|
||||||
|
*
|
||||||
|
* @param timeslot_type
|
||||||
|
*/
|
||||||
|
function showTimeSlotTypeIndicators(timeslot_type) {
|
||||||
|
timeslots.removeClass('wrong-timeslot-type');
|
||||||
|
timeslots.filter('[data-type!="' + timeslot_type + '"]').addClass('wrong-timeslot-type');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Should this timeslot be treated as a future timeslot?
|
* Should this timeslot be treated as a future timeslot?
|
||||||
*
|
*
|
||||||
|
@ -277,19 +296,42 @@ jQuery(document).ready(function () {
|
||||||
return Boolean(event.originalEvent.dataTransfer.getData(dnd_mime_type));
|
return Boolean(event.originalEvent.dataTransfer.getData(dnd_mime_type));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the session element being dragged
|
||||||
|
*
|
||||||
|
* @param event drag-related event
|
||||||
|
*/
|
||||||
|
function getDraggedSession(event) {
|
||||||
|
if (!isSessionDragEvent(event)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const sessionId = event.originalEvent.dataTransfer.getData(dnd_mime_type);
|
||||||
|
const sessionElements = sessions.filter("#" + sessionId);
|
||||||
|
if (sessionElements.length > 0) {
|
||||||
|
return sessionElements[0];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Can a session be dropped in this element?
|
* Can a session be dropped in this element?
|
||||||
*
|
*
|
||||||
* Drop is allowed in drop-zones that are in unassigned-session or timeslot containers
|
* Drop is allowed in drop-zones that are in unassigned-session or timeslot containers
|
||||||
* not marked as 'past'.
|
* not marked as 'past'.
|
||||||
*/
|
*/
|
||||||
function sessionDropAllowed(elt) {
|
function sessionDropAllowed(dropElement, sessionElement) {
|
||||||
if (!officialSchedule) {
|
const relevant_parent = dropElement.closest('.timeslot, .unassigned-sessions');
|
||||||
return true;
|
if (!relevant_parent || !sessionElement) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const relevant_parent = elt.closest('.timeslot, .unassigned-sessions');
|
if (officialSchedule && relevant_parent.classList.contains('past')) {
|
||||||
return relevant_parent && !(relevant_parent.classList.contains('past'));
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !relevant_parent.dataset.type || (
|
||||||
|
relevant_parent.dataset.type === sessionElement.dataset.type
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!content.find(".edit-grid").hasClass("read-only")) {
|
if (!content.find(".edit-grid").hasClass("read-only")) {
|
||||||
|
@ -314,7 +356,7 @@ jQuery(document).ready(function () {
|
||||||
// dropping
|
// dropping
|
||||||
let dropElements = content.find(".timeslot .drop-target,.unassigned-sessions .drop-target");
|
let dropElements = content.find(".timeslot .drop-target,.unassigned-sessions .drop-target");
|
||||||
dropElements.on('dragenter', function (event) {
|
dropElements.on('dragenter', function (event) {
|
||||||
if (sessionDropAllowed(this)) {
|
if (sessionDropAllowed(this, getDraggedSession(event))) {
|
||||||
event.preventDefault(); // default action is signalling that this is not a valid target
|
event.preventDefault(); // default action is signalling that this is not a valid target
|
||||||
jQuery(this).parent().addClass("dropping");
|
jQuery(this).parent().addClass("dropping");
|
||||||
}
|
}
|
||||||
|
@ -324,7 +366,7 @@ jQuery(document).ready(function () {
|
||||||
// we don't actually need this event, except we need to signal
|
// we don't actually need this event, except we need to signal
|
||||||
// that this is a valid drop target, by cancelling the default
|
// that this is a valid drop target, by cancelling the default
|
||||||
// action
|
// action
|
||||||
if (sessionDropAllowed(this)) {
|
if (sessionDropAllowed(this, getDraggedSession(event))) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -332,7 +374,7 @@ jQuery(document).ready(function () {
|
||||||
dropElements.on('dragleave', function (event) {
|
dropElements.on('dragleave', function (event) {
|
||||||
// skip dragleave events if they are to children
|
// skip dragleave events if they are to children
|
||||||
const leaving_child = event.originalEvent.currentTarget.contains(event.originalEvent.relatedTarget);
|
const leaving_child = event.originalEvent.currentTarget.contains(event.originalEvent.relatedTarget);
|
||||||
if (!leaving_child && sessionDropAllowed(this)) {
|
if (!leaving_child && sessionDropAllowed(this, getDraggedSession(event))) {
|
||||||
jQuery(this).parent().removeClass('dropping');
|
jQuery(this).parent().removeClass('dropping');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -340,30 +382,21 @@ jQuery(document).ready(function () {
|
||||||
dropElements.on('drop', function (event) {
|
dropElements.on('drop', function (event) {
|
||||||
let dropElement = jQuery(this);
|
let dropElement = jQuery(this);
|
||||||
|
|
||||||
if (!isSessionDragEvent(event)) {
|
const sessionElement = getDraggedSession(event);
|
||||||
// event is result of something other than a session drag
|
if (!sessionElement) {
|
||||||
|
// not drag event or not from a session we recognize
|
||||||
dropElement.parent().removeClass("dropping");
|
dropElement.parent().removeClass("dropping");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionId = event.originalEvent.dataTransfer.getData(dnd_mime_type);
|
if (!sessionDropAllowed(this, sessionElement)) {
|
||||||
let sessionElement = sessions.filter("#" + sessionId);
|
|
||||||
if (sessionElement.length === 0) {
|
|
||||||
// drag event is not from a session we recognize
|
|
||||||
dropElement.parent().removeClass("dropping");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We now know this is a drop of a recognized session
|
|
||||||
|
|
||||||
if (!sessionDropAllowed(this)) {
|
|
||||||
dropElement.parent().removeClass("dropping"); // just in case
|
dropElement.parent().removeClass("dropping"); // just in case
|
||||||
return; // drop not allowed
|
return; // drop not allowed
|
||||||
}
|
}
|
||||||
|
|
||||||
event.preventDefault(); // prevent opening as link
|
event.preventDefault(); // prevent opening as link
|
||||||
|
|
||||||
let dragParent = sessionElement.parent();
|
let dragParent = jQuery(sessionElement).parent();
|
||||||
if (dragParent.is(this)) {
|
if (dragParent.is(this)) {
|
||||||
dropElement.parent().removeClass("dropping");
|
dropElement.parent().removeClass("dropping");
|
||||||
return;
|
return;
|
||||||
|
@ -400,7 +433,7 @@ jQuery(document).ready(function () {
|
||||||
timeout: 5 * 1000,
|
timeout: 5 * 1000,
|
||||||
data: {
|
data: {
|
||||||
action: "unassign",
|
action: "unassign",
|
||||||
session: sessionId.slice("session".length)
|
session: sessionElement.id.slice("session".length)
|
||||||
}
|
}
|
||||||
}).fail(failHandler).done(done);
|
}).fail(failHandler).done(done);
|
||||||
}
|
}
|
||||||
|
@ -410,7 +443,7 @@ jQuery(document).ready(function () {
|
||||||
method: "post",
|
method: "post",
|
||||||
data: {
|
data: {
|
||||||
action: "assign",
|
action: "assign",
|
||||||
session: sessionId.slice("session".length),
|
session: sessionElement.id.slice("session".length),
|
||||||
timeslot: dropParent.attr("id").slice("timeslot".length)
|
timeslot: dropParent.attr("id").slice("timeslot".length)
|
||||||
},
|
},
|
||||||
timeout: 5 * 1000
|
timeout: 5 * 1000
|
||||||
|
@ -673,7 +706,7 @@ jQuery(document).ready(function () {
|
||||||
// toggling visible sessions by session parents
|
// toggling visible sessions by session parents
|
||||||
let sessionParentInputs = content.find(".session-parent-toggles input");
|
let sessionParentInputs = content.find(".session-parent-toggles input");
|
||||||
|
|
||||||
function setSessionHidden(sess, hide) {
|
function setSessionHiddenParent(sess, hide) {
|
||||||
sess.toggleClass('hidden-parent', hide);
|
sess.toggleClass('hidden-parent', hide);
|
||||||
sess.prop('draggable', !hide);
|
sess.prop('draggable', !hide);
|
||||||
}
|
}
|
||||||
|
@ -684,18 +717,76 @@ jQuery(document).ready(function () {
|
||||||
checked.push(".parent-" + this.value);
|
checked.push(".parent-" + this.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
setSessionHidden(sessions.not(".untoggleable").filter(checked.join(",")), false);
|
setSessionHiddenParent(sessions.not(".untoggleable-by-parent").filter(checked.join(",")), false);
|
||||||
setSessionHidden(sessions.not(".untoggleable").not(checked.join(",")), true);
|
setSessionHiddenParent(sessions.not(".untoggleable-by-parent").not(checked.join(",")), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionParentInputs.on("click", updateSessionParentToggling);
|
sessionParentInputs.on("click", updateSessionParentToggling);
|
||||||
updateSessionParentToggling();
|
updateSessionParentToggling();
|
||||||
|
|
||||||
// toggling visible timeslots
|
// Toggling timeslot types
|
||||||
let timeslotGroupInputs = content.find("#timeslot-group-toggles-modal .modal-body input");
|
let timeSlotTypeInputs = content.find('.timeslot-type-toggles input');
|
||||||
function updateTimeslotGroupToggling() {
|
function updateTimeSlotTypeToggling() {
|
||||||
let checked = [];
|
let checked = [];
|
||||||
timeslotGroupInputs.filter(":checked").each(function () {
|
timeSlotTypeInputs.filter(":checked").each(function () {
|
||||||
|
checked.push("[data-type=" + this.value + "]");
|
||||||
|
});
|
||||||
|
|
||||||
|
sessions.filter(checked.join(",")).removeClass('hidden-timeslot-type');
|
||||||
|
sessions.not(checked.join(",")).addClass('hidden-timeslot-type');
|
||||||
|
timeslots.filter(checked.join(",")).removeClass('hidden-timeslot-type');
|
||||||
|
timeslots.not(checked.join(",")).addClass('hidden-timeslot-type');
|
||||||
|
}
|
||||||
|
if (timeSlotTypeInputs.length > 0) {
|
||||||
|
timeSlotTypeInputs.on("change", updateTimeSlotTypeToggling);
|
||||||
|
updateTimeSlotTypeToggling();
|
||||||
|
content.find('#timeslot-group-toggles-modal .timeslot-type-toggles .select-all').get(0).addEventListener(
|
||||||
|
'click',
|
||||||
|
function() {
|
||||||
|
timeSlotTypeInputs.prop('checked', true);
|
||||||
|
updateTimeSlotTypeToggling();
|
||||||
|
});
|
||||||
|
content.find('#timeslot-group-toggles-modal .timeslot-type-toggles .clear-all').get(0).addEventListener(
|
||||||
|
'click',
|
||||||
|
function() {
|
||||||
|
timeSlotTypeInputs.prop('checked', false);
|
||||||
|
updateTimeSlotTypeToggling();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggling session purposes
|
||||||
|
let sessionPurposeInputs = content.find('.session-purpose-toggles input');
|
||||||
|
function updateSessionPurposeToggling(evt) {
|
||||||
|
let checked = [];
|
||||||
|
sessionPurposeInputs.filter(":checked").each(function () {
|
||||||
|
checked.push(".purpose-" + this.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
sessions.filter(checked.join(",")).removeClass('hidden-purpose');
|
||||||
|
sessions.not(checked.join(",")).addClass('hidden-purpose');
|
||||||
|
}
|
||||||
|
if (sessionPurposeInputs.length > 0) {
|
||||||
|
sessionPurposeInputs.on("change", updateSessionPurposeToggling);
|
||||||
|
updateSessionPurposeToggling();
|
||||||
|
content.find('#session-toggles-modal .select-all').get(0).addEventListener(
|
||||||
|
'click',
|
||||||
|
function() {
|
||||||
|
sessionPurposeInputs.prop('checked', true);
|
||||||
|
updateSessionPurposeToggling();
|
||||||
|
});
|
||||||
|
content.find('#session-toggles-modal .clear-all').get(0).addEventListener(
|
||||||
|
'click',
|
||||||
|
function() {
|
||||||
|
sessionPurposeInputs.prop('checked', false);
|
||||||
|
updateSessionPurposeToggling();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// toggling visible timeslots
|
||||||
|
let timeSlotGroupInputs = content.find("#timeslot-group-toggles-modal .modal-body .individual-timeslots input");
|
||||||
|
function updateTimeSlotGroupToggling() {
|
||||||
|
let checked = [];
|
||||||
|
timeSlotGroupInputs.filter(":checked").each(function () {
|
||||||
checked.push("." + this.value);
|
checked.push("." + this.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -707,8 +798,21 @@ jQuery(document).ready(function () {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
timeslotGroupInputs.on("click change", updateTimeslotGroupToggling);
|
timeSlotGroupInputs.on("click change", updateTimeSlotGroupToggling);
|
||||||
updateTimeslotGroupToggling();
|
content.find('#timeslot-group-toggles-modal .timeslot-group-buttons .select-all').get(0).addEventListener(
|
||||||
|
'click',
|
||||||
|
function() {
|
||||||
|
timeSlotGroupInputs.prop('checked', true);
|
||||||
|
updateTimeSlotGroupToggling();
|
||||||
|
});
|
||||||
|
content.find('#timeslot-group-toggles-modal .timeslot-group-buttons .clear-all').get(0).addEventListener(
|
||||||
|
'click',
|
||||||
|
function() {
|
||||||
|
timeSlotGroupInputs.prop('checked', false);
|
||||||
|
updateTimeSlotGroupToggling();
|
||||||
|
});
|
||||||
|
|
||||||
|
updateTimeSlotGroupToggling();
|
||||||
updatePastTimeslots();
|
updatePastTimeslots();
|
||||||
setInterval(updatePastTimeslots, 10 * 1000 /* ms */);
|
setInterval(updatePastTimeslots, 10 * 1000 /* ms */);
|
||||||
|
|
||||||
|
|
43
ietf/static/ietf/js/meeting/create_timeslot.js
Normal file
43
ietf/static/ietf/js/meeting/create_timeslot.js
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
// Copyright The IETF Trust 2021, All Rights Reserved
|
||||||
|
/* global URLSearchParams */
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function initialize() {
|
||||||
|
const form = document.getElementById('timeslot-form');
|
||||||
|
if (!form) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams(document.location.search);
|
||||||
|
const day = params.get('day');
|
||||||
|
const date = params.get('date');
|
||||||
|
const location = params.get('location');
|
||||||
|
const time = params.get('time');
|
||||||
|
const duration = params.get('duration');
|
||||||
|
|
||||||
|
if (day) {
|
||||||
|
const inp = form.querySelector('#id_days input[value="' + day +'"]');
|
||||||
|
if (inp) {
|
||||||
|
inp.checked = true;
|
||||||
|
} else if (date) {
|
||||||
|
const date_field = form.querySelector('#id_other_date');
|
||||||
|
date_field.value = date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (location) {
|
||||||
|
const inp = form.querySelector('#id_locations input[value="' + location + '"]');
|
||||||
|
inp.checked=true;
|
||||||
|
}
|
||||||
|
if (time) {
|
||||||
|
const inp = form.querySelector('input#id_time');
|
||||||
|
inp.value = time;
|
||||||
|
}
|
||||||
|
if (duration) {
|
||||||
|
const inp = form.querySelector('input#id_duration');
|
||||||
|
inp.value = duration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('load', initialize);
|
||||||
|
})();
|
115
ietf/static/ietf/js/meeting/session_details_form.js
Normal file
115
ietf/static/ietf/js/meeting/session_details_form.js
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
/* 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 (row) {
|
||||||
|
if (purpose === 'regular') {
|
||||||
|
row.setAttribute('hidden', 'hidden');
|
||||||
|
} else {
|
||||||
|
row.removeAttribute('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Factory for event handler with a closure */
|
||||||
|
function purpose_change_handler(name_elt, type_elt, type_options, allowed_types) {
|
||||||
|
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 ? (form.dataset.prefix + '-') : '');
|
||||||
|
const purpose_elt = document.getElementById(id_prefix + 'purpose');
|
||||||
|
if (purpose_elt.type === 'hidden') {
|
||||||
|
return; // element is hidden, so nothing to do
|
||||||
|
}
|
||||||
|
const name_elt = document.getElementById(id_prefix + 'name');
|
||||||
|
const type_elt = document.getElementById(id_prefix + 'type');
|
||||||
|
const type_options = type_elt.getElementsByTagName('option');
|
||||||
|
const allowed_types = (type_elt.dataset.allowedOptions) ?
|
||||||
|
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,124 +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">
|
|
||||||
<div class="hidden-xs">
|
<div class="hidden-xs">
|
||||||
{% include "meeting/timeslot_start_end.html" %}
|
{% include "meeting/timeslot_start_end.html" %}
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</td>
|
||||||
<th colspan="4">
|
<td colspan="3">
|
||||||
<div class="hidden-sm hidden-md hidden-lg">
|
<div class="hidden-sm hidden-md hidden-lg">
|
||||||
{% include "meeting/timeslot_start_end.html" %}
|
{% include "meeting/timeslot_start_end.html" %}
|
||||||
</div>
|
</div>
|
||||||
{{ 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 %}
|
<div 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:',' }}"
|
</div>
|
||||||
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">
|
|
||||||
<div class="hidden-xs">
|
|
||||||
{% include "meeting/timeslot_start_end.html" %}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td colspan="3">
|
|
||||||
<div class="hidden-sm hidden-md hidden-lg">
|
|
||||||
{% include "meeting/timeslot_start_end.html" %}
|
|
||||||
</div>
|
|
||||||
{% if item.timeslot.show_location and item.timeslot.get_html_location %}
|
|
||||||
{% if schedule.meeting.number|add:"0" < 96 %}
|
|
||||||
{% comment %}<a href="https://tools.ietf.org/agenda/{{schedule.meeting.number}}/venue/?room={{ item.timeslot.get_html_location|xslugify }}">{% endcomment %}
|
|
||||||
{{item.timeslot.get_html_location}}
|
|
||||||
}
|
|
||||||
{% comment %}</a>{% endcomment %}
|
|
||||||
{% 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 %}
|
|
||||||
<div 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>
|
|
||||||
</divn>
|
|
||||||
{% 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">
|
||||||
|
<div class="hidden-xs">
|
||||||
|
{% include "meeting/timeslot_start_end.html" %}
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th colspan="4">
|
||||||
|
<div class="hidden-sm hidden-md hidden-lg">
|
||||||
|
{% include "meeting/timeslot_start_end.html" %}
|
||||||
|
</div>
|
||||||
|
{{ 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">
|
||||||
<div class="hidden-xs">
|
<div class="hidden-xs">
|
||||||
{% include "meeting/timeslot_start_end.html" %}
|
{% include "meeting/timeslot_start_end.html" %}
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<td colspan="3">
|
<td colspan="3">
|
||||||
<div class="hidden-sm hidden-md hidden-lg">
|
<div class="hidden-sm hidden-md hidden-lg">
|
||||||
{% include "meeting/timeslot_start_end.html" %}
|
{% include "meeting/timeslot_start_end.html" %}
|
||||||
</div>
|
</div>
|
||||||
{% 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 }}
|
||||||
{% comment %}<a href="https://tools.ietf.org/agenda/{{schedule.meeting.number}}/venue/?room={{ item.timeslot.get_html_location|xslugify }}">{% endcomment %}
|
{% end_location_anchor %}
|
||||||
{{item.timeslot.get_html_location}}
|
</td>
|
||||||
{% comment %}</a>{% endcomment %}
|
|
||||||
{% 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>
|
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<td>
|
<td>
|
||||||
|
@ -274,17 +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 }}
|
||||||
{% comment %}<a href="https://tools.ietf.org/agenda/{{schedule.meeting.number}}/venue/?room={{ item.timeslot.get_html_location|xslugify }}">{% endcomment %}
|
{% end_location_anchor %}
|
||||||
{{item.timeslot.get_html_location}}
|
|
||||||
{% comment %}</a>{% endcomment %}
|
|
||||||
{% 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><div class="hidden-xs">{{item.session.historic_group.historic_parent.acronym}}</div></td>
|
<td><div class="hidden-xs">{{item.session.historic_group.historic_parent.acronym}}</div></td>
|
||||||
|
@ -299,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 %}
|
||||||
|
@ -460,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>
|
||||||
|
|
28
ietf/templates/meeting/create_timeslot.html
Executable file
28
ietf/templates/meeting/create_timeslot.html
Executable file
|
@ -0,0 +1,28 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{# Copyright The IETF Trust 2021, All Rights Reserved #}
|
||||||
|
{% load origin static %}
|
||||||
|
{% 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 id="timeslot-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 %}
|
||||||
|
<script src="{% static 'ietf/js/meeting/create_timeslot.js' %}"></script>
|
||||||
|
{{ form.media.js }}
|
||||||
|
{% endblock %}
|
|
@ -16,6 +16,15 @@
|
||||||
.edit-meeting-schedule .edit-grid .timeslot.past-hint { filter: brightness(0.9); }
|
.edit-meeting-schedule .edit-grid .timeslot.past-hint { filter: brightness(0.9); }
|
||||||
.edit-meeting-schedule .past-flag { visibility: hidden; font-size: smaller; }
|
.edit-meeting-schedule .past-flag { visibility: hidden; font-size: smaller; }
|
||||||
.edit-meeting-schedule .edit-grid .timeslot.past .past-flag { visibility: visible; color: #aaaaaa; }
|
.edit-meeting-schedule .edit-grid .timeslot.past .past-flag { visibility: visible; color: #aaaaaa; }
|
||||||
|
{# style off-agenda sessions to indicate this #}
|
||||||
|
.edit-meeting-schedule .session.off-agenda { filter: brightness(0.9); }
|
||||||
|
{# type and purpose styling #}
|
||||||
|
.edit-meeting-schedule .edit-grid .timeslot.wrong-timeslot-type,
|
||||||
|
.edit-meeting-schedule .edit-grid .timeslot.hidden-timeslot-type { background-color: transparent; ); }
|
||||||
|
.edit-meeting-schedule .edit-grid .timeslot.wrong-timeslot-type .time-label,
|
||||||
|
.edit-meeting-schedule .edit-grid .timeslot.hidden-timeslot-type .time-label { color: transparent; ); }
|
||||||
|
.edit-meeting-schedule .session.hidden-purpose,
|
||||||
|
.edit-meeting-schedule .session.hidden-timeslot-type { filter: blur(3px); }
|
||||||
{% endblock morecss %}
|
{% endblock morecss %}
|
||||||
|
|
||||||
{% block title %}{{ schedule.name }}: IETF {{ meeting.number }} meeting agenda{% endblock %}
|
{% block title %}{{ schedule.name }}: IETF {{ meeting.number }} meeting agenda{% endblock %}
|
||||||
|
@ -40,7 +49,7 @@
|
||||||
·
|
·
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<a href="{% url "ietf.meeting.views.new_meeting_schedule" num=meeting.number owner=schedule.owner_email name=schedule.name %}">New agenda</a>
|
<a href="{% url "ietf.meeting.views.new_meeting_schedule" num=meeting.number owner=schedule.owner_email name=schedule.name %}">Copy agenda</a>
|
||||||
·
|
·
|
||||||
|
|
||||||
<a href="{% url "ietf.meeting.views.list_schedules" num=meeting.number %}">Other Agendas</a>
|
<a href="{% url "ietf.meeting.views.list_schedules" num=meeting.number %}">Other Agendas</a>
|
||||||
|
@ -133,6 +142,7 @@
|
||||||
data-end="{{ t.utc_end_time.isoformat }}"
|
data-end="{{ t.utc_end_time.isoformat }}"
|
||||||
data-duration="{{ t.duration.total_seconds }}"
|
data-duration="{{ t.duration.total_seconds }}"
|
||||||
data-scheduledatlabel="{{ t.time|date:"l G:i" }}-{{ t.end_time|date:"G:i" }}"
|
data-scheduledatlabel="{{ t.time|date:"l G:i" }}-{{ t.end_time|date:"G:i" }}"
|
||||||
|
data-type="{{ t.type.slug }}"
|
||||||
style="width: {{ t.layout_width }}rem;">
|
style="width: {{ t.layout_width }}rem;">
|
||||||
<div class="time-label">
|
<div class="time-label">
|
||||||
<div class="past-flag"> {# blank div keeps time centered vertically #}</div>
|
<div class="past-flag"> {# blank div keeps time centered vertically #}</div>
|
||||||
|
@ -177,16 +187,17 @@
|
||||||
</select>
|
</select>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="session-parent-toggles">
|
<span class="toggle-inputs session-parent-toggles">
|
||||||
Show:
|
Show:
|
||||||
{% for p in session_parents %}
|
{% for p in session_parents %}
|
||||||
<label class="parent-{{ p.acronym }}"><input type="checkbox" checked value="{{ p.acronym }}"> {{ p.acronym }}</label>
|
<label class="parent-{{ p.acronym }}"><input type="checkbox" checked value="{{ p.acronym }}"> {{ p.acronym }}</label>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="timeslot-group-toggles">
|
{% if session_purposes|length > 1 %}
|
||||||
<button class="btn btn-default" data-toggle="modal" data-target="#timeslot-group-toggles-modal"><input type="checkbox" checked="checked" disabled> Timeslots</button>
|
<button id="session-toggle-modal-open" class="btn btn-default" data-toggle="modal" data-target="#session-toggles-modal"><input type="checkbox" checked="checked" disabled> Sessions</button>
|
||||||
</span>
|
{% endif %}
|
||||||
|
<button id="timeslot-toggle-modal-open" class="btn btn-default" data-toggle="modal" data-target="#timeslot-group-toggles-modal"><input type="checkbox" checked="checked" disabled> Timeslots</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -205,14 +216,59 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
{% for day, t_groups in timeslot_groups %}
|
<div class="timeslot-group-buttons">
|
||||||
<div>
|
<button type="button" class="btn btn-default select-all">Select all times</button>
|
||||||
<div><strong>{{ day|date:"M. d" }}</strong></div>
|
<button type="button" class="btn btn-default clear-all">Clear times</button>
|
||||||
{% for start, end, key in t_groups %}
|
</div>
|
||||||
<label><input type="checkbox" name="timeslot-group" value="{{ key }}" checked="checked"> {{ start|date:"H:i" }} - {{ end|date:"H:i" }}</label>
|
<div class="individual-timeslots">
|
||||||
|
|
||||||
|
{% for day, t_groups in timeslot_groups %}
|
||||||
|
<div>
|
||||||
|
<div><strong>{{ day|date:"M. d" }}</strong></div>
|
||||||
|
{% for start, end, key in t_groups %}
|
||||||
|
<label><input type="checkbox" name="timeslot-group" value="{{ key }}" checked="checked"> {{ start|date:"H:i" }} - {{ end|date:"H:i" }}</label>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="timeslots-by-type timeslot-type-toggles">
|
||||||
|
Type:
|
||||||
|
{% for type in timeslot_types %}
|
||||||
|
<label class="timeslot-type-{{ type.slug }}"><input type="checkbox" checked value="{{ type.slug }}"> {{ type }}</label>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
<button type="button" class="btn btn-default select-all">Select all types</button>
|
||||||
{% endfor %}
|
<button type="button" class="btn btn-default clear-all">Clear types</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-primary" data-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="session-toggles-modal" class="modal" role="dialog" aria-labelledby="session-toggles-modal-title">
|
||||||
|
<div class="modal-dialog modal-lg" 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" id="session-toggles-modal-title">Displayed sessions</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="session-purpose-toggles">
|
||||||
|
{% for purpose in session_purposes %}
|
||||||
|
<div>
|
||||||
|
<label class="purpose-{{ purpose.slug }}"><input type="checkbox" checked value="{% firstof purpose.slug 'none' %}"> {{ purpose }}</label>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<button type="button" class="btn btn-default select-all">Select all</button>
|
||||||
|
<button type="button" class="btn btn-default clear-all">Clear all</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
<div id="session{{ session.pk }}" class="session {% if not session.group.parent.scheduling_color %}untoggleable{% endif %} {% if session.parent_acronym %}parent-{{ session.parent_acronym }}{% endif %} {% if session.readonly %}readonly{% endif %}" style="width:{{ session.layout_width }}em;" data-duration="{{ session.requested_duration.total_seconds }}" {% if session.attendees != None %}data-attendees="{{ session.attendees }}"{% endif %}>
|
<div id="session{{ session.pk }}"
|
||||||
|
class="session {% if not session.group.parent.scheduling_color %}untoggleable-by-parent{% endif %} {% if session.parent_acronym %}parent-{{ session.parent_acronym }}{% endif %} purpose-{{ session.purpose.slug }} {% if session.readonly %}readonly{% endif %} {% if not session.on_agenda %}off-agenda{% endif %}"
|
||||||
|
style="width:{{ session.layout_width }}em;"
|
||||||
|
data-duration="{{ session.requested_duration.total_seconds }}" {% if session.attendees != None %}
|
||||||
|
data-attendees="{{ session.attendees }}"{% endif %}
|
||||||
|
data-type="{{ session.type.slug }}">
|
||||||
<div class="session-label {% if session.group and session.group.is_bof %}bof-session{% endif %}">
|
<div class="session-label {% if session.group and session.group.is_bof %}bof-session{% endif %}">
|
||||||
{{ session.scheduling_label }}
|
{{ session.scheduling_label }}
|
||||||
{% if session.group and session.group.is_bof %}<span class="bof-tag">BOF</span>{% endif %}
|
{% if session.group and session.group.is_bof %}<span class="bof-tag">BOF</span>{% endif %}
|
||||||
|
@ -30,14 +35,9 @@
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<strong>
|
<strong>
|
||||||
<span class="time pull-right"></span>
|
<span class="time pull-right"></span>
|
||||||
{{ session.scheduling_label }}
|
{{ session.scheduling_label }} · {{ session.requested_duration_in_hours }}h
|
||||||
· {{ session.requested_duration_in_hours }}h
|
{% if session.purpose_label %} · {{ session.purpose_label }} {% endif %}
|
||||||
{% if session.group %}
|
{% if session.attendees != None %} · {{ session.attendees }} <i class="fa fa-user-o"></i> {% endif %}
|
||||||
· {% if session.group.is_bof %}BOF{% else %}{{ session.group.type.name }}{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% if session.attendees != None %}
|
|
||||||
· {{ session.attendees }} <i class="fa fa-user-o"></i>
|
|
||||||
{% endif %}
|
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -47,6 +47,7 @@
|
||||||
{% if session.group.parent %}
|
{% if session.group.parent %}
|
||||||
· <span class="session-parent">{{ session.group.parent.acronym }}</span>
|
· <span class="session-parent">{{ session.group.parent.acronym }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if not session.on_agenda %}· <i>off agenda</i>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -84,5 +85,7 @@
|
||||||
{% for s in session.other_sessions %}
|
{% for s in session.other_sessions %}
|
||||||
<div class="other-session" data-othersessionid="{{ s.pk }}"><i class="fa fa-calendar"></i> Other session <span class="time" data-scheduled="scheduled: {time}" data-notscheduled="not yet scheduled"></span></div>
|
<div class="other-session" data-othersessionid="{{ s.pk }}"><i class="fa fa-calendar"></i> Other session <span class="time" data-scheduled="scheduled: {time}" data-notscheduled="not yet scheduled"></span></div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
<a href="{% url 'ietf.meeting.views.edit_session' session_id=session.pk %}">Edit session</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
27
ietf/templates/meeting/edit_session.html
Normal file
27
ietf/templates/meeting/edit_session.html
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{# Copyright The IETF Trust 2021, All Rights Reserved #}
|
||||||
|
{% load origin %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
|
||||||
|
{% block pagehead %}
|
||||||
|
{{ form.media.css }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block title %}Edit session "{{ session }}"{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% origin %}
|
||||||
|
<h1>Edit session "{{ session }}"</h1>
|
||||||
|
<form class="session-details-form" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% bootstrap_form form %}
|
||||||
|
{% buttons %}
|
||||||
|
<button type="submit" class="btn btn-primary">Save</button>
|
||||||
|
<a class="btn btn-default" href="{% url 'ietf.meeting.views.edit_meeting_schedule' num=session.meeting.number %}">Cancel</a>
|
||||||
|
{% endbuttons %}
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
{{ form.media.js }}
|
||||||
|
{% endblock %}
|
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,7 +16,7 @@
|
||||||
{% 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://notes.ietf.org/notes-ietf-{{ meeting.number }}-plenary" title="Notepad for note-takers"><span class="fa fa-fw fa-edit"></span></a>
|
<a class="" href="https://notes.ietf.org/notes-ietf-{{ meeting.number }}-plenary" title="Notepad for note-takers"><span class="fa fa-fw fa-edit"></span></a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a class="" href="https://notes.ietf.org/notes-ietf-{{ meeting.number }}-{{acronym}}" title="Notepad for note-takers"><span class="fa fa-fw fa-edit"></span></a>
|
<a class="" href="https://notes.ietf.org/notes-ietf-{{ meeting.number }}-{{acronym}}" title="Notepad for note-takers"><span class="fa fa-fw fa-edit"></span></a>
|
||||||
|
|
|
@ -1,388 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
|
||||||
{% load origin %}
|
|
||||||
{% load static %}
|
|
||||||
{% load ietf_filters %}
|
|
||||||
{% load humanize %}
|
|
||||||
|
|
||||||
{% block morecss %}
|
|
||||||
{% for area in area_list %}
|
|
||||||
.{{ area.upcase_acronym}}-scheme, .meeting_event th.{{ area.upcase_acronym}}-scheme, #{{ area.upcase_acronym }}-groups, #selector-{{ area.upcase_acronym }} { color:{{ area.fg_color }}; background-color: {{ area.bg_color }} }
|
|
||||||
.director-mark-{{ area.upcase_acronym}} {
|
|
||||||
border: 2px solid {{ area.fg_color}};
|
|
||||||
color:{{ area.fg_color }};
|
|
||||||
background-color: {{ area.bg_color }}
|
|
||||||
}
|
|
||||||
{% endfor %}
|
|
||||||
{% endblock morecss %}
|
|
||||||
|
|
||||||
{% block title %}IETF {{ meeting.number }} Meeting Agenda{% endblock %}
|
|
||||||
{% load agenda_custom_tags %}
|
|
||||||
{% block pagehead %}
|
|
||||||
<link rel='stylesheet' type='text/css' href="{% static 'ietf/css/agenda/jquery-ui-themes/jquery-ui-1.8.11.custom.css' %}" />
|
|
||||||
<link rel='stylesheet' type='text/css' href="{% static 'ietf/css/agenda/agenda.css' %}" />
|
|
||||||
{% endblock pagehead %}
|
|
||||||
|
|
||||||
{% block js %}
|
|
||||||
<script type="text/javascript" src="{% static 'ietf/js/agenda/jquery-1.8.2.min.js' %}"></script>
|
|
||||||
<script src="{% static 'js-cookie/src/js.cookie.js' %}"></script>
|
|
||||||
<script>
|
|
||||||
jQuery.ajaxSetup({
|
|
||||||
crossDomain: false, // obviates need for sameOrigin test
|
|
||||||
beforeSend: function(xhr, settings) {
|
|
||||||
if (!csrfSafeMethod(settings.type)) {
|
|
||||||
xhr.setRequestHeader("X-CSRFToken", Cookies.get('csrftoken'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<script type='text/javascript' src="{% static 'ietf/js/agenda/jquery-ui-1.9.0.custom/minified/jquery-ui.custom.min.js' %}"></script>
|
|
||||||
<script type='text/javascript' src="{% static 'ietf/js/agenda/jquery-ui-1.9.0.custom/minified/jquery.ui.widget.min.js' %}"></script>
|
|
||||||
<script type='text/javascript' src="{% static 'ietf/js/agenda/jquery-ui-1.9.0.custom/minified/jquery.ui.droppable.min.js' %}"></script>
|
|
||||||
<script type='text/javascript' src="{% static 'ietf/js/agenda/jquery-ui-1.9.0.custom/minified/jquery.ui.sortable.min.js' %}"></script>
|
|
||||||
<script type='text/javascript' src="{% static 'ietf/js/agenda/jquery-ui-1.9.0.custom/minified/jquery.ui.accordion.min.js' %}"></script>
|
|
||||||
<script type='text/javascript' src="{% static 'ietf/js/agenda/jquery-ui-1.9.0.custom/minified/jquery.ui.draggable.min.js' %}"></script>
|
|
||||||
|
|
||||||
<script type='text/javascript' src="{% static 'spin.js/spin.min.js' %}"></script>
|
|
||||||
<script type='text/javascript' src="{% static 'ietf/js/agenda/agenda_edit.js' %}"></script>
|
|
||||||
<script type='text/javascript' src="{% static 'ietf/js/agenda/agenda_helpers.js' %}"></script>
|
|
||||||
<script type='text/javascript' src="{% static 'ietf/js/agenda/agenda_objects.js' %}"></script>
|
|
||||||
<script type='text/javascript' src="{% static 'ietf/js/agenda/agenda_listeners.js' %}"></script>
|
|
||||||
|
|
||||||
|
|
||||||
<script type='text/javascript'>
|
|
||||||
|
|
||||||
|
|
||||||
var meeting_number = "{{ meeting.number }}";
|
|
||||||
var schedule_id = {{ schedule.id }};
|
|
||||||
var schedule_owner_href = "{{ schedule.owner_email }}";
|
|
||||||
var schedule_name = "{{ schedule.name }}";
|
|
||||||
var meeting_base_url = "{{ meeting_base_url }}";
|
|
||||||
var schedule_owner_email = "{{ schedule.owner_email }}";
|
|
||||||
var site_base_url = "{{ site_base_url }}";
|
|
||||||
var assignments_post_href = "{% url "ietf.meeting.ajax.assignments_json" meeting.number schedule.owner_email schedule.name %}";
|
|
||||||
var total_days = {{time_slices|length}};
|
|
||||||
var total_rooms = {{rooms|length}};
|
|
||||||
|
|
||||||
function setup_slots(promiselist){
|
|
||||||
{% for day in time_slices %}
|
|
||||||
days.push("{{day}}");
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% for ad in area_directors %}
|
|
||||||
area_directors["{{ad.group.acronym}}"] = [];
|
|
||||||
{% endfor %}
|
|
||||||
{% for ad in area_directors %}
|
|
||||||
area_directors["{{ad.group.acronym}}"].push(find_person_by_href("{{ad.person.defurl}}"));
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
var ts_promise = load_timeslots("{% url "ietf.meeting.ajax.timeslot_slotsurl" meeting.number %}");
|
|
||||||
var sess_promise = load_sessions("{% url "ietf.meeting.ajax.sessions_json" meeting.number %}");
|
|
||||||
promiselist.push(ts_promise);
|
|
||||||
promiselist.push(sess_promise);
|
|
||||||
|
|
||||||
var ss_promise = load_assignments(ts_promise, sess_promise, assignments_post_href);
|
|
||||||
promiselist.push(ss_promise);
|
|
||||||
|
|
||||||
console.log("setup_slots run");
|
|
||||||
|
|
||||||
{% for area in area_list %}
|
|
||||||
legend_status["{{area.upcase_acronym}}"] = true;
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
|
||||||
<style type='text/css'>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
{% endblock js %}
|
|
||||||
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{% origin %}
|
|
||||||
<div id="read_only">
|
|
||||||
<p>You do not have write permission to agenda: {{schedule.name}}</p>
|
|
||||||
{% if schedule.is_official_record %}
|
|
||||||
<p>This is the official schedule for a meeting in the past.</p>
|
|
||||||
{% endif %}
|
|
||||||
<p>Please save this agenda to your account first.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="content"> {% comment %} For preserving styling across the facelift {% endcomment %}
|
|
||||||
|
|
||||||
<div class="wrapper custom_text_stuff">
|
|
||||||
|
|
||||||
|
|
||||||
<div id="unassigned-items">
|
|
||||||
<div id="all_agendas" class="events_bar_buttons">
|
|
||||||
<a href="{% url "ietf.meeting.views.list_schedules" meeting.number %}">
|
|
||||||
<button class="styled_button">all agendas</button>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div id="hidden_room" class="hide_buttons events_bar_buttons">
|
|
||||||
<div class="very_small left">hidden rooms:<span id="hidden_rooms" >0/{{rooms|length}}</span></div>
|
|
||||||
<div><button class="small_button" id="show_hidden_rooms">Show</button></div>
|
|
||||||
</div>
|
|
||||||
<div id="hidden_day" class="hide_buttons events_bar_buttons">
|
|
||||||
<div class="very_small left">hidden days:<span id="hidden_days" >0/{{time_slices|length}}</span></div>
|
|
||||||
<div><button class="small_button" id="show_hidden_days">Show</button></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="unassigned_order" class="events_bar_buttons">
|
|
||||||
<select id="unassigned_sort_button" class="dialog">
|
|
||||||
<option id="unassigned_alpha" value="alphaname" selected>Alphabetical</option>
|
|
||||||
<option id="unassigned_area" value="area">By Area</option>
|
|
||||||
<option id="unassigned_duration" value="duration">By Duration</option>
|
|
||||||
<option id="unassigned_special" value="special">Special Request</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="agenda_slot_title" >
|
|
||||||
<div style="ui-icon ui-icon-arrow-1-w" id="close_ietf_menubar">
|
|
||||||
<
|
|
||||||
</div>
|
|
||||||
<b>Unassigned Events:</b>
|
|
||||||
<span id="schedule_name">name: {{schedule.name}}</span>
|
|
||||||
</div>
|
|
||||||
<div id="sortable-list" class="ui-droppable bucket-list room_title">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="agenda_div">
|
|
||||||
|
|
||||||
<div id="dialog-confirm" title="" style="display:none">
|
|
||||||
<p>
|
|
||||||
<span class="ui-icon ui-icon-alert" style="background: white; float: left; margin: 0 7px 20px 0;"></span>
|
|
||||||
Are you sure you want to put two sessions into the same slot?
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="can-extend-dialog" title="" class="ui-dialog dialog" style="display:none">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="can-not-extend-dialog" title="" class="ui-dialog dialog" style="display:none">
|
|
||||||
<p>
|
|
||||||
<span class="ui-icon ui-icon-alert" style="background: white; float: left; margin: 0 7px 20px 0;"></span>
|
|
||||||
You can not extend this session. The slot is not available.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div id="dialog-confirm" title="" style="display:none">
|
|
||||||
<p>
|
|
||||||
<span class="ui-icon ui-icon-alert" style="background: white; float: left; margin: 0 7px 20px 0;"></span>
|
|
||||||
Are you sure you want to put two sessions into the same slot?
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- some boxes for dialogues -->
|
|
||||||
<div id="dialog-confirm-two" title="" style="display:none">
|
|
||||||
<p>
|
|
||||||
<span class="ui-icon ui-icon-alert" style="background: white; float: left; margin: 0 7px 20px 0;"></span>
|
|
||||||
<span class="dialog-confirm-text">Are you sure you want to put two sessions into the same slot?</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="dialog-confirm-toosmall" title="" style="display:none">
|
|
||||||
<p>
|
|
||||||
<span class="ui-icon ui-icon-alert" style="background: white; float: left; margin: 0 7px 20px 0;"></span>
|
|
||||||
<span class="dialog-confirm-text">The room you are moving to has a lower
|
|
||||||
room capacity then the requested capacity,<br>
|
|
||||||
Are you sure you want to continue?
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="dialog-confirm-twotoosmall" title="" style="display:none">
|
|
||||||
<p>
|
|
||||||
<span class="ui-icon ui-icon-alert" style="background: white; float: left; margin: 0 7px 20px 0;"></span>
|
|
||||||
<span class="dialog-confirm-text">
|
|
||||||
The slot you are moving to already has a session in it, <br>
|
|
||||||
the room is also smaller than the requested amount.<br>
|
|
||||||
Are you sure you want to continue?
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
{%comment%}<table id="meetings" class="ietf-navbar" style="width:100%">{%endcomment%}
|
|
||||||
<table id="meetings" class="ietf-navbar" >
|
|
||||||
<tr>
|
|
||||||
<th class="schedule_title"><div id="pageloaded" style="display:none">loaded</div><div id="spinner"><!-- spinney goes here --></div></th>
|
|
||||||
{% comment %}<th></th>{% endcomment %}
|
|
||||||
{% for day in time_slices %}
|
|
||||||
<th colspan="{{date_slices|colWidth:day}}" id="{{day|date:'Y-m-d'}}-btn" class=" day_{{day}} agenda_slot_title agenda_slot_unavailable">
|
|
||||||
<div id="close_{{day|date:'Y-m-d'}}" class="close top_left very_small close_day">x</div>
|
|
||||||
{{day|date:'D'}} ({{day}})
|
|
||||||
|
|
||||||
</th>
|
|
||||||
<th class="day_{{day}} spacer {{day|date:'Y-m-d'}}-spacer" id="">
|
|
||||||
<div class="ui-widget-content ui-resizable" id="resize-{{day|date:'Y-m-d'}}-spacer">
|
|
||||||
<div class="spacer_grip ui-resizable-handle ui-resizable-e"></div>
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
{% endfor %}
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
<th class="th_column"><button id="show_all_button" class="styled_button">show all</button></th>
|
|
||||||
{% comment %}<th><!-- resources --></th>{% endcomment %}
|
|
||||||
{% for day in time_slices %}
|
|
||||||
{% for slot in date_slices|lookup:day %}
|
|
||||||
<th class="day_{{day}}-{{slot.0|date:'Hi'}} day_{{day}} room_title ">{{slot.0|date:'Hi'}}-{{slot.1|date:'Hi'}} </th>
|
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
<th class="day_{{day}} spacer {{day|date:'Y-m-d'}}-spacer"></th>
|
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% for r in rooms %}
|
|
||||||
<tr id="{{r.name|to_acceptable_id}}" class="{% cycle 'agenda_row_alt' '' %} agenda_slot">
|
|
||||||
<th class="vert_time">
|
|
||||||
<div class="close very_small close_room top_left small_button" id="close_{{r.name|to_acceptable_id}}">X</div>
|
|
||||||
<div class="right room_name">{{r.name}} <span class="capacity">({{r.capacity}})</span></div>
|
|
||||||
</th>
|
|
||||||
{% comment 'The secretariat is not using these features' %}
|
|
||||||
<th class="room_features">
|
|
||||||
<div class="resource_list">
|
|
||||||
{% for resource in r.resources.all %}
|
|
||||||
<span class="resource_image">
|
|
||||||
<img src="{% static 'ietf/images/{{ resource.icon }}' %}" height=24 alt="{{resource.desc}}" title="{{resource.desc}}"/>
|
|
||||||
</span>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
{% endcomment %}
|
|
||||||
{% for day in time_slices %}
|
|
||||||
{% for slot in date_slices|lookup:day %}
|
|
||||||
<td id="{{r.dom_id}}_{{day}}_{{slot.0|date:'Hi'}}" class="day_{{day}} agenda-column-{{day}}-{{slot.0|date:'Hi'}} agenda_slot agenda_slot_unavailable" capacity="{{r.capacity}}" ></td>
|
|
||||||
{% endfor %}
|
|
||||||
<td class="day_{{day}} spacer {{day|date:'Y-m-d'}}-spacer"></td>
|
|
||||||
{% endfor %}
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div id="session-info" class="ui-droppable bucket-list room_title">
|
|
||||||
<div class="agenda_slot_title"><b>Session Information:</b></div>
|
|
||||||
|
|
||||||
<div class="ss_info_box">
|
|
||||||
<div class="ss_info ss_info_left">
|
|
||||||
<table>
|
|
||||||
<tr><td class="ss_info_name_short">Group:</td><td><span id="info_grp"></span>
|
|
||||||
<!-- <button id="agenda_sreq_button" class="right">Edit Request</button> --></tr>
|
|
||||||
<tr><td class="ss_info_name_short">Name:</td> <td id="info_name"></td></tr>
|
|
||||||
<tr><td class="ss_info_name_short">Area:</td> <td><span id="info_area"></span>
|
|
||||||
<button id="show_all_area" class="right">Show All</button></td></tr>
|
|
||||||
<tr>
|
|
||||||
<td colspan=2>
|
|
||||||
<div class="agenda_nice_button" id="agenda_find_free">
|
|
||||||
<button class="agenda_selected_buttons small_button" id="find_free">Find Free</button>
|
|
||||||
</div>
|
|
||||||
<div class="agenda_nice_button button_disabled" id="agenda_double_slot">
|
|
||||||
<button class="agenda_selected_buttons small_button" disabled id="double_slot">Extend</button>
|
|
||||||
</div>
|
|
||||||
<div id="agenda_pin_slot" class="agenda_nice_button button_disabled">
|
|
||||||
<button class="agenda_selected_buttons small_button" disabled id="pin_slot">Pin</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ss_info ss_info_right">
|
|
||||||
<table>
|
|
||||||
<tr><td class="ss_info_name_long">Duration/Capacity:</td>
|
|
||||||
<td class="info_split"><span id="info_duration"></span>
|
|
||||||
<span style="right" id="grp_type"></span></td>
|
|
||||||
<td class="info_split" id="info_capacity"></td></tr>
|
|
||||||
<tr><td class="ss_info_name_long">Location:</td><td colspan=2 id="info_location"></td></tr>
|
|
||||||
<tr><td class="ss_info_name_long">Responsible AD:</td><td colspan=2 id="info_responsible"></td></tr>
|
|
||||||
<tr><td class="ss_info_name_long">Requested By:</td><td colspan=2 id="info_requestedby"></td></tr>
|
|
||||||
<tr>
|
|
||||||
<td colspan=3>
|
|
||||||
<div class="agenda_nice_button button_disabled" id="agenda_prev_session">
|
|
||||||
<button class="agenda_selected_buttons small_button" disabled id="prev_session">Prev</button>
|
|
||||||
</div>
|
|
||||||
<div class="agenda_nice_button button_disabled" id="agenda_show">
|
|
||||||
<button class="agenda_selected_buttons small_button" disabled id="show_session">Show</button>
|
|
||||||
</div>
|
|
||||||
<div class="agenda_nice_button button_disabled" id="agenda_next_session">
|
|
||||||
<button class="agenda_selected_buttons small_button" disabled id="next_session">Next</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="request_features" id="agenda_requested_features">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="conflict_table">
|
|
||||||
<div id="special_requests">Special Requests</div>
|
|
||||||
<table>
|
|
||||||
<tbody id="conflict_table_body">
|
|
||||||
<tr class="conflict_list_row">
|
|
||||||
<td class="conflict_list_title">
|
|
||||||
Group conflicts
|
|
||||||
</td>
|
|
||||||
<td id="conflict_group_list">
|
|
||||||
<ul>
|
|
||||||
</ul>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr class="conflict_list_row">
|
|
||||||
<td class="conflict_list_title">
|
|
||||||
<b>be present</b>
|
|
||||||
</td>
|
|
||||||
<td id="conflict_people_list">
|
|
||||||
<ul>
|
|
||||||
</ul>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="color_legend">
|
|
||||||
{% for area in area_list %}
|
|
||||||
<span class="{{area.upcase_acronym}}-scheme"><input class='color_checkboxes' type="checkbox" id="{{area.upcase_acronym}}" value="{{area.upcase_acronym}}-value" checked>{{area.upcase_acronym}}</span>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="agenda_save_box">
|
|
||||||
|
|
||||||
<div id="agenda_title"><b>Agenda name: </b><span>{{schedule.name}}</span></div>
|
|
||||||
{% if can_edit_properties %}
|
|
||||||
<div><b>Properties</b> <a href="{% url "ietf.meeting.views.edit_schedule_properties" schedule.meeting.number schedule.owner_email schedule.name %}">Edit</a></div>
|
|
||||||
{% endif %}
|
|
||||||
<div id="agenda_saveas">
|
|
||||||
<form action="{{saveasurl}}" method="post">{% csrf_token %}
|
|
||||||
{{ saveas.as_p }}
|
|
||||||
<input id="saveasbutton" type="submit" name="saveas" value="saveas">
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div> {% comment %} End of .content div {% endcomment %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
|
@ -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">
|
||||||
|
@ -35,12 +38,12 @@
|
||||||
{% for schedule in schedules %}
|
{% for schedule in schedules %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url "ietf.meeting.views.edit_schedule" meeting.number schedule.owner_email schedule.name %}" title="Show regular sessions in agenda">{{ schedule.name }}</a>
|
<a href="{% url "ietf.meeting.views.edit_meeting_schedule" meeting.number schedule.owner_email schedule.name %}" title="Show regular sessions in agenda">{{ schedule.name }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ schedule.owner }}</td>
|
<td>{{ schedule.owner }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if schedule.origin %}
|
{% if schedule.origin %}
|
||||||
<a href="{% url "ietf.meeting.views.edit_schedule" meeting.number schedule.origin.owner_email schedule.origin.name %}">{{ schedule.origin.name }}</a>
|
<a href="{% url "ietf.meeting.views.edit_meeting_schedule" meeting.number schedule.origin.owner_email schedule.origin.name %}">{{ schedule.origin.name }}</a>
|
||||||
<a href="{% url "ietf.meeting.views.diff_schedules" meeting.number %}?from_schedule={{ schedule.origin.name|urlencode }}&to_schedule={{ schedule.name|urlencode }}" title="{{ schedule.changes_from_origin }} change{{ schedule.changes_from_origin|pluralize }} from {{ schedule.origin.name }}">+{{ schedule.changes_from_origin }}</a>
|
<a href="{% url "ietf.meeting.views.diff_schedules" meeting.number %}?from_schedule={{ schedule.origin.name|urlencode }}&to_schedule={{ schedule.name|urlencode }}" title="{{ schedule.changes_from_origin }} change{{ schedule.changes_from_origin|pluralize }} from {{ schedule.origin.name }}">+{{ schedule.changes_from_origin }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -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 %}
|
||||||
<div id="session-buttons-{{session.pk}}" class="text-nowrap">
|
<div id="session-buttons-{{session.pk}}" class="text-nowrap">
|
||||||
{% with acronym=session.historic_group.acronym %}
|
{% with acronym=session.historic_group.acronym %}
|
||||||
|
@ -112,3 +114,4 @@
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
{% endwith %}{% endwith %}{% endwith %}{% endwith %}
|
{% endwith %}{% endwith %}{% endwith %}{% endwith %}
|
||||||
|
{% endif %}
|
25
ietf/templates/meeting/session_details_form.html
Normal file
25
ietf/templates/meeting/session_details_form.html
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{# Copyright The IETF Trust 2007-2020, All Rights Reserved #}
|
||||||
|
<div class="session-details-form" data-prefix="{{ form.prefix }}">
|
||||||
|
{% if hidden %}{{ form.name.as_hidden }}{{ form.purpose.as_hidden }}{{ form.type.as_hidden }}{{ form.requested_duration.as_hidden }}
|
||||||
|
{% else %}
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>{{ form.name.label_tag }}</th>
|
||||||
|
<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>
|
||||||
|
<tr>
|
||||||
|
<th>{{ form.requested_duration.label_tag }}</th>
|
||||||
|
<td>{{ form.requested_duration }}{{ form.requested_duration.errors }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
{# hidden fields shown whether or not the whole form is hidden #}
|
||||||
|
{{ form.attendees.as_hidden }}{{ form.comments.as_hidden }}{{ form.id.as_hidden }}{{ form.on_agenda.as_hidden }}{{ form.DELETE.as_hidden }}
|
||||||
|
</div>
|
|
@ -1,57 +1,441 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
||||||
{% load origin %}
|
{% load origin %}
|
||||||
{% load agenda_custom_tags %}
|
{% load agenda_custom_tags misc_filters %}
|
||||||
|
|
||||||
{% 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%;}
|
{% comment %}
|
||||||
.tstable th { white-space: nowrap;}
|
Put scrollbars on the editor table. Requires fixed height so the scroll bars appear on the div, not the page body.
|
||||||
.tstable td { white-space: nowrap;}
|
Note that 100vh is viewport height. Using that minus 25rem seems to leave space for the page header/footer.
|
||||||
.capacity { font-size:80%; font-weight: normal;}
|
{% endcomment %}
|
||||||
|
.timeslot-edit { overflow: auto; height: max(30rem, calc(100vh - 25rem));}
|
||||||
|
.tstable { width: 100%; border-collapse: separate; } {# "separate" to ensure sticky cells keep their borders #}
|
||||||
|
.tstable thead { position: sticky; top: 0; z-index: 2; background-color: white;}
|
||||||
|
.tstable th:first-child, .tstable td:first-child {
|
||||||
|
background-color: white; {# needs to match the lighter of the striped-table colors! #}
|
||||||
|
position: sticky;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1.5; {# render above other cells / borders but below thead (z-index 2, above) #}
|
||||||
|
}
|
||||||
|
.tstable tbody > tr:nth-of-type(odd) > th:first-child {
|
||||||
|
background-color: rgb(249, 249, 249); {# needs to match the darker of the striped-table colors! #}
|
||||||
|
}
|
||||||
|
|
||||||
.tstable .tstype_unavail {background-color:#666;}
|
.tstable th { white-space: nowrap;}
|
||||||
|
.tstable td { white-space: nowrap;}
|
||||||
|
.capacity { font-size:80%; font-weight: normal;}
|
||||||
|
|
||||||
|
a.new-timeslot-link { color: lightgray; font-size: large;}
|
||||||
{% 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 %}{% with slots=slot_slices|lookup:day %}
|
||||||
|
{% for slice in date_slices|lookup:day %}{% with cell_ts=ts_list.popleft slot=slots|index:forloop.counter0 %}
|
||||||
|
<td class="tscell {% if cell_ts|length > 1 %}timeslot-collision {% endif %}{% for ts in cell_ts %}tstype_{{ ts.type.slug }} {% endfor %}">
|
||||||
|
{% if cell_ts %}
|
||||||
|
{% 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 %}
|
||||||
|
{% endif %}
|
||||||
|
<a class="new-timeslot-link {% if cell_ts %}hidden{% endif %}"
|
||||||
|
href="{% url "ietf.meeting.views.create_timeslot" num=meeting.number %}?day={{ day.toordinal }}&date={{ day|date:"Y-m-d" }}&location={{ room.pk }}&time={{ slot.time|date:'H:i' }}&duration={{ slot.duration }}">
|
||||||
|
<span class="fa fa-plus-square"></span>
|
||||||
|
</a>
|
||||||
|
{% endwith %}{% endfor %}
|
||||||
|
</td>
|
||||||
|
{% endwith %}{% 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.closest('td.tscell').querySelector('.new-timeslot-link').classList.remove('hidden');
|
||||||
|
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>
|
|
@ -65,22 +65,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(
|
||||||
|
|
|
@ -60,3 +60,13 @@ def keep_only(items, arg):
|
||||||
present and truthy. The attribute can be an int or a string.
|
present and truthy. The attribute can be an int or a string.
|
||||||
"""
|
"""
|
||||||
return [item for item, value in zip(items, list_extract(items, arg)) if value]
|
return [item for item, value in zip(items, list_extract(items, arg)) if value]
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def index(container, index):
|
||||||
|
"""Retrieve item from an indexable container
|
||||||
|
|
||||||
|
Usage: {{ item_list|index:forloop.counter0 }}
|
||||||
|
Returns the index corresponding to the loop counter. (For a literal value,
|
||||||
|
just use the {{ item_list.0 }} syntax.)
|
||||||
|
"""
|
||||||
|
return container[index]
|
||||||
|
|
|
@ -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