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 %}