Merged in [14468] from rjsparks@nostrum.com:

Add the ability to add a comment to a group's history. Fixes #1483.
 - Legacy-Id: 14485
Note: SVN reference [14468] has been migrated to Git commit 13e8f8982c
This commit is contained in:
Henrik Levkowetz 2018-01-01 00:42:10 +00:00
commit 7a0a99001c
10 changed files with 171 additions and 8 deletions

View file

@ -57,3 +57,12 @@ def email_milestones_changed(request, group, changes, states):
if (addrs.to or addrs.cc) and msg: if (addrs.to or addrs.cc) and msg:
wrap_up_email(addrs, 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)

View file

@ -738,6 +738,30 @@ class GroupEditTests(TestCase):
group = Group.objects.get(acronym=group.acronym) group = Group.objects.get(acronym=group.acronym)
self.assertEqual(group.state_id, "active") 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): class MilestoneTests(TestCase):
def create_test_milestones(self): def create_test_milestones(self):
draft = make_test_data() draft = make_test_data()

View file

@ -24,6 +24,7 @@ info_detail_urls = [
url(r'^about/status/edit/$', views.group_about_status_edit), url(r'^about/status/edit/$', views.group_about_status_edit),
url(r'^about/status/meeting/(?P<num>\d+)/$', views.group_about_status_meeting), url(r'^about/status/meeting/(?P<num>\d+)/$', views.group_about_status_meeting),
url(r'^history/$',views.history), url(r'^history/$',views.history),
url(r'^history/addcomment/$',views.add_comment),
url(r'^email/$', views.email), url(r'^email/$', views.email),
url(r'^deps/(?P<output_type>[\w-]+)/$', views.dependencies), url(r'^deps/(?P<output_type>[\w-]+)/$', views.dependencies),
url(r'^meetings/$', views.meetings), url(r'^meetings/$', views.meetings),

View file

@ -41,6 +41,7 @@ import datetime
from tempfile import mkstemp from tempfile import mkstemp
from collections import OrderedDict, defaultdict from collections import OrderedDict, defaultdict
from django import forms
from django.conf import settings from django.conf import settings
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.db.models.aggregates import Max 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, from ietf.group.forms import (GroupForm, StatusUpdateForm, ConcludeGroupForm, StreamEditForm,
ManageReviewRequestForm, EmailOpenAssignmentsForm, ReviewerSettingsForm, ManageReviewRequestForm, EmailOpenAssignmentsForm, ReviewerSettingsForm,
AddUnavailablePeriodForm, EndUnavailablePeriodForm, ReviewSecretarySettingsForm, ) 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.models import ( Group, Role, GroupEvent, GroupStateTransitions, GroupURL, ChangeStateGroupEvent )
from ietf.group.utils import (get_charter_text, can_manage_group_type, from ietf.group.utils import (get_charter_text, can_manage_group_type,
milestone_reviewer_for_group_type, can_provide_status_update, 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, save_group_in_history, can_manage_group,
get_group_or_404, setup_default_community_list_for_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.mailtrigger.utils import gather_relevant_expansions
from ietf.meeting.helpers import get_meeting from ietf.meeting.helpers import get_meeting
from ietf.meeting.utils import group_sessions 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) group = get_group_or_404(acronym, group_type)
events = group.groupevent_set.all().select_related('by').order_by('-time', '-id') 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', return render(request, 'group/history.html',
construct_group_menu_context(request, group, "history", group_type, { construct_group_menu_context(request, group, "history", group_type, {
"group": group,
"events": events, "events": events,
"can_add_comment": can_add_comment,
})) }))
def materials(request, acronym, group_type=None): 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, 'back_url': back_url,
'settings_form': settings_form, '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, })

View file

@ -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() 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): def is_individual_draft_author(user, doc):
if not user.is_authenticated: if not user.is_authenticated:

View file

@ -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)
]

View file

@ -2732,6 +2732,19 @@
"model": "mailtrigger.mailtrigger", "model": "mailtrigger.mailtrigger",
"pk": "doc_telechat_details_changed" "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": { "fields": {
"cc": [], "cc": [],
@ -3091,6 +3104,7 @@
"fields": { "fields": {
"cc": [ "cc": [
"doc_group_chairs", "doc_group_chairs",
"doc_group_mail_list",
"doc_notify", "doc_notify",
"doc_shepherd", "doc_shepherd",
"iesg_secretary" "iesg_secretary"
@ -8470,7 +8484,7 @@
}, },
{ {
"fields": { "fields": {
"default_offset_days": -89, "default_offset_days": -68,
"desc": "IETF Online Registration Opens", "desc": "IETF Online Registration Opens",
"name": "Registration Opens", "name": "Registration Opens",
"order": 0, "order": 0,
@ -9988,7 +10002,7 @@
"fields": { "fields": {
"command": "xym", "command": "xym",
"switch": "--version", "switch": "--version",
"time": "2017-09-29T00:07:40.668", "time": "2017-12-31T00:07:14.314",
"used": true, "used": true,
"version": "xym 0.4" "version": "xym 0.4"
}, },
@ -9999,7 +10013,7 @@
"fields": { "fields": {
"command": "pyang", "command": "pyang",
"switch": "--version", "switch": "--version",
"time": "2017-09-29T00:07:41.703", "time": "2017-12-31T00:07:15.241",
"used": true, "used": true,
"version": "pyang 1.7.3" "version": "pyang 1.7.3"
}, },
@ -10010,9 +10024,9 @@
"fields": { "fields": {
"command": "yanglint", "command": "yanglint",
"switch": "--version", "switch": "--version",
"time": "2017-09-29T00:07:41.812", "time": "2017-12-31T00:07:15.325",
"used": true, "used": true,
"version": "yanglint 0.13.49" "version": "yanglint 0.14.53"
}, },
"model": "utils.versioninfo", "model": "utils.versioninfo",
"pk": 3 "pk": 3

View file

@ -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 %}
<h1>Add comment<br><small>{{ group }} ({{ group.acronym }})</small></h1>
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
<p class="help-block">The comment will be added to the history trail for the group.</p>
{% buttons %}
<button type="submit" class="btn btn-primary">Submit</button>
<a class="btn btn-default pull-right" href="{% url "ietf.group.views.history" acronym=group.acronym %}">Back</a>
{% endbuttons %}
</form>
{% endblock %}

View file

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

View file

@ -10,6 +10,10 @@
{% block group_content %} {% block group_content %}
{% origin %} {% origin %}
<h2>Group History</h2>
{% if can_add_comment %}
<a class="btn btn-default" href="{% url 'ietf.group.views.add_comment' acronym=group.acronym %}"><span class="fa fa-plus"></span> Add comment</a>
{% endif %}
<table class="table table-condensed table-striped tablesorter"> <table class="table table-condensed table-striped tablesorter">
<thead> <thead>
<tr> <tr>
@ -35,4 +39,4 @@
{% block js %} {% block js %}
<script src="{% static "jquery.tablesorter/js/jquery.tablesorter.combined.min.js" %}"></script> <script src="{% static "jquery.tablesorter/js/jquery.tablesorter.combined.min.js" %}"></script>
{% endblock %} {% endblock %}