diff --git a/ietf/group/mails.py b/ietf/group/mails.py index 181fb563d..3db25f464 100644 --- a/ietf/group/mails.py +++ b/ietf/group/mails.py @@ -57,3 +57,12 @@ def email_milestones_changed(request, group, changes, states): if (addrs.to or addrs.cc) and msg: wrap_up_email(addrs, msg) +def email_comment(request, event): + (to, cc) = gather_address_lists('group_added_comment',group=event.group) + send_mail(request, to, None, "Comment added to %s history"%event.group.acronym, + "group/comment_added_email.txt", + dict( event = event, + group_url=settings.IDTRACKER_BASE_URL + event.group.about_url(), + ), + cc = cc) + diff --git a/ietf/group/tests_info.py b/ietf/group/tests_info.py index a6aa87223..f21d42562 100644 --- a/ietf/group/tests_info.py +++ b/ietf/group/tests_info.py @@ -738,6 +738,30 @@ class GroupEditTests(TestCase): group = Group.objects.get(acronym=group.acronym) self.assertEqual(group.state_id, "active") + def test_add_comment(self): + make_test_data() + group = Group.objects.get(acronym="mars") + url = urlreverse('ietf.group.views.add_comment', kwargs=dict(acronym=group.acronym)) + empty_outbox() + for username in ['secretary','ad','marschairman','marssecretary','marsdelegate']: + login_testing_unauthorized(self, username, url) + # get + r = self.client.get(url) + self.assertContains(r, "Add comment") + self.assertContains(r, group.acronym) + q = PyQuery(r.content) + self.assertEqual(len(q('form textarea[name=comment]')), 1) + # post + r = self.client.post(url, dict(comment="Test comment %s"%username)) + self.assertEqual(r.status_code, 302) + person = Person.objects.get(user__username=username) + self.assertTrue(GroupEvent.objects.filter(group=group,by=person,type='added_comment',desc='Test comment %s'%username).exists()) + self.client.logout() + self.client.login(username='ameschairman',password='ameschairman+password') + r=self.client.get(url) + self.assertEqual(r.status_code,403) + self.assertEqual(len(outbox),5) + class MilestoneTests(TestCase): def create_test_milestones(self): draft = make_test_data() diff --git a/ietf/group/urls.py b/ietf/group/urls.py index 0951698e7..6f02b8b27 100644 --- a/ietf/group/urls.py +++ b/ietf/group/urls.py @@ -24,6 +24,7 @@ info_detail_urls = [ url(r'^about/status/edit/$', views.group_about_status_edit), url(r'^about/status/meeting/(?P\d+)/$', views.group_about_status_meeting), url(r'^history/$',views.history), + url(r'^history/addcomment/$',views.add_comment), url(r'^email/$', views.email), url(r'^deps/(?P[\w-]+)/$', views.dependencies), url(r'^meetings/$', views.meetings), diff --git a/ietf/group/views.py b/ietf/group/views.py index e196385a8..19d6df343 100644 --- a/ietf/group/views.py +++ b/ietf/group/views.py @@ -41,6 +41,7 @@ import datetime from tempfile import mkstemp from collections import OrderedDict, defaultdict +from django import forms from django.conf import settings from django.contrib.auth.decorators import login_required from django.db.models.aggregates import Max @@ -65,7 +66,7 @@ from ietf.group.dot import make_dot from ietf.group.forms import (GroupForm, StatusUpdateForm, ConcludeGroupForm, StreamEditForm, ManageReviewRequestForm, EmailOpenAssignmentsForm, ReviewerSettingsForm, AddUnavailablePeriodForm, EndUnavailablePeriodForm, ReviewSecretarySettingsForm, ) -from ietf.group.mails import email_admin_re_charter, email_personnel_change +from ietf.group.mails import email_admin_re_charter, email_personnel_change, email_comment from ietf.group.models import ( Group, Role, GroupEvent, GroupStateTransitions, GroupURL, ChangeStateGroupEvent ) from ietf.group.utils import (get_charter_text, can_manage_group_type, milestone_reviewer_for_group_type, can_provide_status_update, @@ -74,7 +75,7 @@ from ietf.group.utils import (get_charter_text, can_manage_group_type, save_group_in_history, can_manage_group, get_group_or_404, setup_default_community_list_for_group, ) # -from ietf.ietfauth.utils import has_role +from ietf.ietfauth.utils import has_role, is_authorized_in_group from ietf.mailtrigger.utils import gather_relevant_expansions from ietf.meeting.helpers import get_meeting from ietf.meeting.utils import group_sessions @@ -632,10 +633,13 @@ def history(request, acronym, group_type=None): group = get_group_or_404(acronym, group_type) events = group.groupevent_set.all().select_related('by').order_by('-time', '-id') + can_add_comment = is_authorized_in_group(request.user,group) return render(request, 'group/history.html', construct_group_menu_context(request, group, "history", group_type, { + "group": group, "events": events, + "can_add_comment": can_add_comment, })) def materials(request, acronym, group_type=None): @@ -1784,4 +1788,24 @@ def change_review_secretary_settings(request, acronym, group_type=None): 'back_url': back_url, 'settings_form': settings_form, }) + +class AddCommentForm(forms.Form): + comment = forms.CharField(required=True, widget=forms.Textarea, strip=False) + +def add_comment(request, acronym, group_type=None): + group = get_group_or_404(acronym, group_type) + + if not is_authorized_in_group(request.user,group): + return HttpResponseForbidden("You need to a chair, secretary, or delegate of this group to add a comment.") + if request.method == 'POST': + form = AddCommentForm(request.POST) + if form.is_valid(): + comment = form.cleaned_data['comment'] + event = GroupEvent.objects.create(group=group,desc=comment,type="added_comment",by=request.user.person) + email_comment(request,event) + return redirect('ietf.group.views.history', acronym=group.acronym) + else: + form = AddCommentForm() + + return render(request, 'group/add_comment.html', { 'group':group, 'form':form, }) diff --git a/ietf/ietfauth/utils.py b/ietf/ietfauth/utils.py index f79846999..617354323 100644 --- a/ietf/ietfauth/utils.py +++ b/ietf/ietfauth/utils.py @@ -144,6 +144,26 @@ def is_authorized_in_doc_stream(user, doc): return Role.objects.filter(Q(name__in=("chair", "secr", "delegate", "auth"), person__user=user) & group_req).exists() +def is_authorized_in_group(user, group): + """Return whether user is authorized to perform duties on + a given group.""" + + if not user.is_authenticated: + return False + + if has_role(user, ["Secretariat",]): + return True + + if group.parent: + if group.parent.type_id == 'area' and has_role(user, ['Area Director',]): + return True + if group.parent.acronym == 'irtf' and has_role(user, ['IRTF Chair',]): + return True + if group.parent.acronym == 'iab' and has_role(user, ['IAB','IAB Executive Director',]): + return True + + return Role.objects.filter(name__in=("chair", "secr", "delegate", "auth"), person__user=user,group=group ).exists() + def is_individual_draft_author(user, doc): if not user.is_authenticated: diff --git a/ietf/mailtrigger/migrations/0011_group_added_comment.py b/ietf/mailtrigger/migrations/0011_group_added_comment.py new file mode 100644 index 000000000..444db5672 --- /dev/null +++ b/ietf/mailtrigger/migrations/0011_group_added_comment.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.8 on 2017-12-28 11:11 +from __future__ import unicode_literals + +from django.db import migrations + +def forward(apps, schema_editor): + MailTrigger = apps.get_model('mailtrigger','MailTrigger') + Recipient = apps.get_model('mailtrigger', 'Recipient') + group_added_comment = MailTrigger.objects.create( + slug='group_added_comment', + desc="Recipients when a comment is added to a group's history", + ) + group_added_comment.to = Recipient.objects.filter(slug__in=[ + 'group_chairs', + 'group_secretaries', + 'group_responsible_directors', + ]) + +def reverse(apps, schema_editor): + MailTrigger = apps.get_model('mailtrigger','MailTrigger') + MailTrigger.objects.filter(slug='group_added_comment').delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('mailtrigger', '0010_auto_20161207_1104'), + ] + + operations = [ + migrations.RunPython(forward, reverse) + ] diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index 23d222586..56b26c681 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -2732,6 +2732,19 @@ "model": "mailtrigger.mailtrigger", "pk": "doc_telechat_details_changed" }, + { + "fields": { + "cc": [], + "desc": "Recipients when a comment is added to a group's history", + "to": [ + "group_chairs", + "group_responsible_directors", + "group_secretaries" + ] + }, + "model": "mailtrigger.mailtrigger", + "pk": "group_added_comment" + }, { "fields": { "cc": [], @@ -3091,6 +3104,7 @@ "fields": { "cc": [ "doc_group_chairs", + "doc_group_mail_list", "doc_notify", "doc_shepherd", "iesg_secretary" @@ -8470,7 +8484,7 @@ }, { "fields": { - "default_offset_days": -89, + "default_offset_days": -68, "desc": "IETF Online Registration Opens", "name": "Registration Opens", "order": 0, @@ -9988,7 +10002,7 @@ "fields": { "command": "xym", "switch": "--version", - "time": "2017-09-29T00:07:40.668", + "time": "2017-12-31T00:07:14.314", "used": true, "version": "xym 0.4" }, @@ -9999,7 +10013,7 @@ "fields": { "command": "pyang", "switch": "--version", - "time": "2017-09-29T00:07:41.703", + "time": "2017-12-31T00:07:15.241", "used": true, "version": "pyang 1.7.3" }, @@ -10010,9 +10024,9 @@ "fields": { "command": "yanglint", "switch": "--version", - "time": "2017-09-29T00:07:41.812", + "time": "2017-12-31T00:07:15.325", "used": true, - "version": "yanglint 0.13.49" + "version": "yanglint 0.14.53" }, "model": "utils.versioninfo", "pk": 3 diff --git a/ietf/templates/group/add_comment.html b/ietf/templates/group/add_comment.html new file mode 100644 index 000000000..5a0af8577 --- /dev/null +++ b/ietf/templates/group/add_comment.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} +{# Copyright The IETF Trust 2015, All Rights Reserved #} +{% load origin %} + +{% load bootstrap3 %} + +{% block title %}Add comment for {{ group }}{% endblock %} + +{% block content %} + {% origin %} +

Add comment
{{ group }} ({{ group.acronym }})

+ +
+ {% csrf_token %} + {% bootstrap_form form %} +

The comment will be added to the history trail for the group.

+ + {% buttons %} + + Back + {% endbuttons %} +
+ +{% endblock %} diff --git a/ietf/templates/group/comment_added_email.txt b/ietf/templates/group/comment_added_email.txt new file mode 100644 index 000000000..1814a3228 --- /dev/null +++ b/ietf/templates/group/comment_added_email.txt @@ -0,0 +1,10 @@ +{% autoescape off %} +Please DO NOT reply to this email. + +{{event.by}} added the following comment to the history of {{event.group.acronym}}: + +{{ event.desc }} + +Information for the group can be found at {{ group_url }}. + +{% endautoescape%} diff --git a/ietf/templates/group/history.html b/ietf/templates/group/history.html index 3c8e23bec..91d393d1d 100644 --- a/ietf/templates/group/history.html +++ b/ietf/templates/group/history.html @@ -10,6 +10,10 @@ {% block group_content %} {% origin %} +

Group History

+ {% if can_add_comment %} + Add comment + {% endif %} @@ -35,4 +39,4 @@ {% block js %} -{% endblock %} \ No newline at end of file +{% endblock %}