Skip to content

Commit 1ea5dcf

Browse files
committed
Liaison changes from IETF 94 demo. Includes tab for Action Needed statements and ability to add comments to history. Commit ready for merge
- Legacy-Id: 10464
1 parent 3f1b281 commit 1ea5dcf

14 files changed

Lines changed: 221 additions & 52 deletions

File tree

ietf/liaisons/forms.py

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ def validate_emails(value):
117117
# -------------------------------------------------
118118
# Form Classes
119119
# -------------------------------------------------
120+
class AddCommentForm(forms.Form):
121+
comment = forms.CharField(required=True, widget=forms.Textarea)
122+
private = forms.BooleanField(label="Private comment", required=False,help_text="If this box is checked the comment will not appear in the statement's public history view.")
120123

121124
class RadioRenderer(RadioFieldRenderer):
122125
def render(self):
@@ -127,6 +130,7 @@ def render(self):
127130

128131

129132
class SearchLiaisonForm(forms.Form):
133+
'''Expects initial keyword argument queryset which then gets filtered based on form data'''
130134
text = forms.CharField(required=False)
131135
scope = forms.ChoiceField(choices=(("all", "All text fields"), ("title", "Title field")), required=False, initial='title', widget=forms.RadioSelect(renderer=RadioRenderer))
132136
source = forms.CharField(required=False)
@@ -135,11 +139,11 @@ class SearchLiaisonForm(forms.Form):
135139
end_date = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1" }, label='End date', required=False)
136140

137141
def __init__(self, *args, **kwargs):
138-
self.state = kwargs.pop('state')
142+
self.queryset = kwargs.pop('queryset')
139143
super(SearchLiaisonForm, self).__init__(*args, **kwargs)
140144

141145
def get_results(self):
142-
results = LiaisonStatement.objects.filter(state=self.state)
146+
results = self.queryset
143147
if self.is_bound:
144148
query = self.cleaned_data.get('text')
145149
if query:
@@ -157,11 +161,19 @@ def get_results(self):
157161

158162
source = self.cleaned_data.get('source')
159163
if source:
160-
results = results.filter(Q(from_groups__name__icontains=source) | Q(from_groups__acronym__iexact=source))
164+
source_list = source.split(',')
165+
if len(source_list) > 1:
166+
results = results.filter(Q(from_groups__acronym__in=source_list))
167+
else:
168+
results = results.filter(Q(from_groups__name__icontains=source) | Q(from_groups__acronym__iexact=source))
161169

162170
destination = self.cleaned_data.get('destination')
163171
if destination:
164-
results = results.filter(Q(to_groups__name__icontains=destination) | Q(to_groups__acronym__iexact=destination))
172+
destination_list = destination.split(',')
173+
if len(destination_list) > 1:
174+
results = results.filter(Q(to_groups__acronym__in=destination_list))
175+
else:
176+
results = results.filter(Q(to_groups__name__icontains=destination) | Q(to_groups__acronym__iexact=destination))
165177

166178
start_date = self.cleaned_data.get('start_date')
167179
end_date = self.cleaned_data.get('end_date')
@@ -192,10 +204,13 @@ def prepare_value(self, value):
192204

193205

194206
class LiaisonModelForm(BetterModelForm):
195-
'''Specify fields which require a custom widget or that are not part of the model'''
196-
from_groups = forms.ModelMultipleChoiceField(queryset=Group.objects.all(),label=u'Groups')
207+
'''Specify fields which require a custom widget or that are not part of the model.
208+
NOTE: from_groups and to_groups are marked as not required because select2 has
209+
a problem with validating
210+
'''
211+
from_groups = forms.ModelMultipleChoiceField(queryset=Group.objects.all(),label=u'Groups',required=False)
197212
from_contact = forms.EmailField()
198-
to_groups = forms.ModelMultipleChoiceField(queryset=Group.objects,label=u'Groups')
213+
to_groups = forms.ModelMultipleChoiceField(queryset=Group.objects,label=u'Groups',required=False)
199214
deadline = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1" }, label='Deadline', required=True)
200215
related_to = SearchableLiaisonStatementsField(label=u'Related Liaison Statement', required=False)
201216
submitted_date = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1" }, label='Submission date', required=True, initial=datetime.date.today())
@@ -221,6 +236,7 @@ class Meta:
221236
def __init__(self, user, *args, **kwargs):
222237
super(LiaisonModelForm, self).__init__(*args, **kwargs)
223238
self.user = user
239+
self.edit = False
224240
self.person = get_person_for_user(user)
225241
self.is_new = not self.instance.pk
226242

@@ -237,6 +253,18 @@ def __init__(self, user, *args, **kwargs):
237253
self.set_from_fields()
238254
self.set_to_fields()
239255

256+
def clean_from_groups(self):
257+
from_groups = self.cleaned_data.get('from_groups')
258+
if not from_groups:
259+
raise forms.ValidationError('You must specify a From Group')
260+
return from_groups
261+
262+
def clean_to_groups(self):
263+
to_groups = self.cleaned_data.get('to_groups')
264+
if not to_groups:
265+
raise forms.ValidationError('You must specify a To Group')
266+
return to_groups
267+
240268
def clean_from_contact(self):
241269
contact = self.cleaned_data.get('from_contact')
242270
try:
@@ -294,6 +322,7 @@ def save(self, *args, **kwargs):
294322

295323
self.save_related_liaisons()
296324
self.save_attachments()
325+
self.save_tags()
297326

298327
return self.instance
299328

@@ -350,6 +379,11 @@ def save_related_liaisons(self):
350379
if related.target not in new_related:
351380
related.delete()
352381

382+
def save_tags(self):
383+
'''Create tags as needed'''
384+
if self.instance.deadline and not self.instance.tags.filter(slug='taken'):
385+
self.instance.tags.add('required')
386+
353387
def set_from_fields(self):
354388
assert NotImplemented
355389

@@ -488,9 +522,9 @@ def set_from_fields(self):
488522
self.fields['from_groups'].choices = get_internal_choices(self.user)
489523
else:
490524
if has_role(self.user, "Secretariat"):
491-
queryset = Group.objects.filter(type="sdo", state="active").order_by('name')
525+
queryset = Group.objects.filter(type="sdo").order_by('name')
492526
else:
493-
queryset = Group.objects.filter(type="sdo", state="active", role__person=self.person, role__name__in=("liaiman", "auth")).distinct().order_by('name')
527+
queryset = Group.objects.filter(type="sdo", role__person=self.person, role__name__in=("liaiman", "auth")).distinct().order_by('name')
494528
self.fields['from_contact'].widget.attrs['readonly'] = True
495529
self.fields['from_groups'].queryset = queryset
496530

@@ -501,10 +535,10 @@ def set_to_fields(self):
501535
if self.instance.is_outgoing():
502536
# if the user is a Liaison Manager and nothing more, reduce to set to his SDOs
503537
if has_role(self.user, "Liaison Manager") and not self.person.role_set.filter(name__in=('ad','chair'),group__state='active'):
504-
queryset = Group.objects.filter(type="sdo", state="active", role__person=self.person, role__name="liaiman").distinct().order_by('name')
538+
queryset = Group.objects.filter(type="sdo", role__person=self.person, role__name="liaiman").distinct().order_by('name')
505539
else:
506540
# get all outgoing entities
507-
queryset = Group.objects.filter(type="sdo", state="active").order_by('name')
541+
queryset = Group.objects.filter(type="sdo").order_by('name')
508542
self.fields['to_groups'].queryset = queryset
509543
else:
510544
self.fields['to_groups'].choices = get_internal_choices(None)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import unicode_literals
3+
4+
from django.db import models, migrations
5+
6+
def create_required_tags(apps, schema_editor):
7+
LiaisonStatement = apps.get_model("liaisons", "LiaisonStatement")
8+
for s in LiaisonStatement.objects.filter(deadline__isnull=False):
9+
if not s.tags.filter(slug='taken'):
10+
s.tags.add('required')
11+
12+
class Migration(migrations.Migration):
13+
14+
dependencies = [
15+
('liaisons', '0007_auto_20151009_1220'),
16+
]
17+
18+
operations = [
19+
migrations.RunPython(create_required_tags),
20+
]
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import unicode_literals
3+
4+
from django.db import migrations
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('liaisons', '0008_auto_20151110_1352'),
11+
]
12+
13+
operations = [
14+
migrations.RemoveField(
15+
model_name='liaisonstatement',
16+
name='from_name',
17+
),
18+
migrations.RemoveField(
19+
model_name='liaisonstatement',
20+
name='to_name',
21+
),
22+
]

ietf/liaisons/models.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class LiaisonStatement(models.Model):
2828
from_groups = models.ManyToManyField(Group, blank=True, related_name='liaisonstatement_from_set')
2929
from_contact = models.ForeignKey(Email, blank=True, null=True)
3030
to_groups = models.ManyToManyField(Group, blank=True, related_name='liaisonstatement_to_set')
31-
to_contacts = models.CharField(max_length=255, help_text="Contacts at recipient body")
31+
to_contacts = models.CharField(max_length=255, help_text="Contacts at recipient group")
3232

3333
response_contacts = models.CharField(blank=True, max_length=255, help_text="Where to send a response") # RFC4053
3434
technical_contacts = models.CharField(blank=True, max_length=255, help_text="Who to contact for clarification") # RFC4053
@@ -44,10 +44,6 @@ class LiaisonStatement(models.Model):
4444
attachments = models.ManyToManyField(Document, through='LiaisonStatementAttachment', blank=True)
4545
state = models.ForeignKey(LiaisonStatementState, default='pending')
4646

47-
# remove these fields post upgrade
48-
from_name = models.CharField(max_length=255, help_text="Name of the sender body")
49-
to_name = models.CharField(max_length=255, help_text="Name of the recipient body")
50-
5147
def __unicode__(self):
5248
return self.title or u"<no title>"
5349

ietf/liaisons/tests.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,35 @@ def test_add_restrictions(self):
338338
r = self.client.get(url)
339339
self.assertEqual(r.status_code, 200)
340340

341+
def test_add_comment(self):
342+
make_test_data()
343+
liaison = make_liaison_models()
344+
345+
# test unauthorized
346+
url = urlreverse('ietf.liaisons.views.liaison_history',kwargs=dict(object_id=liaison.pk))
347+
addurl = urlreverse('ietf.liaisons.views.add_comment',kwargs=dict(object_id=liaison.pk))
348+
r = self.client.get(url)
349+
self.assertEqual(r.status_code, 200)
350+
q = PyQuery(r.content)
351+
self.assertEqual(len(q("a.btn:contains('Add Comment')")), 0)
352+
login_testing_unauthorized(self, "secretary", addurl)
353+
354+
# public comment
355+
self.client.login(username="secretary", password="secretary+password")
356+
comment = 'Test comment'
357+
r = self.client.post(addurl, dict(comment=comment))
358+
self.assertEqual(r.status_code,302)
359+
qs = liaison.liaisonstatementevent_set.filter(type='comment',desc=comment)
360+
self.assertTrue(qs.count(),1)
361+
362+
# private comment
363+
r = self.client.post(addurl, dict(comment='Private comment',private=True),follow=True)
364+
self.assertEqual(r.status_code,200)
365+
self.assertTrue('Private comment' in r.content)
366+
self.client.logout()
367+
r = self.client.get(url)
368+
self.assertFalse('Private comment' in r.content)
369+
341370
def test_taken_care_of(self):
342371
make_test_data()
343372
liaison = make_liaison_models()
@@ -721,7 +750,7 @@ def test_add_incoming_liaison(self):
721750
r = self.client.post(url,
722751
dict(from_groups=from_groups,
723752
from_contact=submitter.email_address(),
724-
to_groups=str(to_group.pk),
753+
to_groups=[str(to_group.pk)],
725754
to_contacts='to_contacts@example.com',
726755
technical_contacts="technical_contact@example.com",
727756
action_holder_contacts="action_holder_contacts@example.com",

ietf/liaisons/urls.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@
2020
# Views
2121
urlpatterns += patterns('ietf.liaisons.views',
2222
(r'^$', 'liaison_list'),
23-
(r'^(?P<state>(posted|pending|dead))/$', 'liaison_list'),
23+
(r'^(?P<state>(posted|pending|dead))/', 'liaison_list'),
2424
(r'^(?P<object_id>\d+)/$', 'liaison_detail'),
25+
(r'^(?P<object_id>\d+)/addcomment/$', 'add_comment'),
2526
(r'^(?P<object_id>\d+)/edit/$', 'liaison_edit'),
2627
(r'^(?P<object_id>\d+)/edit-attachment/(?P<doc_id>[A-Za-z0-9._+-]+)$', 'liaison_edit_attachment'),
2728
(r'^(?P<object_id>\d+)/delete-attachment/(?P<attach_id>[A-Za-z0-9._+-]+)$', 'liaison_delete_attachment'),

ietf/liaisons/views.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from ietf.liaisons.utils import (get_person_for_user, can_add_outgoing_liaison,
1919
can_add_incoming_liaison, can_edit_liaison,can_submit_liaison_required,
2020
can_add_liaison)
21-
from ietf.liaisons.forms import liaison_form_factory, SearchLiaisonForm, EditAttachmentForm
21+
from ietf.liaisons.forms import liaison_form_factory, SearchLiaisonForm, EditAttachmentForm, AddCommentForm
2222
from ietf.liaisons.mails import notify_pending_by_email, send_liaison_by_email
2323
from ietf.liaisons.fields import select2_id_liaison_json
2424
from ietf.name.models import LiaisonStatementTagName
@@ -288,6 +288,32 @@ def redirect_for_approval(request, object_id=None):
288288
# -------------------------------------------------
289289
# View Functions
290290
# -------------------------------------------------
291+
@role_required('Secretariat',)
292+
def add_comment(request, object_id):
293+
"""Add comment to history"""
294+
statement = get_object_or_404(LiaisonStatement, id=object_id)
295+
login = request.user.person
296+
297+
if request.method == 'POST':
298+
form = AddCommentForm(request.POST)
299+
if form.is_valid():
300+
if form.cleaned_data.get('private'):
301+
type_id = 'private_comment'
302+
else:
303+
type_id = 'comment'
304+
305+
LiaisonStatementEvent.objects.create(
306+
by=login,
307+
type_id=type_id,
308+
statement=statement,
309+
desc=form.cleaned_data['comment']
310+
)
311+
messages.success(request, 'Comment added.')
312+
return redirect("ietf.liaisons.views.liaison_history", object_id=statement.id)
313+
else:
314+
form = AddCommentForm()
315+
316+
return render(request, 'liaisons/add_comment.html',dict(liaison=statement,form=form))
291317

292318
@can_submit_liaison_required
293319
def liaison_add(request, type=None, **kwargs):
@@ -330,6 +356,8 @@ def liaison_history(request, object_id):
330356
"""Show the history for a specific liaison statement"""
331357
liaison = get_object_or_404(LiaisonStatement, id=object_id)
332358
events = liaison.liaisonstatementevent_set.all().order_by("-time", "-id").select_related("by")
359+
if not has_role(request.user, "Secretariat"):
360+
events = events.exclude(type='private_comment')
333361

334362
return render(request, "liaisons/detail_history.html", {
335363
'events':events,
@@ -439,6 +467,7 @@ def liaison_edit_attachment(request, object_id, doc_id):
439467
def liaison_list(request, state='posted'):
440468
"""A generic list view with tabs for different states: posted, pending, dead"""
441469
# use prefetch to speed up main liaison page load
470+
selected_menu_entry = state
442471
liaisons = LiaisonStatement.objects.filter(state=state).prefetch_related(
443472
Prefetch('from_groups',queryset=Group.objects.order_by('acronym').select_related('type'),to_attr='prefetched_from_groups'),
444473
Prefetch('to_groups',queryset=Group.objects.order_by('acronym').select_related('type'),to_attr='prefetched_to_groups'),
@@ -451,15 +480,20 @@ def liaison_list(request, state='posted'):
451480
msg = "Restricted to participants who are authorized to submit liaison statements on behalf of the various IETF entities"
452481
return HttpResponseForbidden(msg)
453482

483+
if 'tags' in request.GET:
484+
value = request.GET.get('tags')
485+
liaisons = liaisons.filter(tags__slug=value)
486+
selected_menu_entry = 'action needed'
487+
454488
# perform search / filter
455489
if 'text' in request.GET:
456-
form = SearchLiaisonForm(data=request.GET,state=state)
490+
form = SearchLiaisonForm(data=request.GET,queryset=liaisons)
457491
search_conducted = True
458492
if form.is_valid():
459493
results = form.get_results()
460494
liaisons = results
461495
else:
462-
form = SearchLiaisonForm(state=state)
496+
form = SearchLiaisonForm(queryset=liaisons)
463497
search_conducted = False
464498

465499
# perform sort
@@ -482,6 +516,7 @@ def liaison_list(request, state='posted'):
482516
entries = []
483517
entries.append(("Posted", urlreverse("ietf.liaisons.views.liaison_list", kwargs={'state':'posted'})))
484518
if can_add_liaison(request.user):
519+
entries.append(("Action Needed", urlreverse("ietf.liaisons.views.liaison_list", kwargs={'state':'posted'}) + '?tags=required'))
485520
entries.append(("Pending", urlreverse("ietf.liaisons.views.liaison_list", kwargs={'state':'pending'})))
486521
entries.append(("Dead", urlreverse("ietf.liaisons.views.liaison_list", kwargs={'state':'dead'})))
487522

@@ -494,7 +529,7 @@ def liaison_list(request, state='posted'):
494529

495530
return render(request, 'liaisons/liaison_base.html', {
496531
'liaisons':liaisons,
497-
'selected_menu_entry':state,
532+
'selected_menu_entry':selected_menu_entry,
498533
'menu_entries':entries,
499534
'menu_actions':actions,
500535
'sort':sort,
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import unicode_literals
3+
4+
from django.db import migrations
5+
6+
def populate_names(apps, schema_editor):
7+
LiaisonStatementEventTypeName = apps.get_model("name", "LiaisonStatementEventTypeName")
8+
LiaisonStatementEventTypeName.objects.create(slug="private_comment", order=10, name="Private Comment")
9+
10+
11+
class Migration(migrations.Migration):
12+
13+
dependencies = [
14+
('name', '0009_auto_20151021_1102'),
15+
]
16+
17+
operations = [
18+
migrations.RunPython(populate_names),
19+
]

0 commit comments

Comments
 (0)