Allow configuration of group conflict types used for each meeting Fixes #2770. Commit ready for merge.
- Legacy-Id: 19266
This commit is contained in:
parent
ec86d98bb7
commit
336d762123
|
@ -9,7 +9,7 @@ import datetime
|
|||
from django.core.files.base import ContentFile
|
||||
|
||||
from ietf.meeting.models import Meeting, Session, SchedulingEvent, Schedule, TimeSlot, SessionPresentation, FloorPlan, Room, SlideSubmission
|
||||
from ietf.name.models import SessionStatusName
|
||||
from ietf.name.models import ConstraintName, SessionStatusName
|
||||
from ietf.group.factories import GroupFactory
|
||||
from ietf.person.factories import PersonFactory
|
||||
|
||||
|
@ -75,6 +75,24 @@ class MeetingFactory(factory.DjangoModelFactory):
|
|||
obj.schedule = ScheduleFactory(meeting=obj)
|
||||
obj.save()
|
||||
|
||||
@factory.post_generation
|
||||
def group_conflicts(obj, create, extracted, **kwargs): # pulint: disable=no-self-argument
|
||||
"""Add conflict types
|
||||
|
||||
Pass a list of ConflictNames as group_conflicts to specify which are enabled.
|
||||
"""
|
||||
if extracted is None:
|
||||
extracted = [
|
||||
ConstraintName.objects.get(slug=s) for s in [
|
||||
'chair_conflict', 'tech_overlap', 'key_participant'
|
||||
]]
|
||||
if create:
|
||||
for cn in extracted:
|
||||
obj.group_conflict_types.add(
|
||||
cn if isinstance(cn, ConstraintName) else ConstraintName.objects.get(slug=cn)
|
||||
)
|
||||
|
||||
|
||||
class SessionFactory(factory.DjangoModelFactory):
|
||||
class Meta:
|
||||
model = Session
|
||||
|
|
File diff suppressed because it is too large
Load diff
19
ietf/meeting/migrations/0042_meeting_group_conflict_types.py
Normal file
19
ietf/meeting/migrations/0042_meeting_group_conflict_types.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 2.2.20 on 2021-05-20 12:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('name', '0026_add_conflict_constraintnames'),
|
||||
('meeting', '0041_assign_correct_constraintnames'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='meeting',
|
||||
name='group_conflict_types',
|
||||
field=models.ManyToManyField(blank=True, limit_choices_to={'is_group_conflict': True}, help_text='Types of scheduling conflict between groups to consider', to='name.ConstraintName'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,42 @@
|
|||
# Generated by Django 2.2.20 on 2021-05-20 12:30
|
||||
|
||||
from django.db import migrations
|
||||
from django.db.models import IntegerField
|
||||
from django.db.models.functions import Cast
|
||||
|
||||
|
||||
def forward(apps, schema_editor):
|
||||
Meeting = apps.get_model('meeting', 'Meeting')
|
||||
ConstraintName = apps.get_model('name', 'ConstraintName')
|
||||
|
||||
# old for pre-106
|
||||
old_constraints = ConstraintName.objects.filter(slug__in=['conflict', 'conflic2', 'conflic3'])
|
||||
new_constraints = ConstraintName.objects.filter(slug__in=['chair_conflict', 'tech_overlap', 'key_participant'])
|
||||
|
||||
# get meetings with numeric 'number' field to avoid lexicographic ordering
|
||||
ietf_meetings = Meeting.objects.filter(
|
||||
type='ietf'
|
||||
).annotate(
|
||||
number_as_int=Cast('number', output_field=IntegerField())
|
||||
)
|
||||
|
||||
for mtg in ietf_meetings.filter(number_as_int__lt=106):
|
||||
for cn in old_constraints:
|
||||
mtg.group_conflict_types.add(cn)
|
||||
for mtg in ietf_meetings.filter(number_as_int__gte=106):
|
||||
for cn in new_constraints:
|
||||
mtg.group_conflict_types.add(cn)
|
||||
|
||||
def reverse(apps, schema_editor):
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('meeting', '0042_meeting_group_conflict_types'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(forward, reverse),
|
||||
]
|
|
@ -17,7 +17,7 @@ import debug # pyflakes:ignore
|
|||
|
||||
from django.core.validators import MinValueValidator, RegexValidator
|
||||
from django.db import models
|
||||
from django.db.models import Max, Subquery, OuterRef, TextField, Value
|
||||
from django.db.models import Max, Subquery, OuterRef, TextField, Value, Q
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.conf import settings
|
||||
# mostly used by json_dict()
|
||||
|
@ -111,6 +111,9 @@ class Meeting(models.Model):
|
|||
show_important_dates = models.BooleanField(default=False)
|
||||
attendees = models.IntegerField(blank=True, null=True, default=None,
|
||||
help_text="Number of Attendees for backfilled meetings, leave it blank for new meetings, and then it is calculated from the registrations")
|
||||
group_conflict_types = models.ManyToManyField(
|
||||
ConstraintName, blank=True, limit_choices_to=dict(is_group_conflict=True),
|
||||
help_text='Types of scheduling conflict between groups to consider')
|
||||
|
||||
def __str__(self):
|
||||
if self.type_id == "ietf":
|
||||
|
@ -197,6 +200,15 @@ class Meeting(models.Model):
|
|||
else:
|
||||
return self.date + datetime.timedelta(days=self.submission_correction_day_offset)
|
||||
|
||||
def enabled_constraint_names(self):
|
||||
return ConstraintName.objects.filter(
|
||||
Q(is_group_conflict=False) # any non-group-conflict constraints
|
||||
| Q(is_group_conflict=True, meeting=self) # or specifically enabled for this meeting
|
||||
)
|
||||
|
||||
def enabled_constraints(self):
|
||||
return self.constraint_set.filter(name__in=self.enabled_constraint_names())
|
||||
|
||||
def get_schedule_by_name(self, name):
|
||||
return self.schedule_set.filter(name=name).first()
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import shutil
|
|||
from unittest import skipIf
|
||||
from mock import patch
|
||||
from pyquery import PyQuery
|
||||
from lxml.etree import tostring
|
||||
from io import StringIO, BytesIO
|
||||
from bs4 import BeautifulSoup
|
||||
from urllib.parse import urlparse, urlsplit
|
||||
|
@ -1984,6 +1985,121 @@ class EditTests(TestCase):
|
|||
assignment.session.refresh_from_db()
|
||||
self.assertEqual(assignment.session.agenda_note, "New Test Note")
|
||||
|
||||
def test_edit_meeting_schedule_conflict_types(self):
|
||||
"""The meeting schedule editor should show the constraint types enabled for the meeting"""
|
||||
meeting = MeetingFactory(
|
||||
type_id='ietf',
|
||||
group_conflicts=[], # show none to start with
|
||||
)
|
||||
s1 = SessionFactory(
|
||||
meeting=meeting,
|
||||
type_id='regular',
|
||||
attendees=12,
|
||||
comments='chair conflict',
|
||||
)
|
||||
|
||||
s2 = SessionFactory(
|
||||
meeting=meeting,
|
||||
type_id='regular',
|
||||
attendees=34,
|
||||
comments='old-fashioned conflict',
|
||||
)
|
||||
|
||||
Constraint.objects.create(
|
||||
meeting=meeting,
|
||||
source=s1.group,
|
||||
target=s2.group,
|
||||
name=ConstraintName.objects.get(slug="chair_conflict"),
|
||||
)
|
||||
|
||||
Constraint.objects.create(
|
||||
meeting=meeting,
|
||||
source=s2.group,
|
||||
target=s1.group,
|
||||
name=ConstraintName.objects.get(slug="conflict"),
|
||||
)
|
||||
|
||||
|
||||
# log in as secretary so we have access
|
||||
self.client.login(username="secretary", password="secretary+password")
|
||||
|
||||
url = urlreverse("ietf.meeting.views.edit_meeting_schedule", kwargs=dict(num=meeting.number))
|
||||
|
||||
# Should have no conflict constraints listed because the meeting has all disabled
|
||||
r = self.client.get(url)
|
||||
q = PyQuery(r.content)
|
||||
|
||||
self.assertEqual(len(q('#session{} span.constraints > span'.format(s1.pk))), 0)
|
||||
self.assertEqual(len(q('#session{} span.constraints > span'.format(s2.pk))), 0)
|
||||
|
||||
# Now enable the 'chair_conflict' constraint only
|
||||
chair_conflict = ConstraintName.objects.get(slug='chair_conflict')
|
||||
chair_conf_label = b'<i class="fa fa-gavel"/>' # result of etree.tostring(etree.fromstring(editor_label))
|
||||
meeting.group_conflict_types.add(chair_conflict)
|
||||
r = self.client.get(url)
|
||||
q = PyQuery(r.content)
|
||||
|
||||
# verify that there is a constraint pointing from 1 to 2
|
||||
#
|
||||
# The constraint is represented in the HTML as
|
||||
# <div id="session<pk>">
|
||||
# [...]
|
||||
# <span class="constraints">
|
||||
# <span data-sessions="<other pk>">[constraint label]</span>
|
||||
# </span>
|
||||
# </div>
|
||||
#
|
||||
# Where the constraint label is the editor_label for the ConstraintName.
|
||||
# If this pk is the constraint target, the editor_label includes a
|
||||
# '-' prefix, which may be before the editor_label or inserted inside
|
||||
# it.
|
||||
#
|
||||
# For simplicity, this test is tied to the current values of editor_label.
|
||||
# It also assumes the order of constraints will be constant.
|
||||
# If those change, the test will need to be updated.
|
||||
s1_constraints = q('#session{} span.constraints > span'.format(s1.pk))
|
||||
s2_constraints = q('#session{} span.constraints > span'.format(s2.pk))
|
||||
|
||||
# Check the forward constraint
|
||||
self.assertEqual(len(s1_constraints), 1)
|
||||
self.assertEqual(s1_constraints[0].attrib['data-sessions'], str(s2.pk))
|
||||
self.assertEqual(s1_constraints[0].text, None) # no '-' prefix on the source
|
||||
self.assertEqual(tostring(s1_constraints[0][0]), chair_conf_label) # [0][0] is the innermost <span>
|
||||
|
||||
# And the reverse constraint
|
||||
self.assertEqual(len(s2_constraints), 1)
|
||||
self.assertEqual(s2_constraints[0].attrib['data-sessions'], str(s1.pk))
|
||||
self.assertEqual(s2_constraints[0].text, '-') # '-' prefix on the target
|
||||
self.assertEqual(tostring(s2_constraints[0][0]), chair_conf_label) # [0][0] is the innermost <span>
|
||||
|
||||
# Now also enable the 'conflict' constraint
|
||||
conflict = ConstraintName.objects.get(slug='conflict')
|
||||
conf_label = b'<span class="encircled">1</span>'
|
||||
conf_label_reversed = b'<span class="encircled">-1</span>' # the '-' is inside the span!
|
||||
meeting.group_conflict_types.add(conflict)
|
||||
r = self.client.get(url)
|
||||
q = PyQuery(r.content)
|
||||
|
||||
s1_constraints = q('#session{} span.constraints > span'.format(s1.pk))
|
||||
s2_constraints = q('#session{} span.constraints > span'.format(s2.pk))
|
||||
|
||||
# Check the forward constraint
|
||||
self.assertEqual(len(s1_constraints), 2)
|
||||
self.assertEqual(s1_constraints[0].attrib['data-sessions'], str(s2.pk))
|
||||
self.assertEqual(s1_constraints[0].text, None) # no '-' prefix on the source
|
||||
self.assertEqual(tostring(s1_constraints[0][0]), chair_conf_label) # [0][0] is the innermost <span>
|
||||
|
||||
self.assertEqual(s1_constraints[1].attrib['data-sessions'], str(s2.pk))
|
||||
self.assertEqual(tostring(s1_constraints[1][0]), conf_label_reversed) # [0][0] is the innermost <span>
|
||||
|
||||
# And the reverse constraint
|
||||
self.assertEqual(len(s2_constraints), 2)
|
||||
self.assertEqual(s2_constraints[0].attrib['data-sessions'], str(s1.pk))
|
||||
self.assertEqual(s2_constraints[0].text, '-') # '-' prefix on the target
|
||||
self.assertEqual(tostring(s2_constraints[0][0]), chair_conf_label) # [0][0] is the innermost <span>
|
||||
|
||||
self.assertEqual(s2_constraints[1].attrib['data-sessions'], str(s1.pk))
|
||||
self.assertEqual(tostring(s2_constraints[1][0]), conf_label) # [0][0] is the innermost <span>
|
||||
|
||||
def test_new_meeting_schedule(self):
|
||||
meeting = make_meeting_test_data()
|
||||
|
|
|
@ -296,7 +296,7 @@ def reverse_editor_label(label):
|
|||
def preprocess_constraints_for_meeting_schedule_editor(meeting, sessions):
|
||||
# process constraint names - we synthesize extra names to be able
|
||||
# to treat the concepts in the same manner as the modelled ones
|
||||
constraint_names = {n.pk: n for n in ConstraintName.objects.all()}
|
||||
constraint_names = {n.pk: n for n in meeting.enabled_constraint_names()}
|
||||
|
||||
joint_with_groups_constraint_name = ConstraintName(
|
||||
slug='joint_with_groups',
|
||||
|
@ -327,7 +327,7 @@ def preprocess_constraints_for_meeting_schedule_editor(meeting, sessions):
|
|||
n.countless_formatted_editor_label = format_html(n.formatted_editor_label, count="") if "{count}" in n.formatted_editor_label else n.formatted_editor_label
|
||||
|
||||
# convert human-readable rules in the database to constraints on actual sessions
|
||||
constraints = list(Constraint.objects.filter(meeting=meeting).prefetch_related('target', 'person', 'timeranges'))
|
||||
constraints = list(meeting.enabled_constraints().prefetch_related('target', 'person', 'timeranges'))
|
||||
|
||||
# synthesize AD constraints - we can treat them as a special kind of 'bethere'
|
||||
responsible_ad_for_group = {}
|
||||
|
|
|
@ -99,7 +99,9 @@ class MeetingModelForm(forms.ModelForm):
|
|||
class Meta:
|
||||
model = Meeting
|
||||
exclude = ('type', 'schedule', 'session_request_lock_message')
|
||||
|
||||
widgets = {
|
||||
'group_conflict_types': forms.CheckboxSelectMultiple(),
|
||||
}
|
||||
|
||||
def __init__(self,*args,**kwargs):
|
||||
super(MeetingModelForm, self).__init__(*args,**kwargs)
|
||||
|
@ -118,6 +120,9 @@ class MeetingModelForm(forms.ModelForm):
|
|||
meeting.type_id = 'ietf'
|
||||
if commit:
|
||||
meeting.save()
|
||||
# must call save_m2m() because we saved with commit=False above, see:
|
||||
# https://docs.djangoproject.com/en/2.2/topics/forms/modelforms/#the-save-method
|
||||
self.save_m2m()
|
||||
return meeting
|
||||
|
||||
class MeetingRoomForm(forms.ModelForm):
|
||||
|
|
|
@ -14,9 +14,10 @@ from django.conf import settings
|
|||
from django.urls import reverse
|
||||
|
||||
from ietf.group.models import Group, GroupEvent
|
||||
from ietf.meeting.factories import MeetingFactory
|
||||
from ietf.meeting.models import Meeting, Room, TimeSlot, SchedTimeSessAssignment, Session, SchedulingEvent
|
||||
from ietf.meeting.test_data import make_meeting_test_data
|
||||
from ietf.name.models import SessionStatusName
|
||||
from ietf.name.models import ConstraintName, SessionStatusName
|
||||
from ietf.person.models import Person
|
||||
from ietf.secr.meetings.forms import get_times
|
||||
from ietf.utils.mail import outbox
|
||||
|
@ -73,18 +74,21 @@ class SecrMeetingTestCase(TestCase):
|
|||
number = int(meeting.number) + 1
|
||||
count = Meeting.objects.count()
|
||||
url = reverse('ietf.secr.meetings.views.add')
|
||||
post_data = dict(number=number,city='Toronto',date='2014-07-20',country='CA',
|
||||
time_zone='America/New_York',venue_name='Hilton',
|
||||
days=6,
|
||||
venue_addr='100 First Ave',
|
||||
idsubmit_cutoff_day_offset_00=13,
|
||||
idsubmit_cutoff_day_offset_01=20,
|
||||
idsubmit_cutoff_time_utc =datetime.timedelta(hours=23, minutes=59, seconds=59),
|
||||
idsubmit_cutoff_warning_days =datetime.timedelta(days=21),
|
||||
submission_start_day_offset=90,
|
||||
submission_cutoff_day_offset=26,
|
||||
submission_correction_day_offset=50,
|
||||
)
|
||||
post_data = dict(
|
||||
number=number,city='Toronto',date='2014-07-20',country='CA',
|
||||
time_zone='America/New_York',venue_name='Hilton',
|
||||
days=6,
|
||||
venue_addr='100 First Ave',
|
||||
idsubmit_cutoff_day_offset_00=13,
|
||||
idsubmit_cutoff_day_offset_01=20,
|
||||
idsubmit_cutoff_time_utc =datetime.timedelta(hours=23, minutes=59, seconds=59),
|
||||
idsubmit_cutoff_warning_days =datetime.timedelta(days=21),
|
||||
submission_start_day_offset=90,
|
||||
submission_cutoff_day_offset=26,
|
||||
submission_correction_day_offset=50,
|
||||
group_conflict_types=('conflict', 'conflic2', 'key_participant'),
|
||||
)
|
||||
|
||||
self.client.login(username='secretary', password='secretary+password')
|
||||
response = self.client.post(url, post_data, follow=True)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
@ -96,6 +100,38 @@ class SecrMeetingTestCase(TestCase):
|
|||
self.assertTrue(new_meeting.schedule.base)
|
||||
self.assertEqual(new_meeting.schedule.base.name, 'base')
|
||||
self.assertEqual(new_meeting.attendees, None)
|
||||
self.assertCountEqual(
|
||||
[cn.slug for cn in new_meeting.group_conflict_types.all()],
|
||||
post_data['group_conflict_types'],
|
||||
)
|
||||
|
||||
def test_add_meeting_default_conflict_types(self):
|
||||
"""Add meeting should default to same conflict types as previous meeting"""
|
||||
def _run_test(mtg):
|
||||
url = reverse('ietf.secr.meetings.views.add')
|
||||
r = self.client.get(url)
|
||||
q = PyQuery(r.content)
|
||||
selected_items = q('#id_group_conflict_types input[checked]')
|
||||
selected_values = [si.value for si in selected_items]
|
||||
expected_values = [cn.slug for cn in mtg.group_conflict_types.all()]
|
||||
self.assertCountEqual(selected_values, expected_values)
|
||||
|
||||
self.client.login(username='secretary', password='secretary+password')
|
||||
|
||||
meeting = MeetingFactory(type_id='ietf', group_conflicts=[]) # start with no conflicts selected
|
||||
_run_test(meeting)
|
||||
|
||||
# enable one
|
||||
meeting.group_conflict_types.add(ConstraintName.objects.filter(is_group_conflict=True).first())
|
||||
self.assertEqual(meeting.group_conflict_types.count(), 1)
|
||||
_run_test(meeting)
|
||||
|
||||
# enable a few ([::2] selects every other)
|
||||
meeting.group_conflict_types.clear()
|
||||
for cn in ConstraintName.objects.filter(is_group_conflict=True)[::2]:
|
||||
meeting.group_conflict_types.add(cn)
|
||||
self.assertGreater(meeting.group_conflict_types.count(), 1)
|
||||
_run_test(meeting)
|
||||
|
||||
def test_edit_meeting(self):
|
||||
"Edit Meeting"
|
||||
|
@ -111,13 +147,25 @@ class SecrMeetingTestCase(TestCase):
|
|||
submission_cutoff_day_offset=26,
|
||||
submission_correction_day_offset=50,
|
||||
attendees=1234,
|
||||
group_conflict_types=[
|
||||
cn.slug for cn in ConstraintName.objects.filter(
|
||||
is_group_conflict=True
|
||||
).exclude(
|
||||
meeting=meeting, # replace original set with those not assigned to the meeting
|
||||
)
|
||||
]
|
||||
)
|
||||
self.assertGreater(len(post_data['group_conflict_types']), 0) # test should include at least one
|
||||
self.client.login(username="secretary", password="secretary+password")
|
||||
response = self.client.post(url, post_data, follow=True)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
meeting = Meeting.objects.get(number=meeting.number)
|
||||
self.assertEqual(meeting.city,'Toronto')
|
||||
self.assertEqual(meeting.attendees,1234)
|
||||
self.assertCountEqual(
|
||||
[cn.slug for cn in meeting.group_conflict_types.all()],
|
||||
post_data['group_conflict_types'],
|
||||
)
|
||||
|
||||
def test_blue_sheets_upload(self):
|
||||
"Test Bluesheets"
|
||||
|
|
|
@ -7,7 +7,8 @@ import time
|
|||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.db.models import Max
|
||||
from django.db.models import IntegerField
|
||||
from django.db.models.functions import Cast
|
||||
from django.forms.models import inlineformset_factory
|
||||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from django.utils.text import slugify
|
||||
|
@ -249,8 +250,17 @@ def add(request):
|
|||
return redirect('ietf.secr.meetings.views.main')
|
||||
else:
|
||||
# display initial forms
|
||||
max_number = Meeting.objects.filter(type='ietf').aggregate(Max('number'))['number__max']
|
||||
form = MeetingModelForm(initial={'number':int(max_number) + 1})
|
||||
last_ietf_meeting = Meeting.objects.filter(
|
||||
type='ietf'
|
||||
).annotate(
|
||||
number_as_int=Cast('number', output_field=IntegerField())
|
||||
).order_by('-number_as_int').first()
|
||||
initial = dict()
|
||||
# fill in defaults if we can
|
||||
if last_ietf_meeting is not None:
|
||||
initial['number'] = last_ietf_meeting.number_as_int + 1
|
||||
initial['group_conflict_types'] = [cn.pk for cn in last_ietf_meeting.group_conflict_types.all()]
|
||||
form = MeetingModelForm(initial=initial)
|
||||
|
||||
return render(request, 'meetings/add.html', {
|
||||
'form': form},
|
||||
|
|
|
@ -6,7 +6,7 @@ from django import forms
|
|||
|
||||
import debug # pyflakes:ignore
|
||||
|
||||
from ietf.name.models import TimerangeName
|
||||
from ietf.name.models import TimerangeName, ConstraintName
|
||||
from ietf.group.models import Group
|
||||
from ietf.meeting.models import ResourceAssociation, Constraint
|
||||
from ietf.person.fields import SearchablePersonsField
|
||||
|
@ -106,7 +106,7 @@ class SessionForm(forms.Form):
|
|||
|
||||
# Set up constraints for the meeting
|
||||
self._wg_field_data = []
|
||||
for constraintname in meeting.session_constraintnames.all():
|
||||
for constraintname in meeting.group_conflict_types.all():
|
||||
# two fields for each constraint: a CharField for the group list and a selector to add entries
|
||||
constraint_field = forms.CharField(max_length=255, required=False)
|
||||
constraint_field.widget.attrs['data-slug'] = constraintname.slug
|
||||
|
@ -126,6 +126,27 @@ class SessionForm(forms.Form):
|
|||
self.fields[cselector_id] = selector_field
|
||||
self._wg_field_data.append((constraintname, cfield_id, cselector_id))
|
||||
|
||||
# Show constraints that are not actually used by the meeting so these don't get lost
|
||||
self._inactive_wg_field_data = []
|
||||
inactive_cnames = ConstraintName.objects.filter(
|
||||
is_group_conflict=True # Only collect group conflicts...
|
||||
).exclude(
|
||||
meeting=meeting # ...that are not enabled for this meeting...
|
||||
).filter(
|
||||
constraint__source=group, # ...but exist for this group...
|
||||
constraint__meeting=meeting, # ... at this meeting.
|
||||
).distinct()
|
||||
|
||||
for inactive_constraint_name in inactive_cnames:
|
||||
field_id = 'delete_{}'.format(inactive_constraint_name.slug)
|
||||
self.fields[field_id] = forms.BooleanField(required=False, label='Delete this conflict', help_text='Delete this inactive conflict?')
|
||||
constraints = group.constraint_source_set.filter(meeting=meeting, name=inactive_constraint_name)
|
||||
self._inactive_wg_field_data.append(
|
||||
(inactive_constraint_name,
|
||||
' '.join([c.target.acronym for c in constraints]),
|
||||
field_id)
|
||||
)
|
||||
|
||||
self.fields['joint_with_groups_selector'].widget.attrs['onChange'] = "document.form_post.joint_with_groups.value=document.form_post.joint_with_groups.value + ' ' + this.options[this.selectedIndex].value; return 1;"
|
||||
self.fields['third_session'].widget.attrs['onClick'] = "if (document.form_post.num_session.selectedIndex < 2) { alert('Cannot use this field - Number of Session is not set to 2'); return false; } else { if (this.checked==true) { document.form_post.length_session3.disabled=false; } else { document.form_post.length_session3.value=0;document.form_post.length_session3.disabled=true; } }"
|
||||
self.fields["resources"].choices = [(x.pk,x.desc) for x in ResourceAssociation.objects.filter(name__used=True).order_by('name__order') ]
|
||||
|
@ -150,11 +171,27 @@ class SessionForm(forms.Form):
|
|||
for cname, cfield_id, cselector_id in self._wg_field_data:
|
||||
yield cname, self[cfield_id], self[cselector_id]
|
||||
|
||||
def wg_constraint_count(self):
|
||||
"""How many wg constraints are there?"""
|
||||
return len(self._wg_field_data)
|
||||
|
||||
def wg_constraint_field_ids(self):
|
||||
"""Iterates over wg constraint field IDs"""
|
||||
for cname, cfield_id, _ in self._wg_field_data:
|
||||
yield cname, cfield_id
|
||||
|
||||
def inactive_wg_constraints(self):
|
||||
for cname, value, field_id in self._inactive_wg_field_data:
|
||||
yield cname, value, self[field_id]
|
||||
|
||||
def inactive_wg_constraint_count(self):
|
||||
return len(self._inactive_wg_field_data)
|
||||
|
||||
def inactive_wg_constraint_field_ids(self):
|
||||
"""Iterates over wg constraint field IDs"""
|
||||
for cname, _, field_id in self._inactive_wg_field_data:
|
||||
yield cname, field_id
|
||||
|
||||
@staticmethod
|
||||
def _add_widget_class(widget, new_class):
|
||||
"""Add a new class, taking care in case some already exist"""
|
||||
|
|
|
@ -12,7 +12,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.name.models import ConstraintName, TimerangeName
|
||||
from ietf.person.models import Person
|
||||
from ietf.secr.sreq.forms import SessionForm
|
||||
from ietf.utils.mail import outbox, empty_outbox, get_payload_text
|
||||
|
@ -94,7 +94,7 @@ class SessionRequestTestCase(TestCase):
|
|||
'length_session1':'3600',
|
||||
'length_session2':'3600',
|
||||
'attendees':'10',
|
||||
'constraint_conflict': iabprog.acronym,
|
||||
'constraint_chair_conflict':iabprog.acronym,
|
||||
'comments':'need lights',
|
||||
'session_time_relation': 'subsequent-days',
|
||||
'adjacent_with_wg': group2.acronym,
|
||||
|
@ -111,7 +111,7 @@ class SessionRequestTestCase(TestCase):
|
|||
self.assertEqual(len(sessions), 2)
|
||||
session = sessions[0]
|
||||
|
||||
self.assertEqual(session.constraints().get(name='conflict').target.acronym, iabprog.acronym)
|
||||
self.assertEqual(session.constraints().get(name='chair_conflict').target.acronym, iabprog.acronym)
|
||||
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(
|
||||
|
@ -134,7 +134,7 @@ class SessionRequestTestCase(TestCase):
|
|||
'length_session1':'3600',
|
||||
'length_session2':'3600',
|
||||
'attendees':'10',
|
||||
'constraint_conflict':'',
|
||||
'constraint_chair_conflict':'',
|
||||
'comments':'need lights',
|
||||
'joint_with_groups': group2.acronym,
|
||||
'joint_for_session': '1',
|
||||
|
@ -156,6 +156,50 @@ class SessionRequestTestCase(TestCase):
|
|||
r = self.client.get(redirect_url)
|
||||
self.assertContains(r, 'First session with: {}'.format(group2.acronym))
|
||||
|
||||
def test_edit_inactive_conflicts(self):
|
||||
"""Inactive conflicts should be displayed and removable"""
|
||||
meeting = MeetingFactory(type_id='ietf', date=datetime.date.today(), group_conflicts=['chair_conflict'])
|
||||
mars = RoleFactory(name_id='chair', person__user__username='marschairman', group__acronym='mars').group
|
||||
SessionFactory(meeting=meeting, group=mars, status_id='sched')
|
||||
other_group = GroupFactory()
|
||||
Constraint.objects.create(
|
||||
meeting=meeting,
|
||||
name_id='conflict', # not in group_conflicts for the meeting
|
||||
source=mars,
|
||||
target=other_group,
|
||||
)
|
||||
|
||||
url = reverse('ietf.secr.sreq.views.edit', kwargs=dict(acronym='mars'))
|
||||
self.client.login(username='marschairman', password='marschairman+password')
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
q = PyQuery(r.content)
|
||||
|
||||
# check that the inactive session is displayed
|
||||
found = q('input#id_delete_conflict[type="checkbox"]')
|
||||
self.assertEqual(len(found), 1)
|
||||
delete_checkbox = found[0]
|
||||
# check that the label on the checkbox is correct
|
||||
self.assertIn('Delete this conflict', delete_checkbox.tail)
|
||||
# check that the target is displayed correctly in the UI
|
||||
self.assertIn(other_group.acronym, delete_checkbox.find('../input[@type="text"]').value)
|
||||
|
||||
post_data = {
|
||||
'num_session': '1',
|
||||
'length_session1': '3600',
|
||||
'attendees': '10',
|
||||
'constraint_chair_conflict':'',
|
||||
'comments':'',
|
||||
'joint_with_groups': '',
|
||||
'joint_for_session': '',
|
||||
'submit': 'Save',
|
||||
'delete_conflict': 'on',
|
||||
}
|
||||
r = self.client.post(url, post_data, HTTP_HOST='example.com')
|
||||
redirect_url = reverse('ietf.secr.sreq.views.view', kwargs={'acronym': 'mars'})
|
||||
self.assertRedirects(r, redirect_url)
|
||||
self.assertEqual(len(mars.constraint_source_set.filter(name_id='conflict')), 0)
|
||||
|
||||
def test_tool_status(self):
|
||||
MeetingFactory(type_id='ietf', date=datetime.date.today())
|
||||
url = reverse('ietf.secr.sreq.views.tool_status')
|
||||
|
@ -166,38 +210,34 @@ class SessionRequestTestCase(TestCase):
|
|||
self.assertRedirects(r,reverse('ietf.secr.sreq.views.main'))
|
||||
|
||||
def test_new_req_constraint_types(self):
|
||||
"""ITEF meetings 106 and later use different constraint types
|
||||
"""Configurable constraint types should be handled correctly in a new request
|
||||
|
||||
Relies on SessionForm representing constraint values with element IDs
|
||||
like id_constraint_<ConstraintName slug>
|
||||
"""
|
||||
should_have_pre106 = ['conflict', 'conflic2', 'conflic3']
|
||||
should_have = ['chair_conflict', 'tech_overlap', 'key_participant']
|
||||
|
||||
meeting = MeetingFactory(type_id='ietf', date=datetime.date.today())
|
||||
RoleFactory(name_id='chair', person__user__username='marschairman', group__acronym='mars')
|
||||
url = reverse('ietf.secr.sreq.views.new', kwargs=dict(acronym='mars'))
|
||||
self.client.login(username="marschairman", password="marschairman+password")
|
||||
|
||||
for meeting_number in ['95', '100', '105', '106', '111', '125']:
|
||||
meeting.number = meeting_number
|
||||
meeting.save()
|
||||
for expected in [
|
||||
['conflict', 'conflic2', 'conflic3'],
|
||||
['chair_conflict', 'tech_overlap', 'key_participant'],
|
||||
]:
|
||||
meeting.group_conflict_types.clear()
|
||||
for slug in expected:
|
||||
meeting.group_conflict_types.add(ConstraintName.objects.get(slug=slug))
|
||||
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
q = PyQuery(r.content)
|
||||
expected = should_have if int(meeting.number) >= 106 else should_have_pre106
|
||||
self.assertCountEqual(
|
||||
[elt.attr('id') for elt in q.items('*[id^=id_constraint_]')],
|
||||
['id_constraint_{}'.format(conf_name) for conf_name in expected],
|
||||
'Unexpected constraints for meeting number {}'.format(meeting_number),
|
||||
)
|
||||
|
||||
def test_edit_req_constraint_types(self):
|
||||
"""Editing a request constraint should show the expected constraints"""
|
||||
should_have_pre106 = ['conflict', 'conflic2', 'conflic3']
|
||||
should_have = ['chair_conflict', 'tech_overlap', 'key_participant']
|
||||
|
||||
meeting = MeetingFactory(type_id='ietf', date=datetime.date.today())
|
||||
SessionFactory(group__acronym='mars',
|
||||
status_id='schedw',
|
||||
|
@ -208,18 +248,20 @@ class SessionRequestTestCase(TestCase):
|
|||
url = reverse('ietf.secr.sreq.views.edit', kwargs=dict(acronym='mars'))
|
||||
self.client.login(username='marschairman', password='marschairman+password')
|
||||
|
||||
for meeting_number in ['95', '100', '105', '106', '111', '125']:
|
||||
meeting.number = meeting_number
|
||||
meeting.save()
|
||||
for expected in [
|
||||
['conflict', 'conflic2', 'conflic3'],
|
||||
['chair_conflict', 'tech_overlap', 'key_participant'],
|
||||
]:
|
||||
meeting.group_conflict_types.clear()
|
||||
for slug in expected:
|
||||
meeting.group_conflict_types.add(ConstraintName.objects.get(slug=slug))
|
||||
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
q = PyQuery(r.content)
|
||||
expected = should_have if int(meeting.number) >= 106 else should_have_pre106
|
||||
self.assertCountEqual(
|
||||
[elt.attr('id') for elt in q.items('*[id^=id_constraint_]')],
|
||||
['id_constraint_{}'.format(conf_name) for conf_name in expected],
|
||||
'Unexpected constraints for meeting number {}'.format(meeting_number),
|
||||
)
|
||||
|
||||
class SubmitRequestCase(TestCase):
|
||||
|
@ -244,7 +286,7 @@ class SubmitRequestCase(TestCase):
|
|||
post_data = {'num_session':'1',
|
||||
'length_session1':'3600',
|
||||
'attendees':'10',
|
||||
'constraint_conflict':'',
|
||||
'constraint_chair_conflict':'',
|
||||
'comments':'need projector',
|
||||
'adjacent_with_wg': group2.acronym,
|
||||
'timeranges': ['thursday-afternoon-early', 'thursday-afternoon-late'],
|
||||
|
@ -290,7 +332,7 @@ class SubmitRequestCase(TestCase):
|
|||
post_data = {'num_session':'2',
|
||||
'length_session1':'3600',
|
||||
'attendees':'10',
|
||||
'constraint_conflict':'',
|
||||
'constraint_chair_conflict':'',
|
||||
'comments':'need projector'}
|
||||
self.client.login(username="secretary", password="secretary+password")
|
||||
r = self.client.post(url,post_data)
|
||||
|
@ -301,7 +343,8 @@ class SubmitRequestCase(TestCase):
|
|||
|
||||
def test_submit_request_check_constraints(self):
|
||||
m1 = MeetingFactory(type_id='ietf', date=datetime.date.today() - datetime.timedelta(days=100))
|
||||
MeetingFactory(type_id='ietf', date=datetime.date.today())
|
||||
MeetingFactory(type_id='ietf', date=datetime.date.today(),
|
||||
group_conflicts=['chair_conflict', 'conflic2', 'conflic3'])
|
||||
ad = Person.objects.get(user__username='ad')
|
||||
area = RoleFactory(name_id='ad', person=ad, group__type_id='area').group
|
||||
group = GroupFactory(parent=area)
|
||||
|
@ -310,7 +353,7 @@ class SubmitRequestCase(TestCase):
|
|||
meeting=m1,
|
||||
source=group,
|
||||
target=still_active_group,
|
||||
name_id='conflict',
|
||||
name_id='chair_conflict',
|
||||
)
|
||||
inactive_group = GroupFactory(parent=area, state_id='conclude')
|
||||
inactive_group.save()
|
||||
|
@ -318,7 +361,7 @@ class SubmitRequestCase(TestCase):
|
|||
meeting=m1,
|
||||
source=group,
|
||||
target=inactive_group,
|
||||
name_id='conflict',
|
||||
name_id='chair_conflict',
|
||||
)
|
||||
SessionFactory(group=group, meeting=m1)
|
||||
|
||||
|
@ -328,14 +371,14 @@ class SubmitRequestCase(TestCase):
|
|||
r = self.client.get(url + '?previous')
|
||||
self.assertEqual(r.status_code, 200)
|
||||
q = PyQuery(r.content)
|
||||
conflict1 = q('[name="constraint_conflict"]').val()
|
||||
conflict1 = q('[name="constraint_chair_conflict"]').val()
|
||||
self.assertIn(still_active_group.acronym, conflict1)
|
||||
self.assertNotIn(inactive_group.acronym, conflict1)
|
||||
|
||||
post_data = {'num_session':'1',
|
||||
'length_session1':'3600',
|
||||
'attendees':'10',
|
||||
'constraint_conflict': group.acronym,
|
||||
'constraint_chair_conflict': group.acronym,
|
||||
'comments':'need projector',
|
||||
'submit': 'Continue'}
|
||||
r = self.client.post(url,post_data)
|
||||
|
@ -366,7 +409,7 @@ class SubmitRequestCase(TestCase):
|
|||
'length_session2':'3600',
|
||||
'attendees':'10',
|
||||
'bethere':str(ad.pk),
|
||||
'constraint_conflict':'',
|
||||
'constraint_chair_conflict':'',
|
||||
'comments':'',
|
||||
'resources': resource.pk,
|
||||
'session_time_relation': 'subsequent-days',
|
||||
|
@ -485,10 +528,6 @@ class RetrievePreviousCase(TestCase):
|
|||
|
||||
class SessionFormTest(TestCase):
|
||||
def setUp(self):
|
||||
# Ensure meeting numbers are predictable. Temporarily needed while basing
|
||||
# constraint types on meeting number, expected to go away when #2770 is resolved.
|
||||
MeetingFactory.reset_sequence(0)
|
||||
|
||||
self.meeting = MeetingFactory(type_id='ietf')
|
||||
self.group1 = GroupFactory()
|
||||
self.group2 = GroupFactory()
|
||||
|
@ -504,9 +543,9 @@ class SessionFormTest(TestCase):
|
|||
'length_session2': '3600',
|
||||
'length_session3': '3600',
|
||||
'attendees': '10',
|
||||
'constraint_conflict': self.group2.acronym,
|
||||
'constraint_conflic2': self.group3.acronym,
|
||||
'constraint_conflic3': self.group4.acronym,
|
||||
'constraint_chair_conflict': self.group2.acronym,
|
||||
'constraint_tech_overlap': self.group3.acronym,
|
||||
'constraint_key_participant': self.group4.acronym,
|
||||
'comments': 'need lights',
|
||||
'session_time_relation': 'subsequent-days',
|
||||
'adjacent_with_wg': self.group5.acronym,
|
||||
|
@ -542,16 +581,30 @@ class SessionFormTest(TestCase):
|
|||
|
||||
def test_invalid_groups(self):
|
||||
new_form_data = {
|
||||
'constraint_conflict': 'doesnotexist',
|
||||
'constraint_conflic2': 'doesnotexist',
|
||||
'constraint_conflic3': 'doesnotexist',
|
||||
'constraint_chair_conflict': 'doesnotexist',
|
||||
'constraint_tech_overlap': 'doesnotexist',
|
||||
'constraint_key_participant': '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_valid_group_appears_in_multiple_conflicts(self):
|
||||
"""Some conflict types allow overlapping groups"""
|
||||
new_form_data = {
|
||||
'constraint_chair_conflict': self.group2.acronym,
|
||||
'constraint_tech_overlap': self.group2.acronym,
|
||||
}
|
||||
self.valid_form_data.update(new_form_data)
|
||||
form = SessionForm(data=self.valid_form_data, group=self.group1, meeting=self.meeting)
|
||||
self.assertTrue(form.is_valid())
|
||||
|
||||
def test_invalid_group_appears_in_multiple_conflicts(self):
|
||||
"""Some conflict types do not allow overlapping groups"""
|
||||
self.meeting.group_conflict_types.clear()
|
||||
self.meeting.group_conflict_types.add(ConstraintName.objects.get(slug='conflict'))
|
||||
self.meeting.group_conflict_types.add(ConstraintName.objects.get(slug='conflic2'))
|
||||
new_form_data = {
|
||||
'constraint_conflict': self.group2.acronym,
|
||||
'constraint_conflic2': self.group2.acronym,
|
||||
|
@ -561,7 +614,7 @@ class SessionFormTest(TestCase):
|
|||
|
||||
def test_invalid_conflict_with_self(self):
|
||||
new_form_data = {
|
||||
'constraint_conflict': self.group1.acronym,
|
||||
'constraint_chair_conflict': self.group1.acronym,
|
||||
}
|
||||
self._invalid_test_helper(new_form_data)
|
||||
|
||||
|
|
|
@ -545,6 +545,11 @@ def edit(request, acronym, num=None):
|
|||
Constraint.objects.filter(meeting=meeting, source=group, name=cname.slug).delete()
|
||||
save_conflicts(group, meeting, form.cleaned_data[cfield_id], cname.slug)
|
||||
|
||||
# see if any inactive constraints should be deleted
|
||||
for cname, field_id in form.inactive_wg_constraint_field_ids():
|
||||
if form.cleaned_data[field_id]:
|
||||
Constraint.objects.filter(meeting=meeting, source=group, name=cname.slug).delete()
|
||||
|
||||
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')
|
||||
|
|
|
@ -657,6 +657,10 @@ table#sessions-new-table td {
|
|||
|
||||
input.wg_constraint { width: 37em; }
|
||||
|
||||
input.wg_constraint:disabled {
|
||||
background-color: #ffe0e0;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
ul.session-buttons {
|
||||
padding-left: 2px;
|
||||
margin-left: 0;
|
||||
|
|
|
@ -23,18 +23,33 @@
|
|||
<table>
|
||||
<tr>
|
||||
<td colspan="2">Other WGs that included {{ group.name }} in their conflict lists:</td>
|
||||
<td>{{ session_conflicts.inbound }}</td>
|
||||
<td>{{ session_conflicts.inbound|default:"None" }}</td>
|
||||
</tr>
|
||||
{% for cname, cfield, cselector in form.wg_constraint_fields %}
|
||||
<tr class="bg1">
|
||||
{% if forloop.first %}<td rowspan="3" valign="top" width="220">WG Sessions:<br>You may select multiple WGs within each category</td>{% endif %}
|
||||
{% if forloop.first %}<td rowspan="{{ form.wg_constraint_count }}" valign="top" width="220">WG Sessions:<br>You may select multiple WGs within each category</td>{% endif %}
|
||||
<td width="320">{{ cname|title }}</td>
|
||||
<td>{{ cselector }}
|
||||
<input type="button" id="wg_delete_{{ cname.slug }}" value="Delete the last entry" onClick="ietf_sessions.delete_wg_constraint_clicked('{{ cname.slug }}')"><br>
|
||||
{{ cfield.errors }}{{ cfield }}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}{# shown if there are no constraint fields #}
|
||||
<tr class="bg1"><td width="220"></td><td colspan="2">No constraints are enabled for this meeting.</td></tr>
|
||||
{% endfor %}
|
||||
{% if form.inactive_wg_constraints %}
|
||||
{% for cname, value, field in form.inactive_wg_constraints %}
|
||||
<tr class="bg1">
|
||||
{% if forloop.first %}
|
||||
<td rowspan="{{ form.inactive_wg_constraint_count }}" valign="top" width="220">
|
||||
Disabled for this meeting
|
||||
</td>
|
||||
{% endif %}
|
||||
<td width="320">{{ cname|title }}</td>
|
||||
<td><input type="text" value="{{ value }}" maxlength="255" class="wg_constraint" disabled><br>{{ field }} {{ field.label }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td colspan="2">BOF Sessions:</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>
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
{% elif item.timeslot.location.video_stream_url %}
|
||||
<a class=""
|
||||
href="{{item.timeslot.location.video_stream_url|format:session }}"
|
||||
title="Meetecho session"><span class="fa fa-fw fa-video-camera"></span>
|
||||
title="Meetecho video stream"><span class="fa fa-fw fa-video-camera"></span>
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="">
|
||||
|
@ -93,6 +93,10 @@
|
|||
<a class="" href="{{ href }}" title="{{ r.title }}"><span class="fa fa-fw fa-file-o"></span></a>
|
||||
{% endif %}
|
||||
{% endwith %}{% endfor %}
|
||||
{% elif item.timeslot.location.video_stream_url %}
|
||||
<a class=""
|
||||
href="http://www.meetecho.com/ietf{{meeting.number}}/recordings#{{acronym.upper}}"
|
||||
title="Meetecho session recording"><span class="fd fa-fw fd-meetecho"></span></a>
|
||||
{% elif show_empty %}
|
||||
<span class="fa fa-fw"></span>
|
||||
{% endif %}
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
{% if timeslot.location.video_stream_url %}
|
||||
<a class=""
|
||||
href="{{timeslot.location.video_stream_url|format:session }}"
|
||||
title="Meetecho session"><span class="fa fa-fw fa-video-camera"></span>
|
||||
title="Meetecho video stream"><span class="fa fa-fw fa-video-camera"></span>
|
||||
</a>
|
||||
{% endif %}
|
||||
<!-- Audio stream -->
|
||||
|
@ -108,6 +108,11 @@
|
|||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% if timeslot.location.video_stream_url %}
|
||||
<a class=""
|
||||
href="http://www.meetecho.com/ietf{{meeting.number}}/recordings#{{acronym.upper}}"
|
||||
title="Meetecho session recording"><span class="fd fa-fw fd-meetecho"></span></a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
|
|
@ -15,7 +15,7 @@ from ietf.group.models import Group, GroupHistory, Role, RoleHistory
|
|||
from ietf.iesg.models import TelechatDate
|
||||
from ietf.ipr.models import HolderIprDisclosure, IprDocRel, IprDisclosureStateName, IprLicenseTypeName
|
||||
from ietf.meeting.models import Meeting, ResourceAssociation
|
||||
from ietf.name.models import StreamName, DocRelationshipName, RoomResourceName
|
||||
from ietf.name.models import StreamName, DocRelationshipName, RoomResourceName, ConstraintName
|
||||
from ietf.person.models import Person, Email
|
||||
from ietf.group.utils import setup_default_community_list_for_group
|
||||
from ietf.review.models import (ReviewRequest, ReviewerSettings, ReviewResultName, ReviewTypeName, ReviewTeamSettings )
|
||||
|
@ -361,7 +361,7 @@ def make_test_data():
|
|||
)
|
||||
|
||||
# meeting
|
||||
Meeting.objects.create(
|
||||
ietf72 = Meeting.objects.create(
|
||||
number="72",
|
||||
type_id="ietf",
|
||||
date=datetime.date.today() + datetime.timedelta(days=180),
|
||||
|
@ -371,6 +371,9 @@ def make_test_data():
|
|||
break_area="Lounge",
|
||||
reg_area="Lobby",
|
||||
)
|
||||
# Use the "old" conflict names to avoid having to update tests
|
||||
for slug in ['conflict', 'conflic2', 'conflic3']:
|
||||
ietf72.group_conflict_types.add(ConstraintName.objects.get(slug=slug))
|
||||
|
||||
# interim meeting
|
||||
Meeting.objects.create(
|
||||
|
|
Loading…
Reference in a new issue