Skip to content

Commit 825a054

Browse files
author
Sasha Romijn
committed
Add 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. - Legacy-Id: 17321 Note: SVN reference [17289] has been migrated to Git commit a227813
1 parent 1bfcf2e commit 825a054

9 files changed

Lines changed: 294 additions & 15 deletions

File tree

ietf/meeting/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -900,6 +900,7 @@ class Session(models.Model):
900900
short = models.CharField(blank=True, max_length=32, help_text="Short version of 'name' above, for use in filenames.")
901901
type = ForeignKey(TimeSlotTypeName)
902902
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.
903+
joint_with_groups = models.ManyToManyField(Group, related_name='sessions_joint_in')
903904
attendees = models.IntegerField(null=True, blank=True)
904905
agenda_note = models.CharField(blank=True, max_length=255)
905906
requested_duration = models.DurationField(default=datetime.timedelta(0))
@@ -1023,6 +1024,9 @@ def can_manage_materials(self, user):
10231024

10241025
def is_material_submission_cutoff(self):
10251026
return datetime.date.today() > self.meeting.get_submission_correction_date()
1027+
1028+
def joint_with_groups_acronyms(self):
1029+
return [group.acronym for group in self.joint_with_groups.all()]
10261030

10271031
def __str__(self):
10281032
if self.meeting.type_id == "interim":

ietf/secr/sreq/forms.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
# LENGTH_SESSION_CHOICES = (('','--Please select'),('1800','30 minutes'),('3600','1 hour'),('5400','1.5 hours'), ('7200','2 hours'),('9000','2.5 hours'))
2323
LENGTH_SESSION_CHOICES = (('','--Please select'),('1800','30 minutes'),('3600','1 hour'),('5400','1.5 hours'), ('7200','2 hours'))
2424
SESSION_TIME_RELATION_CHOICES = (('', 'No preference'),) + Constraint.TIME_RELATION_CHOICES
25+
JOINT_FOR_SESSION_CHOICES = (('1', 'First session'), ('2', 'Second session'), ('3', 'Third session'), )
2526

2627
# -------------------------------------------------
2728
# Helper Functions
@@ -86,10 +87,13 @@ class SessionForm(forms.Form):
8687
conflict1 = forms.CharField(max_length=255,required=False)
8788
conflict2 = forms.CharField(max_length=255,required=False)
8889
conflict3 = forms.CharField(max_length=255,required=False)
90+
joint_with_groups = forms.CharField(max_length=255,required=False)
91+
joint_for_session = forms.ChoiceField(choices=JOINT_FOR_SESSION_CHOICES, required=False)
8992
comments = forms.CharField(max_length=200,required=False)
9093
wg_selector1 = forms.ChoiceField(choices=[],required=False)
9194
wg_selector2 = forms.ChoiceField(choices=[],required=False)
9295
wg_selector3 = forms.ChoiceField(choices=[],required=False)
96+
wg_selector4 = forms.ChoiceField(choices=[],required=False)
9397
third_session = forms.BooleanField(required=False)
9498
resources = forms.MultipleChoiceField(widget=forms.CheckboxSelectMultiple,required=False)
9599
bethere = SearchablePersonsField(label="Must be present", required=False)
@@ -115,7 +119,7 @@ def __init__(self, group, *args, **kwargs):
115119
other_groups = list(allowed_conflicting_groups().exclude(pk=group.pk).values_list('acronym', 'acronym').order_by('acronym'))
116120
self.fields['adjacent_with_wg'].choices = [('', '--No preference')] + other_groups
117121
group_acronym_choices = [('','--Select WG(s)')] + other_groups
118-
for i in range(1, 4):
122+
for i in range(1, 5):
119123
self.fields['wg_selector{}'.format(i)].choices = group_acronym_choices
120124

121125
# disabling handleconflictfield (which only enables or disables form elements) while we're hacking the meaning of the three constraints currently in use:
@@ -125,6 +129,7 @@ def __init__(self, group, *args, **kwargs):
125129
self.fields['wg_selector1'].widget.attrs['onChange'] = "document.form_post.conflict1.value=document.form_post.conflict1.value + ' ' + this.options[this.selectedIndex].value; return 1;"
126130
self.fields['wg_selector2'].widget.attrs['onChange'] = "document.form_post.conflict2.value=document.form_post.conflict2.value + ' ' + this.options[this.selectedIndex].value; return 1;"
127131
self.fields['wg_selector3'].widget.attrs['onChange'] = "document.form_post.conflict3.value=document.form_post.conflict3.value + ' ' + this.options[this.selectedIndex].value; return 1;"
132+
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;"
128133

129134
# disabling check_prior_conflict javascript while we're hacking the meaning of the three constraints currently in use
130135
#self.fields['wg_selector2'].widget.attrs['onClick'] = "return check_prior_conflict(2);"
@@ -159,7 +164,12 @@ def clean_conflict3(self):
159164
conflict = self.cleaned_data['conflict3']
160165
check_conflict(conflict, self.group)
161166
return conflict
162-
167+
168+
def clean_joint_with_groups(self):
169+
groups = self.cleaned_data['joint_with_groups']
170+
check_conflict(groups, self.group)
171+
return groups
172+
163173
def clean(self):
164174
super(SessionForm, self).clean()
165175
data = self.cleaned_data
@@ -180,12 +190,20 @@ def clean(self):
180190
if data.get('num_session','') == '2':
181191
if not data['length_session2']:
182192
raise forms.ValidationError('You must enter a length for all sessions')
183-
elif data.get('session_time_relation'):
184-
raise forms.ValidationError('Time between sessions can only be used when two sessions are requested.')
185-
193+
else:
194+
if data.get('session_time_relation'):
195+
raise forms.ValidationError('Time between sessions can only be used when two '
196+
'sessions are requested.')
197+
if data['joint_for_session'] == '2':
198+
raise forms.ValidationError('The second session can not be the joint session, '
199+
'because you have not requested a second session.')
200+
186201
if data.get('third_session',False):
187202
if not data['length_session2'] or not data.get('length_session3',None):
188203
raise forms.ValidationError('You must enter a length for all sessions')
204+
elif data['joint_for_session'] == '3':
205+
raise forms.ValidationError('The third session can not be the joint session, '
206+
'because you have not requested a third session.')
189207

190208
return data
191209

ietf/secr/sreq/tests.py

Lines changed: 182 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from ietf.meeting.factories import MeetingFactory, SessionFactory
1818
from ietf.name.models import TimerangeName
1919
from ietf.person.models import Person
20+
from ietf.secr.sreq.forms import SessionForm
2021
from ietf.utils.mail import outbox, empty_outbox
2122

2223
from pyquery import PyQuery
@@ -82,6 +83,8 @@ def test_edit(self):
8283
meeting = MeetingFactory(type_id='ietf', date=datetime.date.today())
8384
mars = RoleFactory(name_id='chair', person__user__username='marschairman', group__acronym='mars').group
8485
group2 = GroupFactory()
86+
group3 = GroupFactory()
87+
group4 = GroupFactory()
8588
SessionFactory(meeting=meeting,group=mars,status_id='sched')
8689

8790
url = reverse('ietf.secr.sreq.views.edit', kwargs={'acronym':'mars'})
@@ -96,6 +99,8 @@ def test_edit(self):
9699
'comments':'need lights',
97100
'session_time_relation': 'subsequent-days',
98101
'adjacent_with_wg': group2.acronym,
102+
'joint_with_groups': group3.acronym + ' ' + group4.acronym,
103+
'joint_for_session': '2',
99104
'timeranges': ['thursday-afternoon-early', 'thursday-afternoon-late'],
100105
'submit': 'Continue'}
101106
r = self.client.post(url, post_data, HTTP_HOST='example.com')
@@ -112,12 +117,43 @@ def test_edit(self):
112117
list(session.constraints().get(name='timerange').timeranges.all().values('name')),
113118
list(TimerangeName.objects.filter(name__in=['thursday-afternoon-early', 'thursday-afternoon-late']).values('name'))
114119
)
115-
120+
self.assertFalse(sessions[0].joint_with_groups.count())
121+
self.assertEqual(list(sessions[1].joint_with_groups.all()), [group3, group4])
122+
116123
# Check whether the updated data is visible on the view page
117124
r = self.client.get(redirect_url)
118125
self.assertContains(r, 'Schedule the sessions on subsequent days')
119126
self.assertContains(r, 'Thursday early afternoon, Thursday late afternoon')
120127
self.assertContains(r, group2.acronym)
128+
self.assertContains(r, 'Second session with: {} {}'.format(group3.acronym, group4.acronym))
129+
130+
# Edit again, changing the joint sessions and clearing some fields. The behaviour of
131+
# edit is different depending on whether previous joint sessions were recorded.
132+
post_data = {'num_session':'2',
133+
'length_session1':'3600',
134+
'length_session2':'3600',
135+
'attendees':'10',
136+
'conflict1':'',
137+
'comments':'need lights',
138+
'joint_with_groups': group2.acronym,
139+
'joint_for_session': '1',
140+
'submit': 'Continue'}
141+
r = self.client.post(url, post_data, HTTP_HOST='example.com')
142+
self.assertRedirects(r, redirect_url)
143+
144+
# Check whether updates were stored in the database
145+
sessions = Session.objects.filter(meeting=meeting, group=mars)
146+
self.assertEqual(len(sessions), 2)
147+
session = sessions[0]
148+
self.assertFalse(session.constraints().filter(name='time_relation'))
149+
self.assertFalse(session.constraints().filter(name='wg_adjacent'))
150+
self.assertFalse(session.constraints().filter(name='timerange'))
151+
self.assertEqual(list(sessions[0].joint_with_groups.all()), [group2])
152+
self.assertFalse(sessions[1].joint_with_groups.count())
153+
154+
# Check whether the updated data is visible on the view page
155+
r = self.client.get(redirect_url)
156+
self.assertContains(r, 'First session with: {}'.format(group2.acronym))
121157

122158
def test_tool_status(self):
123159
MeetingFactory(type_id='ietf', date=datetime.date.today())
@@ -135,6 +171,8 @@ def test_submit_request(self):
135171
area = RoleFactory(name_id='ad', person=ad, group__type_id='area').group
136172
group = GroupFactory(parent=area)
137173
group2 = GroupFactory(parent=area)
174+
group3 = GroupFactory(parent=area)
175+
group4 = GroupFactory(parent=area)
138176
session_count_before = Session.objects.filter(meeting=meeting, group=group).count()
139177
url = reverse('ietf.secr.sreq.views.new',kwargs={'acronym':group.acronym})
140178
confirm_url = reverse('ietf.secr.sreq.views.confirm',kwargs={'acronym':group.acronym})
@@ -146,6 +184,8 @@ def test_submit_request(self):
146184
'comments':'need projector',
147185
'adjacent_with_wg': group2.acronym,
148186
'timeranges': ['thursday-afternoon-early', 'thursday-afternoon-late'],
187+
'joint_with_groups': group3.acronym + ' ' + group4.acronym,
188+
'joint_for_session': '1',
149189
'submit': 'Continue'}
150190
self.client.login(username="secretary", password="secretary+password")
151191
r = self.client.post(url,post_data)
@@ -154,6 +194,7 @@ def test_submit_request(self):
154194
# Verify the contents of the confirm view
155195
self.assertContains(r, 'Thursday early afternoon, Thursday late afternoon')
156196
self.assertContains(r, group2.acronym)
197+
self.assertContains(r, 'First session with: {} {}'.format(group3.acronym, group4.acronym))
157198

158199
post_data['submit'] = 'Submit'
159200
r = self.client.post(confirm_url,post_data)
@@ -166,6 +207,15 @@ def test_submit_request(self):
166207
self.assertRedirects(r, main_url)
167208
session_count_after = Session.objects.filter(meeting=meeting, group=group, type='regular').count()
168209
self.assertEqual(session_count_after, session_count_before + 1)
210+
211+
# Verify database content
212+
session = Session.objects.get(meeting=meeting, group=group)
213+
self.assertEqual(session.constraints().get(name='wg_adjacent').target.acronym, group2.acronym)
214+
self.assertEqual(
215+
list(session.constraints().get(name='timerange').timeranges.all().values('name')),
216+
list(TimerangeName.objects.filter(name__in=['thursday-afternoon-early', 'thursday-afternoon-late']).values('name'))
217+
)
218+
self.assertEqual(list(session.joint_with_groups.all()), [group3, group4])
169219

170220
def test_submit_request_invalid(self):
171221
MeetingFactory(type_id='ietf', date=datetime.date.today())
@@ -237,6 +287,7 @@ def test_request_notification(self):
237287
RoleFactory(name_id='ad', person=ad, group=area)
238288
group = GroupFactory(acronym='ames', parent=area)
239289
group2 = GroupFactory(acronym='ames2', parent=area)
290+
group3 = GroupFactory(acronym='ames2', parent=area)
240291
RoleFactory(name_id='chair', group=group, person__user__username='ameschairman')
241292
resource = ResourceAssociation.objects.create(name_id='project')
242293
# Bit of a test data hack - the fixture now has no used resources to pick from
@@ -256,6 +307,8 @@ def test_request_notification(self):
256307
'resources': resource.pk,
257308
'session_time_relation': 'subsequent-days',
258309
'adjacent_with_wg': group2.acronym,
310+
'joint_with_groups': group3.acronym,
311+
'joint_for_session': '2',
259312
'timeranges': ['thursday-afternoon-early', 'thursday-afternoon-late'],
260313
'submit': 'Continue'}
261314
self.client.login(username="ameschairman", password="ameschairman+password")
@@ -284,10 +337,11 @@ def test_request_notification(self):
284337
list(TimerangeName.objects.filter(name__in=['thursday-afternoon-early', 'thursday-afternoon-late']).values('name'))
285338
)
286339
resource = session.resources.first()
340+
self.assertTrue(resource.desc in notification_payload)
287341
self.assertTrue('Schedule the sessions on subsequent days' in notification_payload)
288342
self.assertTrue(group2.acronym in notification_payload)
289343
self.assertTrue("Can't meet: Thursday early afternoon, Thursday late" in notification_payload)
290-
self.assertTrue(resource.desc in notification_payload)
344+
self.assertTrue('Second session joint with: {}'.format(group3.acronym) in notification_payload)
291345
self.assertTrue(ad.ascii_name() in notification_payload)
292346

293347
class LockAppTestCase(TestCase):
@@ -359,9 +413,133 @@ def test_not_meeting(self):
359413
class RetrievePreviousCase(TestCase):
360414
pass
361415

362-
363-
364416
# test error if already scheduled
365417
# test get previous exists/doesn't exist
366418
# test that groups scheduled and unscheduled add up to total groups
367419
# test access by unauthorized
420+
421+
422+
class SessionFormTest(TestCase):
423+
def setUp(self):
424+
self.group1 = GroupFactory()
425+
self.group2 = GroupFactory()
426+
self.group3 = GroupFactory()
427+
self.group4 = GroupFactory()
428+
self.group5 = GroupFactory()
429+
self.group6 = GroupFactory()
430+
431+
self.valid_form_data = {
432+
'num_session': '2',
433+
'third_session': 'true',
434+
'length_session1': '3600',
435+
'length_session2': '3600',
436+
'length_session3': '3600',
437+
'attendees': '10',
438+
'conflict1': self.group2.acronym,
439+
'conflict2': self.group3.acronym,
440+
'conflict3': self.group4.acronym,
441+
'comments': 'need lights',
442+
'session_time_relation': 'subsequent-days',
443+
'adjacent_with_wg': self.group5.acronym,
444+
'joint_with_groups': self.group6.acronym,
445+
'joint_for_session': '3',
446+
'timeranges': ['thursday-afternoon-early', 'thursday-afternoon-late'],
447+
'submit': 'Continue'
448+
}
449+
450+
def test_valid(self):
451+
# Test with three sessions
452+
form = SessionForm(data=self.valid_form_data, group=self.group1)
453+
self.assertTrue(form.is_valid())
454+
455+
# Test with two sessions
456+
self.valid_form_data.update({
457+
'length_session3': '',
458+
'third_session': '',
459+
'joint_for_session': '2'
460+
})
461+
form = SessionForm(data=self.valid_form_data, group=self.group1)
462+
self.assertTrue(form.is_valid())
463+
464+
# Test with one session
465+
self.valid_form_data.update({
466+
'length_session2': '',
467+
'num_session': 1,
468+
'joint_for_session': '1',
469+
'session_time_relation': '',
470+
})
471+
form = SessionForm(data=self.valid_form_data, group=self.group1)
472+
self.assertTrue(form.is_valid())
473+
474+
def test_invalid_groups(self):
475+
new_form_data = {
476+
'conflict1': 'doesnotexist',
477+
'conflict2': 'doesnotexist',
478+
'conflict3': 'doesnotexist',
479+
'adjacent_with_wg': 'doesnotexist',
480+
'joint_with_groups': 'doesnotexist',
481+
}
482+
form = self._invalid_test_helper(new_form_data)
483+
self.assertEqual(set(form.errors.keys()), set(new_form_data.keys()))
484+
485+
def test_invalid_group_appears_in_multiple_conflicts(self):
486+
new_form_data = {
487+
'conflict1': self.group2.acronym,
488+
'conflict2': self.group2.acronym,
489+
}
490+
form = self._invalid_test_helper(new_form_data)
491+
self.assertEqual(form.non_field_errors(), ['%s appears in conflicts more than once' % self.group2.acronym])
492+
493+
def test_invalid_conflict_with_self(self):
494+
new_form_data = {
495+
'conflict1': self.group1.acronym,
496+
}
497+
self._invalid_test_helper(new_form_data)
498+
499+
def test_invalid_session_time_relation(self):
500+
form = self._invalid_test_helper({
501+
'third_session': '',
502+
'length_session2': '',
503+
'num_session': 1,
504+
'joint_for_session': '1',
505+
})
506+
self.assertEqual(form.non_field_errors(), ['Time between sessions can only be used when two '
507+
'sessions are requested.'])
508+
509+
def test_invalid_joint_for_session(self):
510+
form = self._invalid_test_helper({
511+
'third_session': '',
512+
'num_session': 2,
513+
'joint_for_session': '3',
514+
})
515+
self.assertEqual(form.non_field_errors(), ['The third session can not be the joint session, '
516+
'because you have not requested a third session.'])
517+
518+
form = self._invalid_test_helper({
519+
'third_session': '',
520+
'length_session2': '',
521+
'num_session': 1,
522+
'joint_for_session': '2',
523+
'session_time_relation': '',
524+
})
525+
self.assertEqual(form.non_field_errors(), ['The second session can not be the joint session, '
526+
'because you have not requested a second session.'])
527+
528+
def test_invalid_missing_session_length(self):
529+
form = self._invalid_test_helper({
530+
'length_session2': '',
531+
'third_session': 'true',
532+
})
533+
self.assertEqual(form.non_field_errors(), ['You must enter a length for all sessions'])
534+
535+
form = self._invalid_test_helper({'length_session2': ''})
536+
self.assertEqual(form.non_field_errors(), ['You must enter a length for all sessions'])
537+
538+
form = self._invalid_test_helper({'length_session3': ''})
539+
self.assertEqual(form.non_field_errors(), ['You must enter a length for all sessions'])
540+
541+
def _invalid_test_helper(self, new_form_data):
542+
form_data = dict(self.valid_form_data, **new_form_data)
543+
form = SessionForm(data=form_data, group=self.group1)
544+
self.assertFalse(form.is_valid())
545+
return form

0 commit comments

Comments
 (0)