Skip to content

Commit

Permalink
Allow configuration of group conflict types used for each meeting Fixes
Browse files Browse the repository at this point in the history
#2770. Commit ready for merge.

 - Legacy-Id: 19266
  • Loading branch information
jennifer-richards committed Jul 30, 2021
1 parent ec86d98 commit 336d762
Show file tree
Hide file tree
Showing 18 changed files with 1,816 additions and 1,407 deletions.
20 changes: 19 additions & 1 deletion ietf/meeting/factories.py
Expand Up @@ -9,7 +9,7 @@
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

Expand Down Expand Up @@ -75,6 +75,24 @@ def populate_schedule(obj, create, extracted, **kwargs): # pylint: disable=no-se
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
Expand Down
2,689 changes: 1,351 additions & 1,338 deletions ietf/meeting/management/commands/create_dummy_meeting.py

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions ietf/meeting/migrations/0042_meeting_group_conflict_types.py
@@ -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),
]
14 changes: 13 additions & 1 deletion ietf/meeting/models.py
Expand Up @@ -17,7 +17,7 @@

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()
Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -197,6 +200,15 @@ def get_submission_correction_date(self):
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()

Expand Down
116 changes: 116 additions & 0 deletions ietf/meeting/tests_views.py
Expand Up @@ -13,6 +13,7 @@
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
Expand Down Expand Up @@ -1984,6 +1985,121 @@ def test_edit_meeting_timeslots_and_misc_sessions(self):
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()
Expand Down
4 changes: 2 additions & 2 deletions ietf/meeting/utils.py
Expand Up @@ -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',
Expand Down Expand Up @@ -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 = {}
Expand Down
7 changes: 6 additions & 1 deletion ietf/secr/meetings/forms.py
Expand Up @@ -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)
Expand All @@ -118,6 +120,9 @@ def save(self, force_insert=False, force_update=False, commit=True):
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):
Expand Down

0 comments on commit 336d762

Please sign in to comment.