From ca6512e4fa8e023dfa868fcf84bd9453c954053d Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 23 Mar 2016 19:42:01 +0000 Subject: [PATCH] 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 --- ietf/group/factories.py | 19 +++- ietf/group/info.py | 69 +++++++++++++- ietf/group/tests_info.py | 92 ++++++++++++++++++- ietf/group/urls_info_details.py | 2 + ietf/group/utils.py | 5 + ietf/secr/proceedings/proc_utils.py | 13 ++- .../templates/proceedings/proceedings.html | 5 + ietf/templates/group/group_about.html | 18 ++++ ietf/templates/group/group_about_status.html | 29 ++++++ .../group/group_about_status_edit.html | 32 +++++++ 10 files changed, 280 insertions(+), 4 deletions(-) create mode 100644 ietf/templates/group/group_about_status.html create mode 100644 ietf/templates/group/group_about_status_edit.html diff --git a/ietf/group/factories.py b/ietf/group/factories.py index ca2a37a9b..da9968823 100644 --- a/ietf/group/factories.py +++ b/ietf/group/factories.py @@ -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') diff --git a/ietf/group/info.py b/ietf/group/info.py index fe66fc626..c0a82a19d 100644 --- a/ietf/group/info.py +++ b/ietf/group/info.py @@ -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 diff --git a/ietf/group/tests_info.py b/ietf/group/tests_info.py index 5dc560098..d07053f18 100644 --- a/ietf/group/tests_info.py +++ b/ietf/group/tests_info.py @@ -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.') + + + diff --git a/ietf/group/urls_info_details.py b/ietf/group/urls_info_details.py index 86b41b37e..4b66269f6 100644 --- a/ietf/group/urls_info_details.py +++ b/ietf/group/urls_info_details.py @@ -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[\w-]+)/$', 'ietf.group.info.dependencies'), diff --git a/ietf/group/utils.py b/ietf/group/utils.py index 9e2a37a75..9d56c70ec 100644 --- a/ietf/group/utils.py +++ b/ietf/group/utils.py @@ -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() diff --git a/ietf/secr/proceedings/proc_utils.py b/ietf/secr/proceedings/proc_utils.py index 00c5b256e..37a732daf 100644 --- a/ietf/secr/proceedings/proc_utils.py +++ b/ietf/secr/proceedings/proc_utils.py @@ -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} diff --git a/ietf/secr/templates/proceedings/proceedings.html b/ietf/secr/templates/proceedings/proceedings.html index 63dadb7c7..fa4cb5478 100644 --- a/ietf/secr/templates/proceedings/proceedings.html +++ b/ietf/secr/templates/proceedings/proceedings.html @@ -84,6 +84,11 @@ and end with {% endif %}

+{% if status_update %} +

Status Update (provided {{status_update.time|date:"Y-m-d"}})

+
{{status_update.desc|escape|urlize}}
+{% endif %} +

Recordings:

{% if materials.recording %}