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 #1773 (a ticket entered on behalf of the IESG). Commit ready for merge.

- Legacy-Id: 10969
This commit is contained in:
Robert Sparks 2016-03-23 19:42:01 +00:00
parent 75a3895dd9
commit ca6512e4fa
10 changed files with 280 additions and 4 deletions

View file

@ -1,6 +1,6 @@
import factory
from ietf.group.models import Group
from ietf.group.models import Group, Role, GroupEvent
class GroupFactory(factory.DjangoModelFactory):
class Meta:
@ -8,3 +8,20 @@ class GroupFactory(factory.DjangoModelFactory):
name = factory.Faker('sentence',nb_words=6)
acronym = factory.Sequence(lambda n: 'acronym_%d' %n)
class RoleFactory(factory.DjangoModelFactory):
class Meta:
model = Role
group = factory.SubFactory(GroupFactory)
person = factory.SubFactory('ietf.doc.factories.PersonFactory')
email = factory.LazyAttribute(lambda obj: obj.person.email())
class GroupEventFactory(factory.DjangoModelFactory):
class Meta:
model = GroupEvent
group = factory.SubFactory(GroupFactory)
by = factory.SubFactory('ietf.doc.factories.PersonFactory')
type = 'comment'
desc = factory.Faker('paragraph')

View file

@ -39,6 +39,7 @@ from tempfile import mkstemp
import datetime
from collections import OrderedDict
from django import forms
from django.shortcuts import render, redirect
from django.template.loader import render_to_string
from django.http import HttpResponse, Http404, HttpResponseRedirect
@ -54,9 +55,10 @@ from ietf.doc.utils import get_chartering_type
from ietf.doc.templatetags.ietf_filters import clean_whitespace
from ietf.group.models import Group, Role, ChangeStateGroupEvent
from ietf.name.models import GroupTypeName
from ietf.group.utils import get_charter_text, can_manage_group_type, milestone_reviewer_for_group_type
from ietf.group.utils import get_charter_text, can_manage_group_type, milestone_reviewer_for_group_type, can_provide_status_update
from ietf.group.utils import can_manage_materials, get_group_or_404
from ietf.utils.pipe import pipe
from ietf.utils.textupload import get_cleaned_text_file_content
from ietf.settings import MAILING_LIST_INFO_URL
from ietf.mailtrigger.utils import gather_relevant_expansions
from ietf.ietfauth.utils import has_role
@ -489,14 +491,79 @@ def group_about(request, acronym, group_type=None):
can_manage = can_manage_group_type(request.user, group.type_id)
can_provide_update = can_provide_status_update(request.user, group)
status_update = group.latest_event(type="status_update")
return render(request, 'group/group_about.html',
construct_group_menu_context(request, group, "charter" if group.features.has_chartering_process else "about", group_type, {
"milestones_in_review": group.groupmilestone_set.filter(state="review"),
"milestone_reviewer": milestone_reviewer_for_group_type(group_type),
"requested_close": requested_close,
"can_manage": can_manage,
"can_provide_status_update": can_provide_update,
"status_update": status_update,
}))
def group_about_status(request, acronym, group_type=None):
group = get_group_or_404(acronym, group_type)
status_update = group.latest_event(type='status_update')
can_provide_update = can_provide_status_update(request.user, group)
return render(request, 'group/group_about_status.html',
{ 'group' : group,
'status_update': status_update,
'can_provide_status_update': can_provide_update,
}
)
class StatusUpdateForm(forms.Form):
content = forms.CharField(widget=forms.Textarea, label='Status update', help_text = 'Edit the status update', required=False)
txt = forms.FileField(label='.txt format', help_text='Or upload a .txt file', required=False)
def clean_content(self):
return self.cleaned_data['content'].replace('\r','')
def clean_txt(self):
return get_cleaned_text_file_content(self.cleaned_data["txt"])
def group_about_status_edit(request, acronym, group_type=None):
group = get_group_or_404(acronym, group_type)
if not can_provide_status_update(request.user, group):
raise Http404
old_update = group.latest_event(type='status_update')
login = request.user.person
if request.method == 'POST':
if 'submit_response' in request.POST:
form = StatusUpdateForm(request.POST, request.FILES)
if form.is_valid():
from_file = form.cleaned_data['txt']
if from_file:
update_text = from_file
else:
update_text = form.cleaned_data['content']
group.groupevent_set.create(
by=login,
type='status_update',
desc=update_text,
)
return redirect('ietf.group.info.group_about',acronym=group.acronym)
else:
form = None
else:
form = None
if not form:
form = StatusUpdateForm(initial={"content": old_update.desc if old_update else ""})
return render(request, 'group/group_about_status_edit.html',
{
'form': form,
'group':group,
}
)
def check_group_email_aliases():
pattern = re.compile('expand-(.*?)(-\w+)@.*? +(.*)$')
tot_count = 0

View file

@ -4,6 +4,7 @@ import shutil
import calendar
import datetime
import json
import StringIO
from pyquery import PyQuery
from tempfile import NamedTemporaryFile
@ -12,6 +13,10 @@ import debug # pyflakes:ignore
from django.conf import settings
from django.core.urlresolvers import reverse as urlreverse
from django.core.urlresolvers import NoReverseMatch
from django.contrib.auth.models import User
from django.utils.html import escape
from django.template.defaultfilters import urlize
from ietf.doc.models import Document, DocAlias, DocEvent, State
from ietf.group.models import Group, GroupEvent, GroupMilestone, GroupStateTransitions
@ -22,7 +27,7 @@ from ietf.utils.test_utils import TestCase, unicontent
from ietf.utils.mail import outbox, empty_outbox
from ietf.utils.test_data import make_test_data
from ietf.utils.test_utils import login_testing_unauthorized
from ietf.group.factories import GroupFactory
from ietf.group.factories import GroupFactory, RoleFactory, GroupEventFactory
from ietf.meeting.factories import SessionFactory
class GroupPagesTests(TestCase):
@ -1016,3 +1021,88 @@ class MeetingInfoTests(TestCase):
q = PyQuery(response.content)
self.assertFalse(q('#inprogressmeets'))
class StatusUpdateTests(TestCase):
def test_unsupported_group_types(self):
def ensure_updates_dont_show(group,user):
url = urlreverse('ietf.group.info.group_about',kwargs={'acronym':group.acronym})
if user:
self.client.login(username=user.username,password='%s+password'%user.username)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
q = PyQuery(response.content)
self.assertFalse(q('tr#status_update') )
self.client.logout()
def ensure_cant_edit(group,user):
url = urlreverse('ietf.group.info.group_about_status_edit',kwargs={'acronym':group.acronym})
if user:
self.client.login(username=user.username,password='%s+password'%user.username)
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
self.client.logout()
for type_id in GroupTypeName.objects.exclude(slug__in=('wg','rg','team')).values_list('slug',flat=True):
group = GroupFactory.create(type_id=type_id)
for user in (None,User.objects.get(username='secretary')):
ensure_updates_dont_show(group,user)
ensure_cant_edit(group,user)
def test_see_status_update(self):
chair = RoleFactory(name_id='chair',group__type_id='wg')
GroupEventFactory(type='status_update',group=chair.group)
url = urlreverse('ietf.group.info.group_about',kwargs={'acronym':chair.group.acronym})
response = self.client.get(url)
self.assertEqual(response.status_code,200)
q=PyQuery(response.content)
self.assertTrue(q('tr#status_update'))
self.assertTrue(q('tr#status_update td a:contains("Show")'))
self.assertFalse(q('tr#status_update td a:contains("Edit")'))
self.client.login(username=chair.person.user.username,password='%s+password'%chair.person.user.username)
response = self.client.get(url)
self.assertEqual(response.status_code,200)
q=PyQuery(response.content)
self.assertTrue(q('tr#status_update td a:contains("Show")'))
self.assertTrue(q('tr#status_update td a:contains("Edit")'))
def test_view_status_update(self):
chair = RoleFactory(name_id='chair',group__type_id='wg')
event = GroupEventFactory(type='status_update',group=chair.group)
url = urlreverse('ietf.group.info.group_about_status',kwargs={'acronym':chair.group.acronym})
response = self.client.get(url)
self.assertEqual(response.status_code,200)
q=PyQuery(response.content)
self.assertTrue(urlize(escape(event.desc) in q('pre')))
self.assertFalse(q('a#edit_button'))
self.client.login(username=chair.person.user.username,password='%s+password'%chair.person.user.username)
response = self.client.get(url)
self.assertEqual(response.status_code,200)
q=PyQuery(response.content)
self.assertTrue(q('a#edit_button'))
def test_edit_status_update(self):
chair = RoleFactory(name_id='chair',group__type_id='wg')
event = GroupEventFactory(type='status_update',group=chair.group)
url = urlreverse('ietf.group.info.group_about_status_edit',kwargs={'acronym':chair.group.acronym})
response = self.client.get(url)
self.assertEqual(response.status_code,404)
self.client.login(username=chair.person.user.username,password='%s+password'%chair.person.user.username)
response = self.client.get(url)
self.assertEqual(response.status_code,200)
q=PyQuery(response.content)
self.assertTrue(event.desc in q('form textarea#id_content').text())
response = self.client.post(url,dict(content='Direct content typed into form',submit_response='1'))
self.assertEqual(response.status_code, 302)
self.assertEqual(chair.group.latest_event(type='status_update').desc,'Direct content typed into form')
test_file = StringIO.StringIO("This came from a file.")
test_file.name = "unnamed"
response = self.client.post(url,dict(txt=test_file,submit_response="1"))
self.assertEqual(response.status_code, 302)
self.assertEqual(chair.group.latest_event(type='status_update').desc,'This came from a file.')

View file

@ -7,6 +7,8 @@ urlpatterns = patterns('',
(r'^documents/$', 'ietf.group.info.group_documents', None, "group_docs"),
(r'^charter/$', 'ietf.group.info.group_about', None, 'group_charter'),
(r'^about/$', 'ietf.group.info.group_about', None, 'group_about'),
(r'^about/status/$', 'ietf.group.info.group_about_status'),
(r'^about/status/edit/$', 'ietf.group.info.group_about_status_edit'),
(r'^history/$','ietf.group.info.history'),
(r'^email/$', 'ietf.group.info.email'),
(r'^deps/(?P<output_type>[\w-]+)/$', 'ietf.group.info.dependencies'),

View file

@ -100,6 +100,11 @@ def milestone_reviewer_for_group_type(group_type):
def can_manage_materials(user, group):
return has_role(user, 'Secretariat') or group.has_role(user, ("chair", "delegate", "secr", "matman"))
def can_provide_status_update(user, group):
if not group.type_id in ['wg','rg','team']:
return False
return has_role(user, 'Secretariat') or group.has_role(user, ("chair", "delegate", "secr", "ad",))
def get_group_or_404(acronym, group_type):
"""Helper to overcome the schism between group-type prefixed URLs and generic."""
possible_groups = Group.objects.all()

View file

@ -9,9 +9,12 @@ import glob
import os
import shutil
import debug # pyflakes:ignore
from django.conf import settings
from django.http import HttpRequest
from django.shortcuts import render_to_response, render
from django.db.utils import ConnectionDoesNotExist
from ietf.doc.models import Document, RelatedDocument, DocEvent, NewRevisionDocEvent, State
from ietf.group.models import Group, Role
@ -359,6 +362,7 @@ def create_proceedings(meeting, group, is_final=False):
charter = None
ctime = None
status_update = group.latest_event(type='status_update',time__lte=meeting.get_submission_correction_date())
# rather than return the response as in a typical view function we save it as the snapshot
# proceedings.html
@ -374,7 +378,8 @@ def create_proceedings(meeting, group, is_final=False):
'tas': tas,
'meeting': meeting,
'rfcs': rfcs,
'materials': materials}
'materials': materials,
'status_update': status_update,}
)
# save proceedings
@ -458,6 +463,12 @@ def gen_attendees(context):
attendees = Registration.objects.using('ietf' + meeting.number).all().order_by('lname')
if settings.SERVER_MODE!='production':
try:
attendees.count()
except ConnectionDoesNotExist:
attendees = Registration.objects.none()
html = render_to_response('proceedings/attendee.html',{
'meeting': meeting,
'attendees': attendees}

View file

@ -84,6 +84,11 @@ and end with
{% endif %}
<br /><br /></td></tr></table>
{% if status_update %}
<h3>Status Update (provided {{status_update.time|date:"Y-m-d"}})</h3>
<pre class="pasted">{{status_update.desc|escape|urlize}}</pre>
{% endif %}
<h3>Recordings:</h3>
{% if materials.recording %}
<ul>

View file

@ -64,6 +64,24 @@
</tr>
{% endif %}
{% if can_provide_status_update or status_update %}
<tr id='status_update'>
<td></td>
<th>Status Update</th>
<td>
{% if status_update %}
(last changed {{status_update.time|date:"Y-m-d"}})
{% else %}
(None)
{% endif %}
<a class="btn btn-default btn-xs" href="{% url "ietf.group.info.group_about_status" acronym=group.acronym %}">Show</a>
{% if can_provide_status_update %}
<a class="btn btn-default btn-xs" href="{% url "ietf.group.info.group_about_status_edit" acronym=group.acronym %}">Edit</a>
{% endif %}
</td>
</tr>
{% endif %}
{% with group.groupurl_set.all as urls %}
{% if urls %}

View file

@ -0,0 +1,29 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load staticfiles %}
{% load bootstrap3 %}
{% load ietf_filters %}
{% block title %}
Status update for {{ group.type.name }} {{ group.acronym }}
{% endblock %}
{% block content %}
{% origin %}
<h1>
Status update for {{ group.type.name }} {{ group.acronym }}
</h1>
<pre class="pasted">{{ status_update.desc|default:"(none)"|escape|urlize }}</pre>
{% if can_provide_status_update %}
<a id="edit_button" class="btn btn-primary" href="{% url "ietf.group.info.group_about_status_edit" acronym=group.acronym %}">Edit</a>
{% endif %}
<a class="btn btn-default pull-right" href="{% url "ietf.group.info.group_about" acronym=group.acronym %}">Back</a>
{% if can_provide_status_update %}
<h2>About Status Updates</h2>
<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>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,32 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load bootstrap3 %}
{% block title %}
Edit status for {{ group.type.name }} ({{group.acronym}})
{% endblock %}
{% block content %}
{% origin %}
<h1>
Edit status for {{ group.type.name }} ({{group.acronym}})
</h1>
<form enctype="multipart/form-data" method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button type="submit" class="btn btn-primary" name="submit_response" value="Submit">Submit</button>
<a class="btn btn-default pull-right" href="{% url "ietf.group.info.group_about" acronym=group.acronym %}">Back</a>
{% endbuttons %}
</form>
<h2>About Status Updates</h2>
<p>Capturing group status updates in the datatracker allows including them in meeting proceedings. This capability wa
s 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>
{% endblock %}