Merged in ^/branch/dash/automatic-scheduler@17395, which adds groundwor for

upcoming automatic scheduling assistance:

 . Added a management command to create a dummy IETF 999 meeting.

 . Added display of new constraints and joint sessions to agenda builder
   interface.

 . The new timerange, time_relation and wg_adjacent constraints, along with
   the joint_with_groups option, are now reflected in the special requests
   field.  This allows them to be taken into account while scheduling
   sessions.

 . Clarified the wording in the session request form regarding conflicts with
   BOFs.

 . Added support for structured entry and storage of joint sessions in
   meetings:

   - Also adds additional tests for the SessionForm
   - Fixes a javascript error in session requests for non-WG groups,
     that could cause incorrect form behaviour.
   - Expands the tests added in [17289] a bit.

 . Added 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: 17605
Note: SVN reference [17289] has been migrated to Git commit a227813dc5
This commit is contained in:
Henrik Levkowetz 2020-04-08 20:41:29 +00:00
commit b143dd407f
18 changed files with 5481 additions and 44 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,52 @@
# 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', '0026_cancel_107_sessions'),
]
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.AddField(
model_name='session',
name='joint_with_groups',
field=models.ManyToManyField(related_name='sessions_joint_in', to='group.Group'),
),
migrations.RunPython(forward, reverse),
]

View file

@ -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
@ -815,19 +815,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
@ -835,7 +843,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)
@ -857,6 +872,13 @@ class Constraint(models.Model):
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
@ -890,6 +912,7 @@ class Session(models.Model):
short = models.CharField(blank=True, max_length=32, help_text="Short version of 'name' above, for use in filenames.")
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.
joint_with_groups = models.ManyToManyField(Group, related_name='sessions_joint_in')
attendees = models.IntegerField(null=True, blank=True)
agenda_note = models.CharField(blank=True, max_length=255)
requested_duration = models.DurationField(default=datetime.timedelta(0))
@ -1013,6 +1036,9 @@ class Session(models.Model):
def is_material_submission_cutoff(self):
return datetime.date.today() > self.meeting.get_submission_correction_date()
def joint_with_groups_acronyms(self):
return [group.acronym for group in self.joint_with_groups.all()]
def __str__(self):
if self.meeting.type_id == "interim":
@ -1108,6 +1134,7 @@ class Session(models.Model):
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

View file

@ -1,4 +1,4 @@
# Copyright The IETF Trust 2013-2019, All Rights Reserved
# Copyright The IETF Trust 2013-2020, All Rights Reserved
# -*- coding: utf-8 -*-
@ -12,6 +12,7 @@ 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
@ -119,10 +120,23 @@ class ApiTests(TestCase):
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()
self.assertEqual(set([c_ames.pk, c_person.pk]), set(c["constraint_id"] for c in constraints))
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()

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

@ -5609,6 +5609,39 @@
"model": "name.constraintname",
"pk": "conflict"
},
{
"fields": {
"desc": "",
"name": "Preference for time between sessions",
"order": 0,
"penalty": 100000,
"used": true
},
"model": "name.constraintname",
"pk": "time_relation"
},
{
"fields": {
"desc": "",
"name": "Can't meet within timerange",
"order": 0,
"penalty": 100000,
"used": true
},
"model": "name.constraintname",
"pk": "timerange"
},
{
"fields": {
"desc": "",
"name": "Request for adjacent scheduling with another WG",
"order": 0,
"penalty": 100000,
"used": true
},
"model": "name.constraintname",
"pk": "wg_adjacent"
},
{
"fields": {
"desc": "",
@ -11701,6 +11734,156 @@
"model": "name.streamname",
"pk": "legacy"
},
{
"fields": {
"desc": "Friday early afternoon",
"name": "friday-afternoon-early",
"order": 13,
"used": true
},
"model": "name.timerangename",
"pk": "friday-afternoon-early"
},
{
"fields": {
"desc": "Friday late afternoon",
"name": "friday-afternoon-late",
"order": 14,
"used": true
},
"model": "name.timerangename",
"pk": "friday-afternoon-late"
},
{
"fields": {
"desc": "Friday morning",
"name": "friday-morning",
"order": 12,
"used": true
},
"model": "name.timerangename",
"pk": "friday-morning"
},
{
"fields": {
"desc": "Monday early afternoon",
"name": "monday-afternoon-early",
"order": 1,
"used": true
},
"model": "name.timerangename",
"pk": "monday-afternoon-early"
},
{
"fields": {
"desc": "Monday late afternoon",
"name": "monday-afternoon-late",
"order": 2,
"used": true
},
"model": "name.timerangename",
"pk": "monday-afternoon-late"
},
{
"fields": {
"desc": "Monday morning",
"name": "monday-morning",
"order": 0,
"used": true
},
"model": "name.timerangename",
"pk": "monday-morning"
},
{
"fields": {
"desc": "Thursday early afternoon",
"name": "thursday-afternoon-early",
"order": 10,
"used": true
},
"model": "name.timerangename",
"pk": "thursday-afternoon-early"
},
{
"fields": {
"desc": "Thursday late afternoon",
"name": "thursday-afternoon-late",
"order": 11,
"used": true
},
"model": "name.timerangename",
"pk": "thursday-afternoon-late"
},
{
"fields": {
"desc": "Thursday morning",
"name": "thursday-morning",
"order": 9,
"used": true
},
"model": "name.timerangename",
"pk": "thursday-morning"
},
{
"fields": {
"desc": "Tuesday early afternoon",
"name": "tuesday-afternoon-early",
"order": 4,
"used": true
},
"model": "name.timerangename",
"pk": "tuesday-afternoon-early"
},
{
"fields": {
"desc": "Tuesday late afternoon",
"name": "tuesday-afternoon-late",
"order": 5,
"used": true
},
"model": "name.timerangename",
"pk": "tuesday-afternoon-late"
},
{
"fields": {
"desc": "Tuesday morning",
"name": "tuesday-morning",
"order": 3,
"used": true
},
"model": "name.timerangename",
"pk": "tuesday-morning"
},
{
"fields": {
"desc": "Wednesday early afternoon",
"name": "wednesday-afternoon-early",
"order": 7,
"used": true
},
"model": "name.timerangename",
"pk": "wednesday-afternoon-early"
},
{
"fields": {
"desc": "Wednesday late afternoon",
"name": "wednesday-afternoon-late",
"order": 8,
"used": true
},
"model": "name.timerangename",
"pk": "wednesday-afternoon-late"
},
{
"fields": {
"desc": "Wednesday morning",
"name": "wednesday-morning",
"order": 6,
"used": true
},
"model": "name.timerangename",
"pk": "wednesday-morning"
},
{
"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
from ietf.utils.html import clean_text_field
@ -20,6 +21,8 @@ from ietf.utils.html import clean_text_field
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
JOINT_FOR_SESSION_CHOICES = (('1', 'First session'), ('2', 'Second session'), ('3', 'Third session'), )
# -------------------------------------------------
# Helper Functions
@ -65,10 +68,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
@ -77,13 +87,19 @@ class SessionForm(forms.Form):
conflict1 = forms.CharField(max_length=255,required=False)
conflict2 = forms.CharField(max_length=255,required=False)
conflict3 = forms.CharField(max_length=255,required=False)
joint_with_groups = forms.CharField(max_length=255,required=False)
joint_for_session = forms.ChoiceField(choices=JOINT_FOR_SESSION_CHOICES, required=False)
comments = forms.CharField(max_length=200,required=False)
wg_selector1 = forms.ChoiceField(choices=[],required=False)
wg_selector2 = forms.ChoiceField(choices=[],required=False)
wg_selector3 = forms.ChoiceField(choices=[],required=False)
wg_selector4 = forms.ChoiceField(choices=[],required=False)
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,8 +116,10 @@ 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':'3','cols':'65'})
group_acronym_choices = [('','--Select WG(s)')] + list(allowed_conflicting_groups().exclude(pk=group.pk).values_list('acronym','acronym').order_by('acronym'))
for i in range(1, 4):
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, 5):
self.fields['wg_selector{}'.format(i)].choices = group_acronym_choices
# disabling handleconflictfield (which only enables or disables form elements) while we're hacking the meaning of the three constraints currently in use:
@ -111,6 +129,7 @@ class SessionForm(forms.Form):
self.fields['wg_selector1'].widget.attrs['onChange'] = "document.form_post.conflict1.value=document.form_post.conflict1.value + ' ' + this.options[this.selectedIndex].value; return 1;"
self.fields['wg_selector2'].widget.attrs['onChange'] = "document.form_post.conflict2.value=document.form_post.conflict2.value + ' ' + this.options[this.selectedIndex].value; return 1;"
self.fields['wg_selector3'].widget.attrs['onChange'] = "document.form_post.conflict3.value=document.form_post.conflict3.value + ' ' + this.options[this.selectedIndex].value; return 1;"
self.fields['wg_selector4'].widget.attrs['onChange'] = "document.form_post.joint_with_groups.value=document.form_post.joint_with_groups.value + ' ' + this.options[this.selectedIndex].value; return 1;"
# disabling check_prior_conflict javascript while we're hacking the meaning of the three constraints currently in use
#self.fields['wg_selector2'].widget.attrs['onClick'] = "return check_prior_conflict(2);"
@ -129,6 +148,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']
@ -144,7 +164,12 @@ class SessionForm(forms.Form):
conflict = self.cleaned_data['conflict3']
check_conflict(conflict, self.group)
return conflict
def clean_joint_with_groups(self):
groups = self.cleaned_data['joint_with_groups']
check_conflict(groups, self.group)
return groups
def clean_comments(self):
return clean_text_field(self.cleaned_data['comments'])
@ -168,10 +193,20 @@ 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')
else:
if data.get('session_time_relation'):
raise forms.ValidationError('Time between sessions can only be used when two '
'sessions are requested.')
if data['joint_for_session'] == '2':
raise forms.ValidationError('The second session can not be the joint session, '
'because you have not requested a second session.')
if data.get('third_session',False):
if not data['length_session2'] or not data.get('length_session3',None):
raise forms.ValidationError('You must enter a length for all sessions')
elif data['joint_for_session'] == '3':
raise forms.ValidationError('The third session can not be the joint session, '
'because you have not requested a third session.')
return data

View file

@ -15,7 +15,9 @@ 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.secr.sreq.forms import SessionForm
from ietf.utils.mail import outbox, empty_outbox
from pyquery import PyQuery
@ -80,6 +82,9 @@ 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()
group3 = GroupFactory()
group4 = GroupFactory()
SessionFactory(meeting=meeting,group=mars,status_id='sched')
url = reverse('ietf.secr.sreq.views.edit', kwargs={'acronym':'mars'})
@ -92,10 +97,64 @@ class SessionRequestTestCase(TestCase):
'attendees':'10',
'conflict1':'',
'comments':'need lights',
'session_time_relation': 'subsequent-days',
'adjacent_with_wg': group2.acronym,
'joint_with_groups': group3.acronym + ' ' + group4.acronym,
'joint_for_session': '2',
'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'))
)
self.assertFalse(sessions[0].joint_with_groups.count())
self.assertEqual(list(sessions[1].joint_with_groups.all()), [group3, group4])
# 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)
self.assertContains(r, 'Second session with: {} {}'.format(group3.acronym, group4.acronym))
# Edit again, changing the joint sessions and clearing some fields. The behaviour of
# edit is different depending on whether previous joint sessions were recorded.
post_data = {'num_session':'2',
'length_session1':'3600',
'length_session2':'3600',
'attendees':'10',
'conflict1':'',
'comments':'need lights',
'joint_with_groups': group2.acronym,
'joint_for_session': '1',
'submit': 'Continue'}
r = self.client.post(url, post_data, HTTP_HOST='example.com')
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.assertFalse(session.constraints().filter(name='time_relation'))
self.assertFalse(session.constraints().filter(name='wg_adjacent'))
self.assertFalse(session.constraints().filter(name='timerange'))
self.assertEqual(list(sessions[0].joint_with_groups.all()), [group2])
self.assertFalse(sessions[1].joint_with_groups.count())
# Check whether the updated data is visible on the view page
r = self.client.get(redirect_url)
self.assertContains(r, 'First session with: {}'.format(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 +170,9 @@ 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)
group3 = GroupFactory(parent=area)
group4 = 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 +182,20 @@ class SubmitRequestCase(TestCase):
'attendees':'10',
'conflict1':'',
'comments':'need projector',
'adjacent_with_wg': group2.acronym,
'timeranges': ['thursday-afternoon-early', 'thursday-afternoon-late'],
'joint_with_groups': group3.acronym + ' ' + group4.acronym,
'joint_for_session': '1',
'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)
self.assertContains(r, 'First session with: {} {}'.format(group3.acronym, group4.acronym))
post_data['submit'] = 'Submit'
r = self.client.post(confirm_url,post_data)
self.assertRedirects(r, main_url)
@ -135,6 +207,15 @@ class SubmitRequestCase(TestCase):
self.assertRedirects(r, main_url)
session_count_after = Session.objects.filter(meeting=meeting, group=group, type='regular').count()
self.assertEqual(session_count_after, session_count_before + 1)
# Verify database content
session = Session.objects.get(meeting=meeting, group=group)
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'))
)
self.assertEqual(list(session.joint_with_groups.all()), [group3, group4])
def test_submit_request_invalid(self):
MeetingFactory(type_id='ietf', date=datetime.date.today())
@ -205,6 +286,8 @@ 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)
group3 = 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 +297,26 @@ 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,
'joint_with_groups': group3.acronym,
'joint_for_session': '2',
'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,11 +324,24 @@ 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(resource.desc in notification_payload)
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('Second session joint with: {}'.format(group3.acronym) in notification_payload)
self.assertTrue(ad.ascii_name() in notification_payload)
class LockAppTestCase(TestCase):
@ -311,9 +413,133 @@ class NotMeetingCase(TestCase):
class RetrievePreviousCase(TestCase):
pass
# test error if already scheduled
# test get previous exists/doesn't exist
# test that groups scheduled and unscheduled add up to total groups
# test access by unauthorized
class SessionFormTest(TestCase):
def setUp(self):
self.group1 = GroupFactory()
self.group2 = GroupFactory()
self.group3 = GroupFactory()
self.group4 = GroupFactory()
self.group5 = GroupFactory()
self.group6 = GroupFactory()
self.valid_form_data = {
'num_session': '2',
'third_session': 'true',
'length_session1': '3600',
'length_session2': '3600',
'length_session3': '3600',
'attendees': '10',
'conflict1': self.group2.acronym,
'conflict2': self.group3.acronym,
'conflict3': self.group4.acronym,
'comments': 'need lights',
'session_time_relation': 'subsequent-days',
'adjacent_with_wg': self.group5.acronym,
'joint_with_groups': self.group6.acronym,
'joint_for_session': '3',
'timeranges': ['thursday-afternoon-early', 'thursday-afternoon-late'],
'submit': 'Continue'
}
def test_valid(self):
# Test with three sessions
form = SessionForm(data=self.valid_form_data, group=self.group1)
self.assertTrue(form.is_valid())
# Test with two sessions
self.valid_form_data.update({
'length_session3': '',
'third_session': '',
'joint_for_session': '2'
})
form = SessionForm(data=self.valid_form_data, group=self.group1)
self.assertTrue(form.is_valid())
# Test with one session
self.valid_form_data.update({
'length_session2': '',
'num_session': 1,
'joint_for_session': '1',
'session_time_relation': '',
})
form = SessionForm(data=self.valid_form_data, group=self.group1)
self.assertTrue(form.is_valid())
def test_invalid_groups(self):
new_form_data = {
'conflict1': 'doesnotexist',
'conflict2': 'doesnotexist',
'conflict3': 'doesnotexist',
'adjacent_with_wg': 'doesnotexist',
'joint_with_groups': 'doesnotexist',
}
form = self._invalid_test_helper(new_form_data)
self.assertEqual(set(form.errors.keys()), set(new_form_data.keys()))
def test_invalid_group_appears_in_multiple_conflicts(self):
new_form_data = {
'conflict1': self.group2.acronym,
'conflict2': self.group2.acronym,
}
form = self._invalid_test_helper(new_form_data)
self.assertEqual(form.non_field_errors(), ['%s appears in conflicts more than once' % self.group2.acronym])
def test_invalid_conflict_with_self(self):
new_form_data = {
'conflict1': self.group1.acronym,
}
self._invalid_test_helper(new_form_data)
def test_invalid_session_time_relation(self):
form = self._invalid_test_helper({
'third_session': '',
'length_session2': '',
'num_session': 1,
'joint_for_session': '1',
})
self.assertEqual(form.non_field_errors(), ['Time between sessions can only be used when two '
'sessions are requested.'])
def test_invalid_joint_for_session(self):
form = self._invalid_test_helper({
'third_session': '',
'num_session': 2,
'joint_for_session': '3',
})
self.assertEqual(form.non_field_errors(), ['The third session can not be the joint session, '
'because you have not requested a third session.'])
form = self._invalid_test_helper({
'third_session': '',
'length_session2': '',
'num_session': 1,
'joint_for_session': '2',
'session_time_relation': '',
})
self.assertEqual(form.non_field_errors(), ['The second session can not be the joint session, '
'because you have not requested a second session.'])
def test_invalid_missing_session_length(self):
form = self._invalid_test_helper({
'length_session2': '',
'third_session': 'true',
})
self.assertEqual(form.non_field_errors(), ['You must enter a length for all sessions'])
form = self._invalid_test_helper({'length_session2': ''})
self.assertEqual(form.non_field_errors(), ['You must enter a length for all sessions'])
form = self._invalid_test_helper({'length_session3': ''})
self.assertEqual(form.non_field_errors(), ['You must enter a length for all sessions'])
def _invalid_test_helper(self, new_form_data):
form_data = dict(self.valid_form_data, **new_form_data)
form = SessionForm(data=form_data, group=self.group1)
self.assertFalse(form.is_valid())
return form

View file

@ -21,7 +21,7 @@ from ietf.meeting.models import Meeting, Session, Constraint, ResourceAssociatio
from ietf.meeting.helpers import get_meeting
from ietf.meeting.utils import add_event_info_to_session_qs
from ietf.name.models import SessionStatusName, ConstraintName
from ietf.secr.sreq.forms import SessionForm, ToolStatusForm, allowed_conflicting_groups
from ietf.secr.sreq.forms import SessionForm, ToolStatusForm, allowed_conflicting_groups, JOINT_FOR_SESSION_CHOICES
from ietf.secr.utils.decorators import check_permissions
from ietf.secr.utils.group import get_my_groups
from ietf.utils.mail import send_mail
@ -80,6 +80,19 @@ 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']]
for idx, session in enumerate(sessions):
if session.joint_with_groups.count():
initial['joint_with_groups'] = ' '.join(session.joint_with_groups_acronyms())
initial['joint_for_session'] = str(idx + 1)
initial['joint_for_session_display'] = dict(JOINT_FOR_SESSION_CHOICES)[initial['joint_for_session']]
return initial
def get_lock_message(meeting=None):
@ -165,7 +178,8 @@ def session_conflicts_as_string(group, meeting):
Takes a Group object and Meeting object and returns a string of other groups which have
a conflict with this one
'''
group_list = [ g.source.acronym for g in group.constraint_target_set.filter(meeting=meeting) ]
groups = group.constraint_target_set.filter(meeting=meeting, name__in=['conflict', 'conflic2', 'conflic3'])
group_list = [g.source.acronym for g in groups]
return ', '.join(group_list)
# -------------------------------------------------
@ -262,6 +276,12 @@ 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 session_data.get('joint_for_session'):
session_data['joint_for_session_display'] = dict(JOINT_FOR_SESSION_CHOICES)[session_data['joint_for_session']]
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', '')
@ -301,12 +321,27 @@ def confirm(request, acronym):
)
if 'resources' in form.data:
new_session.resources.set(session_data['resources'])
if int(form.data.get('joint_for_session', '-1')) == 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
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')
@ -445,7 +480,30 @@ def edit(request, acronym, num=None):
session.save()
session_changed(session)
# New sessions may have been created, refresh the sessions list
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 'joint_with_groups' in form.changed_data or 'joint_for_session' in form.changed_data:
joint_with_groups_list = form.cleaned_data.get('joint_with_groups').replace(',', ' ').split()
new_joint_with_groups = Group.objects.filter(acronym__in=joint_with_groups_list)
new_joint_for_session_idx = int(form.data.get('joint_for_session', '-1')) - 1
current_joint_for_session_idx = None
current_joint_with_groups = None
for idx, session in enumerate(sessions):
if session.joint_with_groups.count():
current_joint_for_session_idx = idx
current_joint_with_groups = session.joint_with_groups.all()
if current_joint_with_groups != new_joint_with_groups or current_joint_for_session_idx != new_joint_for_session_idx:
if current_joint_for_session_idx is not None:
sessions[current_joint_for_session_idx].joint_with_groups.clear()
session_changed(sessions[current_joint_for_session_idx])
sessions[new_joint_for_session_idx].joint_with_groups.set(new_joint_with_groups)
session_changed(sessions[new_joint_for_session_idx])
if 'attendees' in form.changed_data:
sessions.update(attendees=form.cleaned_data['attendees'])
if 'comments' in form.changed_data:
@ -459,6 +517,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 +533,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

@ -15,23 +15,34 @@ function stat_ls (val){
if (val == 0) {
document.form_post.length_session1.disabled = true;
document.form_post.length_session2.disabled = true;
document.form_post.length_session3.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_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.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;
document.form_post.length_session3.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;
}
}
@ -111,6 +122,15 @@ function delete_last3 () {
document.form_post.conflict3.value = b;
document.form_post.wg_selector3.selectedIndex=0;
}
function delete_last_joint_with_groups () {
var b = document.form_post.joint_with_groups.value;
var temp = new Array();
temp = b.split(' ');
temp.pop();
b = temp.join(' ');
document.form_post.joint_with_groups.value = b;
document.form_post.wg_selector4.selectedIndex=0;
}
// Not calling check_prior_confict (see ietf/secr/sreq/forms.py definition of SessionForm)
// while we are hacking the use of the current three constraint types around. We could bring

View file

@ -3,6 +3,7 @@
Working Group Name: {{ group.name }}
Area Name: {{ group.parent }}
Session Requester: {{ login }}
{% if session.joint_with_groups %}{{ session.joint_for_session_display }} joint with: {{ session.joint_with_groups }}{% endif %}
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 %}
@ -11,6 +12,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>
@ -49,7 +50,7 @@
</tr>
<tr>
<td colspan="2">BOF or IRTF Sessions:</td>
<td>Please enter free form requests in the Special Requests field below.</td>
<td>If the sessions can not be found in the fields above, please enter free form requests in the Special Requests field below.</td>
</tr>
</table>
</td>
@ -60,7 +61,34 @@
</td>
</tr>
<tr class="bg1">
<td valign="top">Special Requests:<br />&nbsp;<br />i.e. restrictions on meeting times / days, etc.<br /> (limit 200 characters)</td>
<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>
Joint session with:<br />
(To request one session for multiple WGs together.)
</td>
<td>{{ form.wg_selector4 }}
<input type="button" value="Delete the last entry" onClick="delete_last_joint_with_groups(); return 1;"><br>
{{ form.joint_with_groups.errors }}{{ form.joint_with_groups }}
</td>
</tr>
<tr class="bg1">
<td>
Of the sessions requested by this WG, the joint session, if applicable, is:
</td>
<td>{{ form.joint_for_session.errors }}{{ form.joint_for_session }}</td>
</tr>
<tr class="bg2">
<td valign="top">Special Requests:<br />&nbsp;<br />i.e. restrictions on meeting times / days, etc.</td> (limit 200 characters)</td>
<td>{{ form.comments.errors }}{{ form.comments }}</td>
</tr>
</table>

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>{% if session.session_time_relation_display %}{{ session.session_time_relation_display }}{% else %}No preference{% endif %}</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,5 +34,23 @@
<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>Special Requests:</td><td>{{ session.comments }}</td></tr>
<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>
<tr class="row2">
<td>Joint session:</td>
<td>
{% if session.joint_with_groups %}
{{ session.joint_for_session_display }} with: {{ session.joint_with_groups }}
{% else %}
Not a joint session
{% endif %}
</td>
</tr>
<tr class="row1"><td>Special Requests:</td><td>{{ session.comments }}</td></tr>
</table>

View file

@ -1225,12 +1225,30 @@ Session.prototype.generate_info_table = function() {
if(!read_only) {
$("#info_location").html(generate_select_box()+"<button id='info_location_set'>set</button>");
}
if("comments" in this && this.comments.length > 0 && this.comments != "None") {
$("#special_requests").text(this.comments);
} else {
$("#special_requests").text("Special requests: None");
var special_requests_text = '';
if(this.joint_with_groups) {
special_requests_text += 'Joint session with ' + this.joint_with_groups.join(', ') + '. ';
}
if(this.constraints.wg_adjacent) {
for (var target_href in this.constraints.wg_adjacent) {
if (this.constraints.wg_adjacent.hasOwnProperty(target_href)) {
special_requests_text += 'Schedule adjacent with ' + this.constraints.wg_adjacent[target_href].othergroup.acronym + '. ';
}
}
}
if(this.constraints.time_relation) {
special_requests_text += this.constraints.time_relation.time_relation.time_relation_display + '. ';
}
if(this.constraints.timerange) {
special_requests_text += this.constraints.timerange.timerange.timeranges_display + '. ';
}
if("comments" in this && this.comments.length > 0 && this.comments != "None") {
special_requests_text += this.comments;
} else {
special_requests_text += "Special requests: None";
}
$("#special_requests").text(special_requests_text);
this.selectit();
@ -1721,13 +1739,17 @@ Session.prototype.add_constraint_obj = function(obj) {
obj.person = person;
});
} else {
// must be conflic*
// must be conflic*, timerange, time_relation or wg_adjacent
var ogroupname;
if(obj.source_href == this.group_href) {
obj.thisgroup = this.group;
obj.othergroup = find_group_by_href(obj.target_href, "constraint src"+obj.href);
obj.direction = 'ours';
ogroupname = obj.target_href;
if (obj.target_href) {
ogroupname = obj.target_href;
} else {
ogroupname = obj.name;
}
if(this.constraints[listname][ogroupname]) {
console.log("Found multiple instances of",this.group_href,listname,ogroupname);
}

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>
@ -62,6 +62,9 @@
<a href="{% url "ietf.secr.sreq.views.edit" num=meeting.number acronym=session.group.acronym %}">
{{session.group.acronym}}
</a>
{% if session.joint_with_groups.count %}
joint with {{ session.joint_with_groups_acronyms|join:' ' }}
{% endif %}
</th>
<td class="text-right">