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:
parent
8f0eca5032
commit
a227813dc5
|
@ -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),
|
||||
]
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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": "",
|
||||
|
|
58
ietf/name/migrations/0010_timerangename.py
Normal file
58
ietf/name/migrations/0010_timerangename.py
Normal 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),
|
||||
]
|
|
@ -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):
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 /> <br />i.e. restrictions on meeting times / days, etc.</td>
|
||||
<td>{{ form.comments.errors }}{{ form.comments }}</td>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue