Skip to content

Commit 6a40969

Browse files
committed
Merged in [10969] from rjsparks@nostrum.com:
Capture \'Status update\' summaries for groups that want to provide them. These updates show on the groups charter (or about) page, and in the group history. The most recent update provided before proceedings corrections closing date is included in the group's page in the meeting proceedings. This addresses the majority of ietf-tools#1773 (a ticket entered on behalf of the IESG). - Legacy-Id: 10990 Note: SVN reference [10969] has been migrated to Git commit ca6512e
2 parents 9b5e860 + 02af06c commit 6a40969

10 files changed

Lines changed: 280 additions & 4 deletions

File tree

ietf/group/factories.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,27 @@
11
import factory
22

3-
from ietf.group.models import Group
3+
from ietf.group.models import Group, Role, GroupEvent
44

55
class GroupFactory(factory.DjangoModelFactory):
66
class Meta:
77
model = Group
88

99
name = factory.Faker('sentence',nb_words=6)
1010
acronym = factory.Sequence(lambda n: 'acronym_%d' %n)
11+
12+
class RoleFactory(factory.DjangoModelFactory):
13+
class Meta:
14+
model = Role
15+
16+
group = factory.SubFactory(GroupFactory)
17+
person = factory.SubFactory('ietf.doc.factories.PersonFactory')
18+
email = factory.LazyAttribute(lambda obj: obj.person.email())
19+
20+
class GroupEventFactory(factory.DjangoModelFactory):
21+
class Meta:
22+
model = GroupEvent
23+
24+
group = factory.SubFactory(GroupFactory)
25+
by = factory.SubFactory('ietf.doc.factories.PersonFactory')
26+
type = 'comment'
27+
desc = factory.Faker('paragraph')

ietf/group/info.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import datetime
4040
from collections import OrderedDict
4141

42+
from django import forms
4243
from django.shortcuts import render, redirect
4344
from django.template.loader import render_to_string
4445
from django.http import HttpResponse, Http404, HttpResponseRedirect
@@ -54,9 +55,10 @@
5455
from ietf.doc.templatetags.ietf_filters import clean_whitespace
5556
from ietf.group.models import Group, Role, ChangeStateGroupEvent
5657
from ietf.name.models import GroupTypeName
57-
from ietf.group.utils import get_charter_text, can_manage_group_type, milestone_reviewer_for_group_type
58+
from ietf.group.utils import get_charter_text, can_manage_group_type, milestone_reviewer_for_group_type, can_provide_status_update
5859
from ietf.group.utils import can_manage_materials, get_group_or_404
5960
from ietf.utils.pipe import pipe
61+
from ietf.utils.textupload import get_cleaned_text_file_content
6062
from ietf.settings import MAILING_LIST_INFO_URL
6163
from ietf.mailtrigger.utils import gather_relevant_expansions
6264
from ietf.ietfauth.utils import has_role
@@ -489,14 +491,79 @@ def group_about(request, acronym, group_type=None):
489491

490492
can_manage = can_manage_group_type(request.user, group.type_id)
491493

494+
can_provide_update = can_provide_status_update(request.user, group)
495+
status_update = group.latest_event(type="status_update")
496+
497+
492498
return render(request, 'group/group_about.html',
493499
construct_group_menu_context(request, group, "charter" if group.features.has_chartering_process else "about", group_type, {
494500
"milestones_in_review": group.groupmilestone_set.filter(state="review"),
495501
"milestone_reviewer": milestone_reviewer_for_group_type(group_type),
496502
"requested_close": requested_close,
497503
"can_manage": can_manage,
504+
"can_provide_status_update": can_provide_update,
505+
"status_update": status_update,
498506
}))
499507

508+
def group_about_status(request, acronym, group_type=None):
509+
group = get_group_or_404(acronym, group_type)
510+
status_update = group.latest_event(type='status_update')
511+
can_provide_update = can_provide_status_update(request.user, group)
512+
return render(request, 'group/group_about_status.html',
513+
{ 'group' : group,
514+
'status_update': status_update,
515+
'can_provide_status_update': can_provide_update,
516+
}
517+
)
518+
519+
class StatusUpdateForm(forms.Form):
520+
content = forms.CharField(widget=forms.Textarea, label='Status update', help_text = 'Edit the status update', required=False)
521+
txt = forms.FileField(label='.txt format', help_text='Or upload a .txt file', required=False)
522+
523+
def clean_content(self):
524+
return self.cleaned_data['content'].replace('\r','')
525+
526+
def clean_txt(self):
527+
return get_cleaned_text_file_content(self.cleaned_data["txt"])
528+
529+
def group_about_status_edit(request, acronym, group_type=None):
530+
group = get_group_or_404(acronym, group_type)
531+
if not can_provide_status_update(request.user, group):
532+
raise Http404
533+
old_update = group.latest_event(type='status_update')
534+
535+
login = request.user.person
536+
537+
if request.method == 'POST':
538+
if 'submit_response' in request.POST:
539+
form = StatusUpdateForm(request.POST, request.FILES)
540+
if form.is_valid():
541+
from_file = form.cleaned_data['txt']
542+
if from_file:
543+
update_text = from_file
544+
else:
545+
update_text = form.cleaned_data['content']
546+
group.groupevent_set.create(
547+
by=login,
548+
type='status_update',
549+
desc=update_text,
550+
)
551+
return redirect('ietf.group.info.group_about',acronym=group.acronym)
552+
else:
553+
form = None
554+
else:
555+
form = None
556+
557+
if not form:
558+
form = StatusUpdateForm(initial={"content": old_update.desc if old_update else ""})
559+
560+
return render(request, 'group/group_about_status_edit.html',
561+
{
562+
'form': form,
563+
'group':group,
564+
}
565+
)
566+
500567
def check_group_email_aliases():
501568
pattern = re.compile('expand-(.*?)(-\w+)@.*? +(.*)$')
502569
tot_count = 0

ietf/group/tests_info.py

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import calendar
55
import datetime
66
import json
7+
import StringIO
78

89
from pyquery import PyQuery
910
from tempfile import NamedTemporaryFile
@@ -12,6 +13,10 @@
1213
from django.conf import settings
1314
from django.core.urlresolvers import reverse as urlreverse
1415
from django.core.urlresolvers import NoReverseMatch
16+
from django.contrib.auth.models import User
17+
18+
from django.utils.html import escape
19+
from django.template.defaultfilters import urlize
1520

1621
from ietf.doc.models import Document, DocAlias, DocEvent, State
1722
from ietf.group.models import Group, GroupEvent, GroupMilestone, GroupStateTransitions
@@ -22,7 +27,7 @@
2227
from ietf.utils.mail import outbox, empty_outbox
2328
from ietf.utils.test_data import make_test_data
2429
from ietf.utils.test_utils import login_testing_unauthorized
25-
from ietf.group.factories import GroupFactory
30+
from ietf.group.factories import GroupFactory, RoleFactory, GroupEventFactory
2631
from ietf.meeting.factories import SessionFactory
2732

2833
class GroupPagesTests(TestCase):
@@ -1016,3 +1021,88 @@ def test_meeting_info(self):
10161021
q = PyQuery(response.content)
10171022
self.assertFalse(q('#inprogressmeets'))
10181023

1024+
1025+
class StatusUpdateTests(TestCase):
1026+
1027+
def test_unsupported_group_types(self):
1028+
1029+
def ensure_updates_dont_show(group,user):
1030+
url = urlreverse('ietf.group.info.group_about',kwargs={'acronym':group.acronym})
1031+
if user:
1032+
self.client.login(username=user.username,password='%s+password'%user.username)
1033+
response = self.client.get(url)
1034+
self.assertEqual(response.status_code, 200)
1035+
q = PyQuery(response.content)
1036+
self.assertFalse(q('tr#status_update') )
1037+
self.client.logout()
1038+
1039+
def ensure_cant_edit(group,user):
1040+
url = urlreverse('ietf.group.info.group_about_status_edit',kwargs={'acronym':group.acronym})
1041+
if user:
1042+
self.client.login(username=user.username,password='%s+password'%user.username)
1043+
response = self.client.get(url)
1044+
self.assertEqual(response.status_code, 404)
1045+
self.client.logout()
1046+
1047+
for type_id in GroupTypeName.objects.exclude(slug__in=('wg','rg','team')).values_list('slug',flat=True):
1048+
group = GroupFactory.create(type_id=type_id)
1049+
for user in (None,User.objects.get(username='secretary')):
1050+
ensure_updates_dont_show(group,user)
1051+
ensure_cant_edit(group,user)
1052+
1053+
def test_see_status_update(self):
1054+
chair = RoleFactory(name_id='chair',group__type_id='wg')
1055+
GroupEventFactory(type='status_update',group=chair.group)
1056+
url = urlreverse('ietf.group.info.group_about',kwargs={'acronym':chair.group.acronym})
1057+
response = self.client.get(url)
1058+
self.assertEqual(response.status_code,200)
1059+
q=PyQuery(response.content)
1060+
self.assertTrue(q('tr#status_update'))
1061+
self.assertTrue(q('tr#status_update td a:contains("Show")'))
1062+
self.assertFalse(q('tr#status_update td a:contains("Edit")'))
1063+
self.client.login(username=chair.person.user.username,password='%s+password'%chair.person.user.username)
1064+
response = self.client.get(url)
1065+
self.assertEqual(response.status_code,200)
1066+
q=PyQuery(response.content)
1067+
self.assertTrue(q('tr#status_update td a:contains("Show")'))
1068+
self.assertTrue(q('tr#status_update td a:contains("Edit")'))
1069+
1070+
def test_view_status_update(self):
1071+
chair = RoleFactory(name_id='chair',group__type_id='wg')
1072+
event = GroupEventFactory(type='status_update',group=chair.group)
1073+
url = urlreverse('ietf.group.info.group_about_status',kwargs={'acronym':chair.group.acronym})
1074+
response = self.client.get(url)
1075+
self.assertEqual(response.status_code,200)
1076+
q=PyQuery(response.content)
1077+
self.assertTrue(urlize(escape(event.desc) in q('pre')))
1078+
self.assertFalse(q('a#edit_button'))
1079+
self.client.login(username=chair.person.user.username,password='%s+password'%chair.person.user.username)
1080+
response = self.client.get(url)
1081+
self.assertEqual(response.status_code,200)
1082+
q=PyQuery(response.content)
1083+
self.assertTrue(q('a#edit_button'))
1084+
1085+
def test_edit_status_update(self):
1086+
chair = RoleFactory(name_id='chair',group__type_id='wg')
1087+
event = GroupEventFactory(type='status_update',group=chair.group)
1088+
url = urlreverse('ietf.group.info.group_about_status_edit',kwargs={'acronym':chair.group.acronym})
1089+
response = self.client.get(url)
1090+
self.assertEqual(response.status_code,404)
1091+
self.client.login(username=chair.person.user.username,password='%s+password'%chair.person.user.username)
1092+
response = self.client.get(url)
1093+
self.assertEqual(response.status_code,200)
1094+
q=PyQuery(response.content)
1095+
self.assertTrue(event.desc in q('form textarea#id_content').text())
1096+
1097+
response = self.client.post(url,dict(content='Direct content typed into form',submit_response='1'))
1098+
self.assertEqual(response.status_code, 302)
1099+
self.assertEqual(chair.group.latest_event(type='status_update').desc,'Direct content typed into form')
1100+
1101+
test_file = StringIO.StringIO("This came from a file.")
1102+
test_file.name = "unnamed"
1103+
response = self.client.post(url,dict(txt=test_file,submit_response="1"))
1104+
self.assertEqual(response.status_code, 302)
1105+
self.assertEqual(chair.group.latest_event(type='status_update').desc,'This came from a file.')
1106+
1107+
1108+

ietf/group/urls_info_details.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
(r'^documents/$', 'ietf.group.info.group_documents', None, "group_docs"),
88
(r'^charter/$', 'ietf.group.info.group_about', None, 'group_charter'),
99
(r'^about/$', 'ietf.group.info.group_about', None, 'group_about'),
10+
(r'^about/status/$', 'ietf.group.info.group_about_status'),
11+
(r'^about/status/edit/$', 'ietf.group.info.group_about_status_edit'),
1012
(r'^history/$','ietf.group.info.history'),
1113
(r'^email/$', 'ietf.group.info.email'),
1214
(r'^deps/(?P<output_type>[\w-]+)/$', 'ietf.group.info.dependencies'),

ietf/group/utils.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@ def milestone_reviewer_for_group_type(group_type):
100100
def can_manage_materials(user, group):
101101
return has_role(user, 'Secretariat') or group.has_role(user, ("chair", "delegate", "secr", "matman"))
102102

103+
def can_provide_status_update(user, group):
104+
if not group.type_id in ['wg','rg','team']:
105+
return False
106+
return has_role(user, 'Secretariat') or group.has_role(user, ("chair", "delegate", "secr", "ad",))
107+
103108
def get_group_or_404(acronym, group_type):
104109
"""Helper to overcome the schism between group-type prefixed URLs and generic."""
105110
possible_groups = Group.objects.all()

ietf/secr/proceedings/proc_utils.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@
99
import os
1010
import shutil
1111

12+
import debug # pyflakes:ignore
13+
1214
from django.conf import settings
1315
from django.http import HttpRequest
1416
from django.shortcuts import render_to_response, render
17+
from django.db.utils import ConnectionDoesNotExist
1518

1619
from ietf.doc.models import Document, RelatedDocument, DocEvent, NewRevisionDocEvent, State
1720
from ietf.group.models import Group, Role
@@ -359,6 +362,7 @@ def create_proceedings(meeting, group, is_final=False):
359362
charter = None
360363
ctime = None
361364

365+
status_update = group.latest_event(type='status_update',time__lte=meeting.get_submission_correction_date())
362366

363367
# rather than return the response as in a typical view function we save it as the snapshot
364368
# proceedings.html
@@ -374,7 +378,8 @@ def create_proceedings(meeting, group, is_final=False):
374378
'tas': tas,
375379
'meeting': meeting,
376380
'rfcs': rfcs,
377-
'materials': materials}
381+
'materials': materials,
382+
'status_update': status_update,}
378383
)
379384

380385
# save proceedings
@@ -458,6 +463,12 @@ def gen_attendees(context):
458463

459464
attendees = Registration.objects.using('ietf' + meeting.number).all().order_by('lname')
460465

466+
if settings.SERVER_MODE!='production':
467+
try:
468+
attendees.count()
469+
except ConnectionDoesNotExist:
470+
attendees = Registration.objects.none()
471+
461472
html = render_to_response('proceedings/attendee.html',{
462473
'meeting': meeting,
463474
'attendees': attendees}

ietf/secr/templates/proceedings/proceedings.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ <h3>Technical Advisor(s)</h3>
8484
{% endif %}
8585
<br /><br /></td></tr></table>
8686

87+
{% if status_update %}
88+
<h3>Status Update (provided {{status_update.time|date:"Y-m-d"}})</h3>
89+
<pre class="pasted">{{status_update.desc|escape|urlize}}</pre>
90+
{% endif %}
91+
8792
<h3>Recordings:</h3>
8893
{% if materials.recording %}
8994
<ul>

ietf/templates/group/group_about.html

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,24 @@
6464
</tr>
6565
{% endif %}
6666

67+
{% if can_provide_status_update or status_update %}
68+
<tr id='status_update'>
69+
<td></td>
70+
<th>Status Update</th>
71+
<td>
72+
{% if status_update %}
73+
(last changed {{status_update.time|date:"Y-m-d"}})
74+
{% else %}
75+
(None)
76+
{% endif %}
77+
<a class="btn btn-default btn-xs" href="{% url "ietf.group.info.group_about_status" acronym=group.acronym %}">Show</a>
78+
{% if can_provide_status_update %}
79+
<a class="btn btn-default btn-xs" href="{% url "ietf.group.info.group_about_status_edit" acronym=group.acronym %}">Edit</a>
80+
{% endif %}
81+
</td>
82+
</tr>
83+
{% endif %}
84+
6785

6886
{% with group.groupurl_set.all as urls %}
6987
{% if urls %}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{% extends "base.html" %}
2+
{# Copyright The IETF Trust 2015, All Rights Reserved #}
3+
{% load origin %}
4+
{% load staticfiles %}
5+
{% load bootstrap3 %}
6+
{% load ietf_filters %}
7+
8+
{% block title %}
9+
Status update for {{ group.type.name }} {{ group.acronym }}
10+
{% endblock %}
11+
12+
{% block content %}
13+
{% origin %}
14+
<h1>
15+
Status update for {{ group.type.name }} {{ group.acronym }}
16+
</h1>
17+
18+
<pre class="pasted">{{ status_update.desc|default:"(none)"|escape|urlize }}</pre>
19+
20+
{% if can_provide_status_update %}
21+
<a id="edit_button" class="btn btn-primary" href="{% url "ietf.group.info.group_about_status_edit" acronym=group.acronym %}">Edit</a>
22+
{% endif %}
23+
<a class="btn btn-default pull-right" href="{% url "ietf.group.info.group_about" acronym=group.acronym %}">Back</a>
24+
25+
{% if can_provide_status_update %}
26+
<h2>About Status Updates</h2>
27+
<p>Capturing group status updates in the datatracker allows including them in meeting proceedings. This capability was added to address the IESG request at <a href="https://wiki.tools.ietf.org/tools/ietfdb/ticket/1773">ticket 1773</a>. Not all groups are expected to provide status updates. Those that do have historically sent messages by email or have placed them on a wiki. For example, see <a href="https://mailarchive.ietf.org/arch/msg/saag/fo2b3KA47SM4MuQuYj5VIh-Tjok">the Kitten report sent to SAAG for IETF94</a> or the <a href="https://trac.tools.ietf.org/area/rtg/trac/wiki/IETF94summary">Routing area high level summaries for IETF94</a>.</p>
28+
{% endif %}
29+
{% endblock %}

0 commit comments

Comments
 (0)