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:
parent
75a3895dd9
commit
ca6512e4fa
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.')
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
29
ietf/templates/group/group_about_status.html
Normal file
29
ietf/templates/group/group_about_status.html
Normal 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 %}
|
32
ietf/templates/group/group_about_status_edit.html
Normal file
32
ietf/templates/group/group_about_status_edit.html
Normal 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 %}
|
Loading…
Reference in a new issue