Add support for the timerange, wg_adjacent and time_relation constraints.

This adds three new constraints to the database and relevant UIs:
- timerange: "This WG can't meet during these timeframes"
- wg_adjacent: "Schedule adjacent to another WG (directly following,
  no breaks, same room)"
- time_relation: schedule the two sessions of one WG on subsequent
  days or with at least one day seperation
 - Legacy-Id: 17289
This commit is contained in:
Sasha Romijn 2020-02-12 10:42:38 +00:00
parent 8f0eca5032
commit a227813dc5
15 changed files with 482 additions and 25 deletions

View file

@ -0,0 +1,47 @@
# Copyright The IETF Trust 2020, All Rights Reserved
# -*- coding: utf-8 -*-
# Generated by Django 1.11.27 on 2020-02-11 04:47
from __future__ import unicode_literals
from django.db import migrations, models
def forward(apps, schema_editor):
ConstraintName = apps.get_model("name", "ConstraintName")
ConstraintName.objects.create(slug="timerange", desc="", penalty=100000,
name="Can't meet within timerange")
ConstraintName.objects.create(slug="time_relation", desc="", penalty=1000,
name="Preference for time between sessions")
ConstraintName.objects.create(slug="wg_adjacent", desc="", penalty=10000,
name="Request for adjacent scheduling with another WG")
def reverse(apps, schema_editor):
ConstraintName = apps.get_model("name", "ConstraintName")
ConstraintName.objects.filter(slug__in=["timerange", "time_relation", "wg_adjacent"]).delete()
class Migration(migrations.Migration):
dependencies = [
('name', '0010_timerangename'),
('meeting', '0025_rename_type_session_to_regular'),
]
operations = [
migrations.RemoveField(
model_name='constraint',
name='day',
),
migrations.AddField(
model_name='constraint',
name='time_relation',
field=models.CharField(blank=True, choices=[('subsequent-days', 'Schedule the sessions on subsequent days'), ('one-day-seperation', 'Leave at least one free day in between the two sessions')], max_length=200),
),
migrations.AddField(
model_name='constraint',
name='timeranges',
field=models.ManyToManyField(to='name.TimerangeName'),
),
migrations.RunPython(forward, reverse),
]

View file

@ -1,4 +1,4 @@
# Copyright The IETF Trust 2007-2019, All Rights Reserved
# Copyright The IETF Trust 2007-2020, All Rights Reserved
# -*- coding: utf-8 -*-
from __future__ import absolute_import, print_function, unicode_literals
@ -28,7 +28,7 @@ from ietf.dbtemplate.models import DBTemplate
from ietf.doc.models import Document
from ietf.group.models import Group
from ietf.group.utils import can_manage_materials
from ietf.name.models import MeetingTypeName, TimeSlotTypeName, SessionStatusName, ConstraintName, RoomResourceName, ImportantDateName
from ietf.name.models import MeetingTypeName, TimeSlotTypeName, SessionStatusName, ConstraintName, RoomResourceName, ImportantDateName, TimerangeName
from ietf.person.models import Person
from ietf.utils.decorators import memoize
from ietf.utils.storage import NoLocationMigrationFileSystemStorage
@ -810,19 +810,27 @@ class SchedTimeSessAssignment(models.Model):
class Constraint(models.Model):
"""
Specifies a constraint on the scheduling.
One type (name=conflic?) of constraint is between source WG and target WG,
e.g. some kind of conflict.
Another type (name=bethere) of constraint is between source WG and
availability of a particular Person, usually an AD.
A third type (name=avoidday) of constraint is between source WG and
a particular day of the week, specified in day.
Available types are:
- conflict/conflic2/conflic3: a conflict between source and target WG/session,
with varying priority. The first is used for a chair conflict, the second for
technology overlap, third for key person conflict
- bethere: a constraint between source WG and a particular person
- timerange: can not meet during these times
- time_relation: preference for a time difference between sessions
- wg_adjacent: request for source WG to be adjacent (directly before or after,
no breaks, same room) the target WG
"""
TIME_RELATION_CHOICES = (
('subsequent-days', 'Schedule the sessions on subsequent days'),
('one-day-seperation', 'Leave at least one free day in between the two sessions'),
)
meeting = ForeignKey(Meeting)
source = ForeignKey(Group, related_name="constraint_source_set")
target = ForeignKey(Group, related_name="constraint_target_set", null=True)
person = ForeignKey(Person, null=True, blank=True)
day = models.DateTimeField(null=True, blank=True)
name = ForeignKey(ConstraintName)
time_relation = models.CharField(max_length=200, choices=TIME_RELATION_CHOICES, blank=True)
timeranges = models.ManyToManyField(TimerangeName)
active_status = None
@ -830,7 +838,14 @@ class Constraint(models.Model):
return u"%s %s target=%s person=%s" % (self.source, self.name.name.lower(), self.target, self.person)
def brief_display(self):
if self.target and self.person:
if self.name.slug == "wg_adjacent":
return "Adjacent with %s" % self.target.acronym
elif self.name.slug == "time_relation":
return self.get_time_relation_display()
elif self.name.slug == "timerange":
timeranges_str = ", ".join([t.desc for t in self.timeranges.all()])
return "Can't meet %s" % timeranges_str
elif self.target and self.person:
return "%s ; %s" % (self.target.acronym, self.person)
elif self.target and not self.person:
return "%s " % (self.target.acronym)
@ -838,7 +853,7 @@ class Constraint(models.Model):
return "%s " % (self.person)
def json_url(self):
return "/meeting/%s/constraint/%s.json" % (self.meeting.number, self.id)
return "/meeting/%s/constrai.nt/%s.json" % (self.meeting.number, self.id)
def json_dict(self, host_scheme):
ct1 = dict()

View file

@ -1,4 +1,4 @@
# Copyright The IETF Trust 2016-2019, All Rights Reserved
# Copyright The IETF Trust 2010-2020, All Rights Reserved
from django.contrib import admin
from ietf.name.models import (
@ -10,7 +10,7 @@ from ietf.name.models import (
LiaisonStatementState, LiaisonStatementTagName, MeetingTypeName, NomineePositionStateName,
ReviewRequestStateName, ReviewResultName, ReviewTypeName, RoleName, RoomResourceName,
SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName, TopicAudienceName,
DocUrlTagName, ReviewAssignmentStateName, ReviewerQueuePolicyName)
DocUrlTagName, ReviewAssignmentStateName, ReviewerQueuePolicyName, TimerangeName)
from ietf.stats.models import CountryAlias
@ -79,5 +79,6 @@ admin.site.register(SessionStatusName, NameAdmin)
admin.site.register(StdLevelName, NameAdmin)
admin.site.register(StreamName, NameAdmin)
admin.site.register(TimeSlotTypeName, NameAdmin)
admin.site.register(TimerangeName, NameAdmin)
admin.site.register(TopicAudienceName, NameAdmin)
admin.site.register(DocUrlTagName, NameAdmin)

View file

@ -5585,6 +5585,39 @@
"model": "name.constraintname",
"pk": "conflic3"
},
{
"fields": {
"desc": "",
"name": "Can't meet within timerange",
"order": 0,
"penalty": 100000,
"used": true
},
"model": "name.constraintname",
"pk": "timerange"
},
{
"fields": {
"desc": "",
"name": "Preference for time between sessions",
"order": 0,
"penalty": 100000,
"used": true
},
"model": "name.constraintname",
"pk": "time_relation"
},
{
"fields": {
"desc": "",
"name": "Request for adjacent scheduling with another WG",
"order": 0,
"penalty": 100000,
"used": true
},
"model": "name.constraintname",
"pk": "wg_adjacent"
},
{
"fields": {
"desc": "",
@ -11678,6 +11711,156 @@
"model": "name.streamname",
"pk": "legacy"
},
{
"model": "name.timerangename",
"pk": "friday-afternoon-early",
"fields": {
"name": "friday-afternoon-early",
"desc": "Friday early afternoon",
"used": true,
"order": 13
}
},
{
"model": "name.timerangename",
"pk": "friday-afternoon-late",
"fields": {
"name": "friday-afternoon-late",
"desc": "Friday late afternoon",
"used": true,
"order": 14
}
},
{
"model": "name.timerangename",
"pk": "friday-morning",
"fields": {
"name": "friday-morning",
"desc": "Friday morning",
"used": true,
"order": 12
}
},
{
"model": "name.timerangename",
"pk": "monday-afternoon-early",
"fields": {
"name": "monday-afternoon-early",
"desc": "Monday early afternoon",
"used": true,
"order": 1
}
},
{
"model": "name.timerangename",
"pk": "monday-afternoon-late",
"fields": {
"name": "monday-afternoon-late",
"desc": "Monday late afternoon",
"used": true,
"order": 2
}
},
{
"model": "name.timerangename",
"pk": "monday-morning",
"fields": {
"name": "monday-morning",
"desc": "Monday morning",
"used": true,
"order": 0
}
},
{
"model": "name.timerangename",
"pk": "thursday-afternoon-early",
"fields": {
"name": "thursday-afternoon-early",
"desc": "Thursday early afternoon",
"used": true,
"order": 10
}
},
{
"model": "name.timerangename",
"pk": "thursday-afternoon-late",
"fields": {
"name": "thursday-afternoon-late",
"desc": "Thursday late afternoon",
"used": true,
"order": 11
}
},
{
"model": "name.timerangename",
"pk": "thursday-morning",
"fields": {
"name": "thursday-morning",
"desc": "Thursday morning",
"used": true,
"order": 9
}
},
{
"model": "name.timerangename",
"pk": "tuesday-afternoon-early",
"fields": {
"name": "tuesday-afternoon-early",
"desc": "Tuesday early afternoon",
"used": true,
"order": 4
}
},
{
"model": "name.timerangename",
"pk": "tuesday-afternoon-late",
"fields": {
"name": "tuesday-afternoon-late",
"desc": "Tuesday late afternoon",
"used": true,
"order": 5
}
},
{
"model": "name.timerangename",
"pk": "tuesday-morning",
"fields": {
"name": "tuesday-morning",
"desc": "Tuesday morning",
"used": true,
"order": 3
}
},
{
"model": "name.timerangename",
"pk": "wednesday-afternoon-early",
"fields": {
"name": "wednesday-afternoon-early",
"desc": "Wednesday early afternoon",
"used": true,
"order": 7
}
},
{
"model": "name.timerangename",
"pk": "wednesday-afternoon-late",
"fields": {
"name": "wednesday-afternoon-late",
"desc": "Wednesday late afternoon",
"used": true,
"order": 8
}
},
{
"model": "name.timerangename",
"pk": "wednesday-morning",
"fields": {
"name": "wednesday-morning",
"desc": "Wednesday morning",
"used": true,
"order": 6
}
},
{
"fields": {
"desc": "",

View file

@ -0,0 +1,58 @@
# Copyright The IETF Trust 2020, All Rights Reserved
# -*- coding: utf-8 -*-
# Generated by Django 1.11.27 on 2020-02-04 05:43
from __future__ import unicode_literals
from django.db import migrations, models
def forward(apps, schema_editor):
TimerangeName = apps.get_model('name', 'TimerangeName')
timeranges = [
('monday-morning', 'Monday morning'),
('monday-afternoon-early', 'Monday early afternoon'),
('monday-afternoon-late', 'Monday late afternoon'),
('tuesday-morning', 'Tuesday morning'),
('tuesday-afternoon-early', 'Tuesday early afternoon'),
('tuesday-afternoon-late', 'Tuesday late afternoon'),
('wednesday-morning', 'Wednesday morning'),
('wednesday-afternoon-early', 'Wednesday early afternoon'),
('wednesday-afternoon-late', 'Wednesday late afternoon'),
('thursday-morning', 'Thursday morning'),
('thursday-afternoon-early', 'Thursday early afternoon'),
('thursday-afternoon-late', 'Thursday late afternoon'),
('friday-morning', 'Friday morning'),
('friday-afternoon-early', 'Friday early afternoon'),
('friday-afternoon-late', 'Friday late afternoon'),
]
for order, (slug, desc) in enumerate(timeranges):
TimerangeName.objects.create(slug=slug, name=slug, desc=desc, used=True, order=order)
def reverse(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
('name', '0009_add_verified_errata_to_doctagname'),
]
operations = [
migrations.CreateModel(
name='TimerangeName',
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,
},
),
migrations.RunPython(forward, reverse),
]

View file

@ -1,4 +1,4 @@
# Copyright The IETF Trust 2010-2019, All Rights Reserved
# Copyright The IETF Trust 2010-2020, All Rights Reserved
# -*- coding: utf-8 -*-
@ -71,8 +71,10 @@ class SessionStatusName(NameModel):
class TimeSlotTypeName(NameModel):
"""Session, Break, Registration, Other, Reserved, unavail"""
class ConstraintName(NameModel):
"""Conflict"""
"""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)")
class TimerangeName(NameModel):
"""(monday|tuesday|wednesday|thursday|friday)-(morning|afternoon-early|afternoon-late)"""
class LiaisonStatementPurposeName(NameModel):
"""For action, For comment, For information, In response, Other"""
class NomineePositionStateName(NameModel):

View file

@ -1,4 +1,4 @@
# Copyright The IETF Trust 2016-2019, All Rights Reserved
# Copyright The IETF Trust 2014-2020, All Rights Reserved
# Autogenerated by the makeresources management command 2015-08-27 11:01 PDT
from ietf.api import ModelResource
from ietf.api import ToOneField # pyflakes:ignore
@ -17,7 +17,7 @@ from ietf.name.models import ( AgendaTypeName, BallotPositionName, ConstraintNam
LiaisonStatementState, LiaisonStatementTagName, MeetingTypeName, NomineePositionStateName,
ReviewAssignmentStateName, ReviewRequestStateName, ReviewResultName, ReviewTypeName,
RoleName, RoomResourceName, SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName,
TopicAudienceName, ReviewerQueuePolicyName)
TopicAudienceName, ReviewerQueuePolicyName, TimerangeName)
class TimeSlotTypeNameResource(ModelResource):
class Meta:
@ -598,3 +598,20 @@ class AgendaTypeNameResource(ModelResource):
api.name.register(AgendaTypeNameResource())
class TimerangeNameResource(ModelResource):
class Meta:
queryset = TimerangeName.objects.all()
serializer = api.Serializer()
cache = SimpleCache()
#resource_name = 'timerangename'
ordering = ['slug', ]
filtering = {
"slug": ALL,
"name": ALL,
"desc": ALL,
"used": ALL,
"order": ALL,
}
api.name.register(TimerangeNameResource())

View file

@ -8,8 +8,9 @@ from django import forms
import debug # pyflakes:ignore
from ietf.name.models import TimerangeName
from ietf.group.models import Group
from ietf.meeting.models import ResourceAssociation
from ietf.meeting.models import ResourceAssociation, Constraint
from ietf.person.fields import SearchablePersonsField
@ -20,6 +21,7 @@ from ietf.person.fields import SearchablePersonsField
NUM_SESSION_CHOICES = (('','--Please select'),('1','1'),('2','2'))
# LENGTH_SESSION_CHOICES = (('','--Please select'),('1800','30 minutes'),('3600','1 hour'),('5400','1.5 hours'), ('7200','2 hours'),('9000','2.5 hours'))
LENGTH_SESSION_CHOICES = (('','--Please select'),('1800','30 minutes'),('3600','1 hour'),('5400','1.5 hours'), ('7200','2 hours'))
SESSION_TIME_RELATION_CHOICES = (('', 'No preference'),) + Constraint.TIME_RELATION_CHOICES
# -------------------------------------------------
# Helper Functions
@ -65,10 +67,17 @@ class GroupSelectForm(forms.Form):
super(GroupSelectForm, self).__init__(*args,**kwargs)
self.fields['group'].widget.choices = choices
class NameModelMultipleChoiceField(forms.ModelMultipleChoiceField):
def label_from_instance(self, name):
return name.desc
class SessionForm(forms.Form):
num_session = forms.ChoiceField(choices=NUM_SESSION_CHOICES)
length_session1 = forms.ChoiceField(choices=LENGTH_SESSION_CHOICES)
length_session2 = forms.ChoiceField(choices=LENGTH_SESSION_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()
# FIXME: it would cleaner to have these be
@ -84,6 +93,9 @@ class SessionForm(forms.Form):
third_session = forms.BooleanField(required=False)
resources = forms.MultipleChoiceField(widget=forms.CheckboxSelectMultiple,required=False)
bethere = SearchablePersonsField(label="Must be present", required=False)
timeranges = NameModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple, required=False,
queryset=TimerangeName.objects.all())
adjacent_with_wg = forms.ChoiceField(required=False)
def __init__(self, group, *args, **kwargs):
if 'hidden' in kwargs:
@ -100,7 +112,9 @@ class SessionForm(forms.Form):
self.fields['length_session3'].widget.attrs['onClick'] = "if (check_third_session()) { this.disabled=true;}"
self.fields['comments'].widget = forms.Textarea(attrs={'rows':'6','cols':'65'})
group_acronym_choices = [('','--Select WG(s)')] + 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'))
self.fields['adjacent_with_wg'].choices = [('', '--No preference')] + other_groups
group_acronym_choices = [('','--Select WG(s)')] + other_groups
for i in range(1, 4):
self.fields['wg_selector{}'.format(i)].choices = group_acronym_choices
@ -129,6 +143,7 @@ class SessionForm(forms.Form):
for key in list(self.fields.keys()):
self.fields[key].widget = forms.HiddenInput()
self.fields['resources'].widget = forms.MultipleHiddenInput()
self.fields['timeranges'].widget = forms.MultipleHiddenInput()
def clean_conflict1(self):
conflict = self.cleaned_data['conflict1']
@ -165,6 +180,8 @@ class SessionForm(forms.Form):
if data.get('num_session','') == '2':
if not data['length_session2']:
raise forms.ValidationError('You must enter a length for all sessions')
elif data.get('session_time_relation'):
raise forms.ValidationError('Time between sessions can only be used when two sessions are requested.')
if data.get('third_session',False):
if not data['length_session2'] or not data.get('length_session3',None):

View file

@ -15,6 +15,7 @@ from ietf.utils.test_utils import TestCase
from ietf.group.factories import GroupFactory, RoleFactory
from ietf.meeting.models import Session, ResourceAssociation, SchedulingEvent, Constraint
from ietf.meeting.factories import MeetingFactory, SessionFactory
from ietf.name.models import TimerangeName
from ietf.person.models import Person
from ietf.utils.mail import outbox, empty_outbox
@ -80,6 +81,7 @@ class SessionRequestTestCase(TestCase):
def test_edit(self):
meeting = MeetingFactory(type_id='ietf', date=datetime.date.today())
mars = RoleFactory(name_id='chair', person__user__username='marschairman', group__acronym='mars').group
group2 = GroupFactory()
SessionFactory(meeting=meeting,group=mars,status_id='sched')
url = reverse('ietf.secr.sreq.views.edit', kwargs={'acronym':'mars'})
@ -92,10 +94,31 @@ class SessionRequestTestCase(TestCase):
'attendees':'10',
'conflict1':'',
'comments':'need lights',
'session_time_relation': 'subsequent-days',
'adjacent_with_wg': group2.acronym,
'timeranges': ['thursday-afternoon-early', 'thursday-afternoon-late'],
'submit': 'Continue'}
r = self.client.post(url, post_data, HTTP_HOST='example.com')
self.assertRedirects(r,reverse('ietf.secr.sreq.views.view', kwargs={'acronym':'mars'}))
redirect_url = reverse('ietf.secr.sreq.views.view', kwargs={'acronym': 'mars'})
self.assertRedirects(r, redirect_url)
# Check whether updates were stored in the database
sessions = Session.objects.filter(meeting=meeting, group=mars)
self.assertEqual(len(sessions), 2)
session = sessions[0]
self.assertEqual(session.constraints().get(name='time_relation').time_relation, 'subsequent-days')
self.assertEqual(session.constraints().get(name='wg_adjacent').target.acronym, group2.acronym)
self.assertEqual(
list(session.constraints().get(name='timerange').timeranges.all().values('name')),
list(TimerangeName.objects.filter(name__in=['thursday-afternoon-early', 'thursday-afternoon-late']).values('name'))
)
# Check whether the updated data is visible on the view page
r = self.client.get(redirect_url)
self.assertContains(r, 'Schedule the sessions on subsequent days')
self.assertContains(r, 'Thursday early afternoon, Thursday late afternoon')
self.assertContains(r, group2.acronym)
def test_tool_status(self):
MeetingFactory(type_id='ietf', date=datetime.date.today())
url = reverse('ietf.secr.sreq.views.tool_status')
@ -111,6 +134,7 @@ class SubmitRequestCase(TestCase):
ad = Person.objects.get(user__username='ad')
area = RoleFactory(name_id='ad', person=ad, group__type_id='area').group
group = GroupFactory(parent=area)
group2 = GroupFactory(parent=area)
session_count_before = Session.objects.filter(meeting=meeting, group=group).count()
url = reverse('ietf.secr.sreq.views.new',kwargs={'acronym':group.acronym})
confirm_url = reverse('ietf.secr.sreq.views.confirm',kwargs={'acronym':group.acronym})
@ -120,10 +144,17 @@ class SubmitRequestCase(TestCase):
'attendees':'10',
'conflict1':'',
'comments':'need projector',
'adjacent_with_wg': group2.acronym,
'timeranges': ['thursday-afternoon-early', 'thursday-afternoon-late'],
'submit': 'Continue'}
self.client.login(username="secretary", password="secretary+password")
r = self.client.post(url,post_data)
self.assertEqual(r.status_code, 200)
# Verify the contents of the confirm view
self.assertContains(r, 'Thursday early afternoon, Thursday late afternoon')
self.assertContains(r, group2.acronym)
post_data['submit'] = 'Submit'
r = self.client.post(confirm_url,post_data)
self.assertRedirects(r, main_url)
@ -205,6 +236,7 @@ class SubmitRequestCase(TestCase):
area = GroupFactory(type_id='area')
RoleFactory(name_id='ad', person=ad, group=area)
group = GroupFactory(acronym='ames', parent=area)
group2 = GroupFactory(acronym='ames2', parent=area)
RoleFactory(name_id='chair', group=group, person__user__username='ameschairman')
resource = ResourceAssociation.objects.create(name_id='project')
# Bit of a test data hack - the fixture now has no used resources to pick from
@ -214,20 +246,24 @@ class SubmitRequestCase(TestCase):
url = reverse('ietf.secr.sreq.views.new',kwargs={'acronym':group.acronym})
confirm_url = reverse('ietf.secr.sreq.views.confirm',kwargs={'acronym':group.acronym})
len_before = len(outbox)
post_data = {'num_session':'1',
post_data = {'num_session':'2',
'length_session1':'3600',
'length_session2':'3600',
'attendees':'10',
'bethere':str(ad.pk),
'conflict1':'',
'comments':'',
'resources': resource.pk,
'session_time_relation': 'subsequent-days',
'adjacent_with_wg': group2.acronym,
'timeranges': ['thursday-afternoon-early', 'thursday-afternoon-late'],
'submit': 'Continue'}
self.client.login(username="ameschairman", password="ameschairman+password")
# submit
r = self.client.post(url,post_data)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue('Confirm' in six.text_type(q("title")))
self.assertTrue('Confirm' in six.text_type(q("title")), r.context['form'].errors)
# confirm
post_data['submit'] = 'Submit'
r = self.client.post(confirm_url,post_data)
@ -235,10 +271,22 @@ class SubmitRequestCase(TestCase):
self.assertEqual(len(outbox),len_before+1)
notification = outbox[-1]
notification_payload = six.text_type(notification.get_payload(decode=True),"utf-8","replace")
session = Session.objects.get(meeting=meeting,group=group)
sessions = Session.objects.filter(meeting=meeting,group=group)
self.assertEqual(len(sessions), 2)
session = sessions[0]
self.assertEqual(session.resources.count(),1)
self.assertEqual(session.people_constraints.count(),1)
self.assertEqual(session.constraints().get(name='time_relation').time_relation, 'subsequent-days')
self.assertEqual(session.constraints().get(name='wg_adjacent').target.acronym, group2.acronym)
self.assertEqual(
list(session.constraints().get(name='timerange').timeranges.all().values('name')),
list(TimerangeName.objects.filter(name__in=['thursday-afternoon-early', 'thursday-afternoon-late']).values('name'))
)
resource = session.resources.first()
self.assertTrue('Schedule the sessions on subsequent days' in notification_payload)
self.assertTrue(group2.acronym in notification_payload)
self.assertTrue("Can't meet: Thursday early afternoon, Thursday late" in notification_payload)
self.assertTrue(resource.desc in notification_payload)
self.assertTrue(ad.ascii_name() in notification_payload)

View file

@ -80,6 +80,14 @@ def get_initial_session(sessions, prune_conflicts=False):
initial['comments'] = sessions[0].comments
initial['resources'] = sessions[0].resources.all()
initial['bethere'] = [x.person for x in sessions[0].constraints().filter(name='bethere').select_related("person")]
wg_adjacent = conflicts.filter(name__slug='wg_adjacent')
initial['adjacent_with_wg'] = wg_adjacent[0].target.acronym if wg_adjacent else None
time_relation = conflicts.filter(name__slug='time_relation')
initial['session_time_relation'] = time_relation[0].time_relation if time_relation else None
initial['session_time_relation_display'] = time_relation[0].get_time_relation_display if time_relation else None
timeranges = conflicts.filter(name__slug='timerange')
initial['timeranges'] = timeranges[0].timeranges.all() if timeranges else []
initial['timeranges_display'] = [t.desc for t in initial['timeranges']]
return initial
def get_lock_message(meeting=None):
@ -262,6 +270,10 @@ def confirm(request, acronym):
if 'bethere' in session_data:
person_id_list = [ id for id in form.data['bethere'].split(',') if id ]
session_data['bethere'] = Person.objects.filter(pk__in=person_id_list)
if session_data.get('session_time_relation'):
session_data['session_time_relation_display'] = dict(Constraint.TIME_RELATION_CHOICES)[session_data['session_time_relation']]
if form.cleaned_data.get('timeranges'):
session_data['timeranges_display'] = [t.desc for t in form.cleaned_data['timeranges']]
session_data['resources'] = [ ResourceAssociation.objects.get(pk=pk) for pk in request.POST.getlist('resources') ]
button_text = request.POST.get('submit', '')
@ -307,6 +319,17 @@ def confirm(request, acronym):
save_conflicts(group,meeting,form.data.get('conflict1',''),'conflict')
save_conflicts(group,meeting,form.data.get('conflict2',''),'conflic2')
save_conflicts(group,meeting,form.data.get('conflict3',''),'conflic3')
save_conflicts(group, meeting, form.data.get('adjacent_with_wg', ''), 'wg_adjacent')
if form.cleaned_data.get('session_time_relation'):
cn = ConstraintName.objects.get(slug='time_relation')
Constraint.objects.create(source=group, meeting=meeting, name=cn,
time_relation=form.cleaned_data['session_time_relation'])
if form.cleaned_data.get('timeranges'):
cn = ConstraintName.objects.get(slug='timerange')
constraint = Constraint.objects.create(source=group, meeting=meeting, name=cn)
constraint.timeranges.set(form.cleaned_data['timeranges'])
if 'bethere' in form.data:
bethere_cn = ConstraintName.objects.get(slug='bethere')
@ -459,6 +482,9 @@ def edit(request, acronym, num=None):
if 'conflict3' in form.changed_data:
Constraint.objects.filter(meeting=meeting,source=group,name='conflic3').delete()
save_conflicts(group,meeting,form.cleaned_data['conflict3'],'conflic3')
if 'adjacent_with_wg' in form.changed_data:
Constraint.objects.filter(meeting=meeting, source=group, name='wg_adjacent').delete()
save_conflicts(group, meeting, form.cleaned_data['adjacent_with_wg'], 'wg_adjacent')
if 'resources' in form.changed_data:
new_resource_ids = form.cleaned_data['resources']
@ -472,6 +498,20 @@ def edit(request, acronym, num=None):
for p in form.cleaned_data['bethere']:
Constraint.objects.create(name=bethere_cn, source=group, person=p, meeting=session.meeting)
if 'session_time_relation' in form.changed_data:
Constraint.objects.filter(meeting=meeting, source=group, name='time_relation').delete()
if form.cleaned_data['session_time_relation']:
cn = ConstraintName.objects.get(slug='time_relation')
Constraint.objects.create(source=group, meeting=meeting, name=cn,
time_relation=form.cleaned_data['session_time_relation'])
if 'timeranges' in form.changed_data:
Constraint.objects.filter(meeting=meeting, source=group, name='timerange').delete()
if form.cleaned_data['timeranges']:
cn = ConstraintName.objects.get(slug='timerange')
constraint = Constraint.objects.create(source=group, meeting=meeting, name=cn)
constraint.timeranges.set(form.cleaned_data['timeranges'])
# deprecated
# log activity
#add_session_activity(group,'Session Request was updated',meeting,user)

View file

@ -16,22 +16,27 @@ function stat_ls (val){
document.form_post.length_session1.disabled = true;
document.form_post.length_session2.disabled = true;
document.form_post.length_session3.disabled = true;
document.form_post.session_time_relation.disabled = true;
document.form_post.length_session1.value = 0;
document.form_post.length_session2.value = 0;
document.form_post.length_session3.value = 0;
document.form_post.session_time_relation.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;
document.form_post.length_session3.disabled = true;
document.form_post.session_time_relation.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.third_session.checked=false;
}
if (val == 2) {
document.form_post.length_session1.disabled = false;
document.form_post.length_session2.disabled = false;
document.form_post.session_time_relation.disabled = false;
}
}

View file

@ -11,6 +11,9 @@ Conflicts to Avoid:
{% if session.conflict1 %} Chair Conflict: {{ session.conflict1 }}{% endif %}
{% if session.conflict2 %} Technology Overlap: {{ session.conflict2 }}{% endif %}
{% if session.conflict3 %} Key Participant Conflict: {{ session.conflict3 }}{% endif %}
{% if session.session_time_relation_display %} {{ session.session_time_relation_display }}{% endif %}
{% if session.adjacent_with_wg %} Adjacent with WG: {{ session.adjacent_with_wg }}{% endif %}
{% if session.timeranges_display %} Can't meet: {{ session.timeranges_display|join:", " }}{% endif %}
People who must be present:

View file

@ -8,6 +8,7 @@
<tr class="bg1"><td>Number of Sessions:<span class="required">*</span></td><td>{{ form.num_session.errors }}{{ form.num_session }}</td></tr>
<tr class="bg2"><td>Length of Session 1:<span class="required">*</span></td><td>{{ form.length_session1.errors }}{{ form.length_session1 }}</td></tr>
<tr class="bg2"><td>Length of Session 2:<span class="required">*</span></td><td>{{ form.length_session2.errors }}{{ form.length_session2 }}</td></tr>
<tr class="bg2"><td>Time between two sessions:</td><td>{{ form.session_time_relation.errors }}{{ form.session_time_relation }}</td></tr>
{% if group.type.slug == "wg" %}
<tr class="bg2"><td>Additional Session Request:</td><td>{{ form.third_session }} Check this box to request an additional session.<br>
Additional slot may be available after agenda scheduling has closed and with the approval of an Area Director.<br>
@ -59,6 +60,17 @@
{{ form.resources.errors }} {{ form.resources }}
</td>
</tr>
<tr class="bg1">
<td valign="top">Times during which this WG can <strong>not</strong> meet:</td>
<td>{{ form.timeranges.errors }}{{ form.timeranges }}</td>
</tr>
<tr class="bg2">
<td valign="top">
Plan session adjacent with another WG:<br />
(Immediately before or after another WG, no break in between, in the same room.)
</td>
<td>{{ form.adjacent_with_wg.errors }}{{ form.adjacent_with_wg }}</td>
</tr>
<tr class="bg1">
<td valign="top">Special Requests:<br />&nbsp;<br />i.e. restrictions on meeting times / days, etc.</td>
<td>{{ form.comments.errors }}{{ form.comments }}</td>

View file

@ -7,6 +7,7 @@
<tr class="row2"><td>Length of Session 1:</td><td>{{ session.length_session1|display_duration }}</td></tr>
{% if session.length_session2 %}
<tr class="row2"><td>Length of Session 2:</td><td>{{ session.length_session2|display_duration }}</td></tr>
<tr class="row2"><td>Time between sessions:</td><td>{{ session.session_time_relation_display|default:"No preference" }}</td></tr>
{% endif %}
{% if session.length_session3 %}
<tr class="row2"><td>Length of Session 3:</td><td>{{ session.length_session3|display_duration }}</td></tr>
@ -33,6 +34,14 @@
<tr class="row1">
<td>People who must be present:</td>
<td>{% if session.bethere %}<ul>{% for person in session.bethere %}<li>{{ person }}</li>{% endfor %}</ul>{% else %}<i>None</i>{% endif %}</td>
<tr class="row2">
<td>Can not meet on:</td>
<td>{% if session.timeranges_display %}{{ session.timeranges_display|join:', ' }}{% else %}No constraints{% endif %}</td>
</tr>
<tr class="row1">
<td>Adjacent with WG:</td>
<td>{{ session.adjacent_with_wg|default:'No preference' }}</td>
</tr>
{% autoescape off %}
<tr class="row2"><td>Special Requests:</td><td>{{ session.comments }}</td></tr>
{% endautoescape %}

View file

@ -43,7 +43,7 @@
<th>Size</th>
<th>Requester</th>
<th>AD</th>
<th>Conflicts</th>
<th>Constraints</th>
<th>Special requests</th>
</tr>
</thead>