diff --git a/ietf/bin/interim_minutes_reminder b/ietf/bin/interim_minutes_reminder new file mode 100755 index 000000000..f4387e0a3 --- /dev/null +++ b/ietf/bin/interim_minutes_reminder @@ -0,0 +1,23 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# -*- Python -*- +# +''' +This script calls ietf.meeting.helpers.check_interim_minutes() which sends +a reminder email for interim meetings that occurred 10 days ago but still +don't have minutes. +''' + +# Set PYTHONPATH and load environment variables for standalone script ----------------- +import os, sys +basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")) +sys.path = [ basedir ] + sys.path +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ietf.settings") + +import django +django.setup() +# ------------------------------------------------------------------------------------- + +from ietf.meeting.helpers import check_interim_minutes + +check_interim_minutes() \ No newline at end of file diff --git a/ietf/mailtrigger/migrations/0004_auto_20160516_1659.py b/ietf/mailtrigger/migrations/0004_auto_20160516_1659.py new file mode 100644 index 000000000..4da1f0e0a --- /dev/null +++ b/ietf/mailtrigger/migrations/0004_auto_20160516_1659.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations + + +def make_recipients(apps): + Recipient = apps.get_model('mailtrigger', 'Recipient') + + rc = Recipient.objects.create + + rc(slug='group_secretaries', + desc="The group's secretaries", + template=None) + + +def make_mailtriggers(apps): + Recipient = apps.get_model('mailtrigger','Recipient') + MailTrigger = apps.get_model('mailtrigger','MailTrigger') + + def mt_factory(slug,desc,to_slugs,cc_slugs=[]): + + # Try to protect ourselves from typos + all_slugs = to_slugs[:] + all_slugs.extend(cc_slugs) + for recipient_slug in all_slugs: + try: + Recipient.objects.get(slug=recipient_slug) + except Recipient.DoesNotExist: + print "****Some rule tried to use",recipient_slug + raise + + m = MailTrigger.objects.create(slug=slug, desc=desc) + m.to = Recipient.objects.filter(slug__in=to_slugs) + m.cc = Recipient.objects.filter(slug__in=cc_slugs) + + mt_factory(slug='session_minutes_reminder', + desc="Recipients when a group is sent a reminder " + "to submit minutes for a session", + to_slugs=['group_chairs','group_secretaries'], + cc_slugs=['group_responsible_directors'] + ) + +def forward(apps, schema_editor): + make_recipients(apps) + make_mailtriggers(apps) + + +class Migration(migrations.Migration): + dependencies = [ + ('mailtrigger', '0003_merge_request_trigger'), + ] + + operations = [migrations.RunPython(forward)] diff --git a/ietf/mailtrigger/models.py b/ietf/mailtrigger/models.py index 7688193f0..6926fd205 100644 --- a/ietf/mailtrigger/models.py +++ b/ietf/mailtrigger/models.py @@ -157,6 +157,14 @@ class Recipient(models.Model): addrs.extend(Recipient.objects.get(slug='stream_managers').gather(**{'streams':['irtf']})) return addrs + def gather_group_secretaries(self, **kwargs): + addrs = [] + if 'group' in kwargs: + group = kwargs['group'] + if not group.acronym=='none': + addrs.extend(group.role_set.filter(name='secr').values_list('email__address',flat=True)) + return addrs + def gather_doc_group_responsible_directors(self, **kwargs): addrs = [] if 'doc' in kwargs: diff --git a/ietf/meeting/forms.py b/ietf/meeting/forms.py index 9cb060003..b2eaa1a27 100644 --- a/ietf/meeting/forms.py +++ b/ietf/meeting/forms.py @@ -360,3 +360,14 @@ class InterimAnnounceForm(forms.ModelForm): message.save() return message + + +class InterimCancelForm(forms.Form): + group = forms.CharField(max_length=255,required=False) + date = forms.DateField(required=False) + comments = forms.CharField(required=False, widget=forms.Textarea(attrs={'placeholder': 'enter optional comments here'})) + + def __init__(self, *args, **kwargs): + super(InterimCancelForm, self).__init__(*args, **kwargs) + self.fields['group'].widget.attrs['disabled'] = True + self.fields['date'].widget.attrs['disabled'] = True \ No newline at end of file diff --git a/ietf/meeting/helpers.py b/ietf/meeting/helpers.py index a31f5a40c..8b9cf95e3 100644 --- a/ietf/meeting/helpers.py +++ b/ietf/meeting/helpers.py @@ -549,6 +549,39 @@ def send_interim_cancellation_notice(meeting): context, cc=cc_list) + +def send_interim_minutes_reminder(meeting): + """Sends an email reminding chairs to submit minutes of interim *meeting*""" + session = meeting.session_set.first() + group = session.group + (to_email, cc_list) = gather_address_lists('session_minutes_reminder',group=group) + from_email = 'proceedings@ietf.org' + subject = 'Action Required: Minutes from {group} ({acronym}) {type} Interim Meeting on {date}'.format( + group=group.name, + acronym=group.acronym, + type=group.type.slug.upper(), + date=meeting.date.strftime('%Y-%m-%d')) + template = 'meeting/interim_minutes_reminder.txt' + context = locals() + send_mail(None, + to_email, + from_email, + subject, + template, + context, + cc=cc_list) + + +def check_interim_minutes(): + """Finds interim meetings that occured 10 days ago, if they don't + have minutes send a reminder.""" + date = datetime.datetime.today() - datetime.timedelta(days=10) + meetings = Meeting.objects.filter(type='interim', session__status='sched', date=date) + for meeting in meetings: + if not meeting.session_set.first().minutes(): + send_interim_minutes_reminder(meeting) + + def sessions_post_save(forms): """Helper function to perform various post save operations on each form of a InterimSessionModelForm formset""" diff --git a/ietf/meeting/test_data.py b/ietf/meeting/test_data.py index e18a37bb6..6bb8d53a9 100644 --- a/ietf/meeting/test_data.py +++ b/ietf/meeting/test_data.py @@ -16,7 +16,6 @@ def make_interim_meeting(group,date,status='sched'): attendees=10, requested_by=system_person, requested_duration=20, status_id=status, scheduled=datetime.datetime.now(),type_id="session") - #assign_interim_session(session,time) slot = TimeSlot.objects.create( meeting=meeting, type_id="session", diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index 29d23c83a..80299e014 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -14,8 +14,9 @@ from ietf.group.models import Group from ietf.meeting.helpers import can_approve_interim_request, can_view_interim_request from ietf.meeting.helpers import send_interim_approval_request from ietf.meeting.helpers import send_interim_cancellation_notice +from ietf.meeting.helpers import send_interim_minutes_reminder from ietf.meeting.models import Session, TimeSlot, Meeting -from ietf.meeting.test_data import make_meeting_test_data +from ietf.meeting.test_data import make_meeting_test_data, make_interim_meeting from ietf.name.models import SessionStatusName from ietf.utils.test_utils import TestCase, login_testing_unauthorized, unicontent from ietf.utils.mail import outbox @@ -794,13 +795,45 @@ class InterimTests(TestCase): def test_interim_request_cancel(self): make_meeting_test_data() - meeting = Meeting.objects.filter(type='interim',session__status='apprw',session__group__acronym='mars').first() - url = urlreverse('ietf.meeting.views.interim_request_details',kwargs={'number':meeting.number}) - login_testing_unauthorized(self,"secretary",url) - r = self.client.post(url,{'cancel':'Cancel'}) + meeting = Meeting.objects.filter(type='interim', session__status='apprw', session__group__acronym='mars').first() + url = urlreverse('ietf.meeting.views.interim_request_details', kwargs={'number': meeting.number}) + # ensure no cancel button for unauthorized user + self.client.login(username="ameschairman", password="ameschairman+password") + r = self.client.get(url) self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + self.assertEqual(len(q("a.btn:contains('Cancel')")),0) + # ensure cancel button for authorized user + self.client.login(username="marschairman", password="marschairman+password") + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + self.assertEqual(len(q("a.btn:contains('Cancel')")),1) + # ensure fail unauthorized + url = urlreverse('ietf.meeting.views.interim_request_cancel', kwargs={'number': meeting.number}) + comments = 'Bob cannot make it' + self.client.login(username="ameschairman", password="ameschairman+password") + r = self.client.post(url, {'comments': comments}) + self.assertEqual(r.status_code, 403) + # test cancelling before announcement + self.client.login(username="marschairman", password="marschairman+password") + length_before = len(outbox) + r = self.client.post(url, {'comments': comments}) + self.assertRedirects(r, urlreverse('ietf.meeting.views.upcoming')) for session in meeting.session_set.all(): - self.assertEqual(session.status_id,'canceledpa') + self.assertEqual(session.status_id, 'canceledpa') + self.assertEqual(session.comments, comments) + self.assertEqual(len(outbox), length_before) # no email notice + # test cancelling after announcement + meeting = Meeting.objects.filter(type='interim', session__status='sched', session__group__acronym='mars').first() + url = urlreverse('ietf.meeting.views.interim_request_cancel', kwargs={'number': meeting.number}) + r = self.client.post(url, {'comments': comments}) + self.assertRedirects(r, urlreverse('ietf.meeting.views.upcoming')) + for session in meeting.session_set.all(): + self.assertEqual(session.status_id, 'canceled') + self.assertEqual(session.comments, comments) + self.assertEqual(len(outbox), length_before + 1) + self.assertTrue('Interim Meeting Cancelled' in outbox[-1]['Subject']) def test_interim_request_edit(self): make_meeting_test_data() @@ -834,4 +867,14 @@ class InterimTests(TestCase): length_before = len(outbox) send_interim_cancellation_notice(meeting=meeting) self.assertEqual(len(outbox),length_before+1) - self.assertTrue('Interim Meeting Cancelled' in outbox[-1]['Subject']) \ No newline at end of file + self.assertTrue('Interim Meeting Cancelled' in outbox[-1]['Subject']) + + def test_send_interim_minutes_reminder(self): + make_meeting_test_data() + group = Group.objects.get(acronym='mars') + date = datetime.datetime.today() - datetime.timedelta(days=10) + meeting = make_interim_meeting(group=group, date=date, status='sched') + length_before = len(outbox) + send_interim_minutes_reminder(meeting=meeting) + self.assertEqual(len(outbox),length_before+1) + self.assertTrue('Action Required: Minutes' in outbox[-1]['Subject']) \ No newline at end of file diff --git a/ietf/meeting/urls.py b/ietf/meeting/urls.py index 583dd6224..b5a423154 100644 --- a/ietf/meeting/urls.py +++ b/ietf/meeting/urls.py @@ -73,6 +73,7 @@ urlpatterns = [ url(r'^interim/request/$', views.interim_request), url(r'^interim/request/(?P[A-Za-z0-9._+-]+)/$', views.interim_request_details), url(r'^interim/request/(?P[A-Za-z0-9._+-]+)/edit/$', views.interim_request_edit), + url(r'^interim/request/(?P[A-Za-z0-9._+-]+)/cancel/$', views.interim_request_cancel), url(r'^interim/pending/$', views.interim_pending), url(r'^$', views.current_materials), ] diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index fb382519d..9d96a7909 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -47,7 +47,8 @@ from ietf.utils.mail import send_mail_message from ietf.utils.pipe import pipe from ietf.utils.pdf import pdf_pages -from .forms import InterimMeetingModelForm, InterimAnnounceForm, InterimSessionModelForm +from .forms import (InterimMeetingModelForm, InterimAnnounceForm, InterimSessionModelForm, + InterimCancelForm) def get_menu_entries(request): @@ -1056,6 +1057,34 @@ def interim_request(request): "formset": formset}) +@role_required('Area Director', 'Secretariat', 'IRTF Chair', 'WG Chair', + 'RG Chair') +def interim_request_cancel(request, number): + '''View for cancelling an interim meeting request''' + meeting = get_object_or_404(Meeting, number=number) + group = meeting.session_set.first().group + if not can_view_interim_request(meeting, request.user): + return HttpResponseForbidden("You do not have permissions to cancel this meeting request") + + if request.method == 'POST': + form = InterimCancelForm(request.POST) + if form.is_valid(): + if 'comments' in form.changed_data: + meeting.session_set.update(comments=form.cleaned_data.get('comments')) + if meeting.session_set.first().status.slug == 'sched': + meeting.session_set.update(status_id='canceled') + send_interim_cancellation_notice(meeting) + else: + meeting.session_set.update(status_id='canceledpa') + messages.success(request, 'Interim meeting cancelled') + return redirect(upcoming) + else: + form = InterimCancelForm(initial={'group': group.acronym, 'date': meeting.date}) + + return render(request, "meeting/interim_request_cancel.html", { + "form": form}) + + @role_required('Area Director', 'Secretariat', 'IRTF Chair', 'WG Chair', 'RG Chair') def interim_request_details(request, number): @@ -1074,13 +1103,6 @@ def interim_request_details(request, number): if request.POST.get('disapprove'): meeting.session_set.update(status_id='disappr') messages.success(request, 'Interim meeting disapproved') - if request.POST.get('cancel'): - if meeting.session_set.first().status.slug == 'sched': - meeting.session_set.update(status_id='canceled') - send_interim_cancellation_notice(meeting) - else: - meeting.session_set.update(status_id='canceledpa') - messages.success(request, 'Interim meeting cancelled') return render(request, "meeting/interim_request_details.html", { "meeting": meeting, diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index 7dd4ed6fe..9d601c9bd 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -4679,6 +4679,14 @@ "model": "mailtrigger.recipient", "pk": "group_chairs" }, +{ + "fields": { + "template": null, + "desc": "The group's secretaries" + }, + "model": "mailtrigger.recipient", + "pk": "group_secretaries" +}, { "fields": { "template": "{{ changed_personnel | join:\", \" }}", @@ -5902,6 +5910,20 @@ "model": "mailtrigger.mailtrigger", "pk": "session_scheduled" }, +{ + "fields": { + "cc": [ + "group_responsible_directors" + ], + "to": [ + "group_chairs", + "group_secretaries" + ], + "desc": "Recipients when a group is sent a reminder to submit minutes for a session" + }, + "model": "mailtrigger.mailtrigger", + "pk": "session_minutes_reminder" +}, { "fields": { "cc": [ diff --git a/ietf/templates/meeting/interim_minutes_reminder.txt b/ietf/templates/meeting/interim_minutes_reminder.txt new file mode 100644 index 000000000..1cd984786 --- /dev/null +++ b/ietf/templates/meeting/interim_minutes_reminder.txt @@ -0,0 +1,16 @@ +{% load ams_filters %} + +Please note that we have not yet received minutes from the +{{ group.name }} ({{ group.acronym }}) interim meeting held +on {{ meeting.date|date:"Y-m-d"}}. As per the IESG Guidence on Interim Meetings, +Conference Calls and Jabber Sessions [1], detailed minutes must +be provided within 10 days of the event. + +At your earliest convenience, please upload meeting minutes, as +well as any presentations from your sessions by using the Meeting +Materials Manager found here: +https://datatracker.ietf.org/secr/proceedings/. +Alternatively, you are welcome to send them to proceedings@ietf.org +for manual posting. + +[1] http://www.ietf.org/iesg/statement/interim-meetings.html diff --git a/ietf/templates/meeting/interim_request_cancel.html b/ietf/templates/meeting/interim_request_cancel.html new file mode 100644 index 000000000..18ef575ce --- /dev/null +++ b/ietf/templates/meeting/interim_request_cancel.html @@ -0,0 +1,38 @@ +{% extends "base.html" %} +{# Copyright The IETF Trust 2015, All Rights Reserved #} +{% load origin %} +{% load staticfiles bootstrap3 widget_tweaks %} + +{% block title %}Cancel Interim Meeting {% if meeting.session_set.first.status.slug != "sched" %}Request{% endif %}{% endblock %} + +{% block pagehead %} + + + +{% endblock %} + +{% block content %} + {% origin %} +

Cancel Interim Meeting {% if meeting.session_set.first.status.slug != "sched" %}Request{% endif %}

+ +
+ {% csrf_token %} + + {% bootstrap_form form layout='horizontal' %} + +
Submit + Back + {% endbuttons %} +
+ +
+ +{% endblock %} + +{% block js %} + + + +{% endblock %} diff --git a/ietf/templates/meeting/interim_request_details.html b/ietf/templates/meeting/interim_request_details.html index 091a3d5cd..382057c48 100644 --- a/ietf/templates/meeting/interim_request_details.html +++ b/ietf/templates/meeting/interim_request_details.html @@ -53,7 +53,7 @@ {% endif %} {% if can_edit %} {% if sessions.0.status.slug == 'apprw' or sessions.0.status.slug == 'scheda' or sessions.0.status.slug == 'sched' %} - + Cancel {% endif %} {% endif %}