Skip to content

Commit 2eafa85

Browse files
committed
Merged in [17217] from olau@iola.dk:
Tighten validation of session conflicts in the session request tool, preventing conflicts where source = target. Make sure the 'use previous session' button doesn't carry forward conflicts that are no longer valid. Also fix a bug with the group list in the dropdowns being computed statically at module import time instead of being queried dynamically upon each page request. - Legacy-Id: 17222 Note: SVN reference [17217] has been migrated to Git commit c34fec5
2 parents 428525d + c34fec5 commit 2eafa85

3 files changed

Lines changed: 93 additions & 29 deletions

File tree

ietf/secr/sreq/forms.py

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright The IETF Trust 2013-2019, All Rights Reserved
1+
# Copyright The IETF Trust 2013-2020, All Rights Reserved
22
# -*- coding: utf-8 -*-
33

44

@@ -20,20 +20,24 @@
2020
NUM_SESSION_CHOICES = (('','--Please select'),('1','1'),('2','2'))
2121
# LENGTH_SESSION_CHOICES = (('','--Please select'),('1800','30 minutes'),('3600','1 hour'),('5400','1.5 hours'), ('7200','2 hours'),('9000','2.5 hours'))
2222
LENGTH_SESSION_CHOICES = (('','--Please select'),('1800','30 minutes'),('3600','1 hour'),('5400','1.5 hours'), ('7200','2 hours'))
23-
WG_CHOICES = list( Group.objects.filter(type__in=('wg','rg','ag'),state__in=('bof','proposed','active')).values_list('acronym','acronym').order_by('acronym')) # type:ignore
24-
WG_CHOICES.insert(0,('','--Select WG(s)')) # type:ignore
2523

2624
# -------------------------------------------------
2725
# Helper Functions
2826
# -------------------------------------------------
29-
def check_conflict(groups):
27+
def allowed_conflicting_groups():
28+
return Group.objects.filter(type__in=['wg', 'ag', 'rg'], state__in=['bof', 'proposed', 'active'])
29+
30+
def check_conflict(groups, source_group):
3031
'''
3132
Takes a string which is a list of group acronyms. Checks that they are all active groups
3233
'''
3334
# convert to python list (allow space or comma separated lists)
3435
items = groups.replace(',',' ').split()
35-
active_groups = Group.objects.filter(type__in=('wg','ag','rg'), state__in=('bof','proposed','active'))
36+
active_groups = allowed_conflicting_groups()
3637
for group in items:
38+
if group == source_group.acronym:
39+
raise forms.ValidationError("Cannot declare a conflict with the same group: %s" % group)
40+
3741
if not active_groups.filter(acronym=group):
3842
raise forms.ValidationError("Invalid or inactive group acronym: %s" % group)
3943

@@ -67,28 +71,39 @@ class SessionForm(forms.Form):
6771
length_session2 = forms.ChoiceField(choices=LENGTH_SESSION_CHOICES,required=False)
6872
length_session3 = forms.ChoiceField(choices=LENGTH_SESSION_CHOICES,required=False)
6973
attendees = forms.IntegerField()
74+
# FIXME: it would cleaner to have these be
75+
# ModelMultipleChoiceField, and just customize the widgetry, that
76+
# way validation comes for free
7077
conflict1 = forms.CharField(max_length=255,required=False)
7178
conflict2 = forms.CharField(max_length=255,required=False)
7279
conflict3 = forms.CharField(max_length=255,required=False)
7380
comments = forms.CharField(max_length=200,required=False)
74-
wg_selector1 = forms.ChoiceField(choices=WG_CHOICES,required=False)
75-
wg_selector2 = forms.ChoiceField(choices=WG_CHOICES,required=False)
76-
wg_selector3 = forms.ChoiceField(choices=WG_CHOICES,required=False)
81+
wg_selector1 = forms.ChoiceField(choices=[],required=False)
82+
wg_selector2 = forms.ChoiceField(choices=[],required=False)
83+
wg_selector3 = forms.ChoiceField(choices=[],required=False)
7784
third_session = forms.BooleanField(required=False)
7885
resources = forms.MultipleChoiceField(widget=forms.CheckboxSelectMultiple,required=False)
7986
bethere = SearchablePersonsField(label="Must be present", required=False)
8087

81-
def __init__(self, *args, **kwargs):
88+
def __init__(self, group, *args, **kwargs):
8289
if 'hidden' in kwargs:
8390
self.hidden = kwargs.pop('hidden')
8491
else:
8592
self.hidden = False
93+
94+
self.group = group
95+
8696
super(SessionForm, self).__init__(*args, **kwargs)
8797
self.fields['num_session'].widget.attrs['onChange'] = "stat_ls(this.selectedIndex);"
8898
self.fields['length_session1'].widget.attrs['onClick'] = "if (check_num_session(1)) this.disabled=true;"
8999
self.fields['length_session2'].widget.attrs['onClick'] = "if (check_num_session(2)) this.disabled=true;"
90100
self.fields['length_session3'].widget.attrs['onClick'] = "if (check_third_session()) { this.disabled=true;}"
91101
self.fields['comments'].widget = forms.Textarea(attrs={'rows':'6','cols':'65'})
102+
103+
group_acronym_choices = [('','--Select WG(s)')] + list(allowed_conflicting_groups().exclude(pk=group.pk).values_list('acronym','acronym').order_by('acronym'))
104+
for i in range(1, 4):
105+
self.fields['wg_selector{}'.format(i)].choices = group_acronym_choices
106+
92107
# disabling handleconflictfield (which only enables or disables form elements) while we're hacking the meaning of the three constraints currently in use:
93108
#self.fields['wg_selector1'].widget.attrs['onChange'] = "document.form_post.conflict1.value=document.form_post.conflict1.value + ' ' + this.options[this.selectedIndex].value; return handleconflictfield(1);"
94109
#self.fields['wg_selector2'].widget.attrs['onChange'] = "document.form_post.conflict2.value=document.form_post.conflict2.value + ' ' + this.options[this.selectedIndex].value; return handleconflictfield(2);"
@@ -117,17 +132,17 @@ def __init__(self, *args, **kwargs):
117132

118133
def clean_conflict1(self):
119134
conflict = self.cleaned_data['conflict1']
120-
check_conflict(conflict)
135+
check_conflict(conflict, self.group)
121136
return conflict
122137

123138
def clean_conflict2(self):
124139
conflict = self.cleaned_data['conflict2']
125-
check_conflict(conflict)
140+
check_conflict(conflict, self.group)
126141
return conflict
127142

128143
def clean_conflict3(self):
129144
conflict = self.cleaned_data['conflict3']
130-
check_conflict(conflict)
145+
check_conflict(conflict, self.group)
131146
return conflict
132147

133148
def clean(self):

ietf/secr/sreq/tests.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright The IETF Trust 2013-2019, All Rights Reserved
1+
# Copyright The IETF Trust 2013-2020, All Rights Reserved
22
# -*- coding: utf-8 -*-
33

44

@@ -13,7 +13,7 @@
1313

1414
from ietf.utils.test_utils import TestCase
1515
from ietf.group.factories import GroupFactory, RoleFactory
16-
from ietf.meeting.models import Session, ResourceAssociation, SchedulingEvent
16+
from ietf.meeting.models import Session, ResourceAssociation, SchedulingEvent, Constraint
1717
from ietf.meeting.factories import MeetingFactory, SessionFactory
1818
from ietf.person.models import Person
1919
from ietf.utils.mail import outbox, empty_outbox
@@ -154,6 +154,51 @@ def test_submit_request_invalid(self):
154154
self.assertEqual(len(q('#session-request-form')),1)
155155
self.assertContains(r, 'You must enter a length for all sessions')
156156

157+
def test_submit_request_check_constraints(self):
158+
m1 = MeetingFactory(type_id='ietf', date=datetime.date.today() - datetime.timedelta(days=100))
159+
MeetingFactory(type_id='ietf', date=datetime.date.today())
160+
ad = Person.objects.get(user__username='ad')
161+
area = RoleFactory(name_id='ad', person=ad, group__type_id='area').group
162+
group = GroupFactory(parent=area)
163+
still_active_group = GroupFactory(parent=area)
164+
Constraint.objects.create(
165+
meeting=m1,
166+
source=group,
167+
target=still_active_group,
168+
name_id='conflict',
169+
)
170+
inactive_group = GroupFactory(parent=area, state_id='conclude')
171+
inactive_group.save()
172+
Constraint.objects.create(
173+
meeting=m1,
174+
source=group,
175+
target=inactive_group,
176+
name_id='conflict',
177+
)
178+
SessionFactory(group=group, meeting=m1)
179+
180+
self.client.login(username="secretary", password="secretary+password")
181+
182+
url = reverse('ietf.secr.sreq.views.new',kwargs={'acronym':group.acronym})
183+
r = self.client.get(url + '?previous')
184+
self.assertEqual(r.status_code, 200)
185+
q = PyQuery(r.content)
186+
conflict1 = q('[name="conflict1"]').val()
187+
self.assertIn(still_active_group.acronym, conflict1)
188+
self.assertNotIn(inactive_group.acronym, conflict1)
189+
190+
post_data = {'num_session':'1',
191+
'length_session1':'3600',
192+
'attendees':'10',
193+
'conflict1': group.acronym,
194+
'comments':'need projector',
195+
'submit': 'Continue'}
196+
r = self.client.post(url,post_data)
197+
self.assertEqual(r.status_code, 200)
198+
q = PyQuery(r.content)
199+
self.assertEqual(len(q('#session-request-form')),1)
200+
self.assertContains(r, "Cannot declare a conflict with the same group")
201+
157202
def test_request_notification(self):
158203
meeting = MeetingFactory(type_id='ietf', date=datetime.date.today())
159204
ad = Person.objects.get(user__username='ad')

ietf/secr/sreq/views.py

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright The IETF Trust 2013-2019, All Rights Reserved
1+
# Copyright The IETF Trust 2013-2020, All Rights Reserved
22
# -*- coding: utf-8 -*-
33

44

@@ -21,7 +21,7 @@
2121
from ietf.meeting.helpers import get_meeting
2222
from ietf.meeting.utils import add_event_info_to_session_qs
2323
from ietf.name.models import SessionStatusName, ConstraintName
24-
from ietf.secr.sreq.forms import SessionForm, ToolStatusForm
24+
from ietf.secr.sreq.forms import SessionForm, ToolStatusForm, allowed_conflicting_groups
2525
from ietf.secr.utils.decorators import check_permissions
2626
from ietf.secr.utils.group import get_my_groups
2727
from ietf.utils.mail import send_mail
@@ -45,13 +45,13 @@ def check_app_locked(meeting=None):
4545
meeting = get_meeting()
4646
return bool(meeting.session_request_lock_message)
4747

48-
def get_initial_session(sessions):
48+
def get_initial_session(sessions, prune_conflicts=False):
4949
'''
5050
This function takes a queryset of sessions ordered by 'id' for consistency. It returns
5151
a dictionary to be used as the initial for a legacy session form
5252
'''
5353
initial = {}
54-
if(len(sessions) == 0):
54+
if len(sessions) == 0:
5555
return initial
5656

5757
meeting = sessions[0].meeting
@@ -70,9 +70,13 @@ def get_initial_session(sessions):
7070
except IndexError:
7171
pass
7272
initial['attendees'] = sessions[0].attendees
73-
initial['conflict1'] = ' '.join([ c.target.acronym for c in conflicts.filter(name__slug='conflict') ])
74-
initial['conflict2'] = ' '.join([ c.target.acronym for c in conflicts.filter(name__slug='conflic2') ])
75-
initial['conflict3'] = ' '.join([ c.target.acronym for c in conflicts.filter(name__slug='conflic3') ])
73+
74+
def valid_conflict(conflict):
75+
return conflict.target != sessions[0].group and allowed_conflicting_groups().filter(pk=conflict.target_id).exists()
76+
77+
initial['conflict1'] = ' '.join(c.target.acronym for c in conflicts.filter(name__slug='conflict') if not prune_conflicts or valid_conflict(c))
78+
initial['conflict2'] = ' '.join(c.target.acronym for c in conflicts.filter(name__slug='conflic2') if not prune_conflicts or valid_conflict(c))
79+
initial['conflict3'] = ' '.join(c.target.acronym for c in conflicts.filter(name__slug='conflic3') if not prune_conflicts or valid_conflict(c))
7680
initial['comments'] = sessions[0].comments
7781
initial['resources'] = sessions[0].resources.all()
7882
initial['bethere'] = [x.person for x in sessions[0].constraints().filter(name='bethere').select_related("person")]
@@ -243,10 +247,10 @@ def confirm(request, acronym):
243247
to confirm for submission.
244248
'''
245249
# FIXME: this should be using form.is_valid/form.cleaned_data - invalid input will make it crash
246-
form = SessionForm(request.POST, hidden=True)
250+
group = get_object_or_404(Group,acronym=acronym)
251+
form = SessionForm(group, request.POST, hidden=True)
247252
form.is_valid()
248253
meeting = get_meeting()
249-
group = get_object_or_404(Group,acronym=acronym)
250254
login = request.user.person
251255

252256
# check if request already exists for this group
@@ -376,7 +380,7 @@ def edit(request, acronym, num=None):
376380
if button_text == 'Cancel':
377381
return redirect('ietf.secr.sreq.views.view', acronym=acronym)
378382

379-
form = SessionForm(request.POST,initial=initial)
383+
form = SessionForm(group, request.POST, initial=initial)
380384
if form.is_valid():
381385
if form.has_changed():
382386
# might be cleaner to simply delete and rewrite all records (but maintain submitter?)
@@ -485,7 +489,7 @@ def edit(request, acronym, num=None):
485489
else:
486490
if not sessions:
487491
return redirect('ietf.secr.sreq.views.new', acronym=acronym)
488-
form = SessionForm(initial=initial)
492+
form = SessionForm(group, initial=initial)
489493

490494
return render(request, 'sreq/edit.html', {
491495
'is_locked': is_locked,
@@ -583,7 +587,7 @@ def new(request, acronym):
583587
if button_text == 'Cancel':
584588
return redirect('ietf.secr.sreq.views.main')
585589

586-
form = SessionForm(request.POST)
590+
form = SessionForm(group, request.POST)
587591
if form.is_valid():
588592
return confirm(request, acronym)
589593

@@ -596,16 +600,16 @@ def new(request, acronym):
596600
messages.warning(request, 'This group did not meet at %s' % previous_meeting)
597601
return redirect('ietf.secr.sreq.views.new', acronym=acronym)
598602

599-
initial = get_initial_session(previous_sessions)
603+
initial = get_initial_session(previous_sessions, prune_conflicts=True)
600604
add_essential_people(group,initial)
601605
if 'resources' in initial:
602606
initial['resources'] = [x.pk for x in initial['resources']]
603-
form = SessionForm(initial=initial)
607+
form = SessionForm(group, initial=initial)
604608

605609
else:
606610
initial={}
607611
add_essential_people(group,initial)
608-
form = SessionForm(initial=initial)
612+
form = SessionForm(group, initial=initial)
609613

610614
return render(request, 'sreq/new.html', {
611615
'meeting': meeting,

0 commit comments

Comments
 (0)