Add utility script to send interim meeting minutes reminder. Add ability to include comments with meeting cancellation

- Legacy-Id: 11199
This commit is contained in:
Ryan Cross 2016-05-17 22:55:47 +00:00
parent 8fef55dc31
commit cecdea3d72
13 changed files with 287 additions and 17 deletions

View file

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

View file

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

View file

@ -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:

View file

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

View file

@ -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"""

View file

@ -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",

View file

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

View file

@ -73,6 +73,7 @@ urlpatterns = [
url(r'^interim/request/$', views.interim_request),
url(r'^interim/request/(?P<number>[A-Za-z0-9._+-]+)/$', views.interim_request_details),
url(r'^interim/request/(?P<number>[A-Za-z0-9._+-]+)/edit/$', views.interim_request_edit),
url(r'^interim/request/(?P<number>[A-Za-z0-9._+-]+)/cancel/$', views.interim_request_cancel),
url(r'^interim/pending/$', views.interim_pending),
url(r'^$', views.current_materials),
]

View file

@ -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,

View file

@ -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": [

View file

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

View file

@ -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 %}
<link rel="stylesheet" href="{% static 'select2/select2.css' %}">
<link rel="stylesheet" href="{% static 'select2-bootstrap-css/select2-bootstrap.min.css' %}">
<link rel="stylesheet" href="{% static 'bootstrap-datepicker/css/bootstrap-datepicker3.min.css' %}">
{% endblock %}
{% block content %}
{% origin %}
<h1>Cancel Interim Meeting {% if meeting.session_set.first.status.slug != "sched" %}Request{% endif %}</h1>
<form id="interim-request-cancel-form" role="form" method="post" class="form-horizontal">
{% csrf_token %}
{% bootstrap_form form layout='horizontal' %}
<div class="form-group"
{% buttons %}
<button type="submit" class="btn btn-primary">Submit</button>
<a class="btn btn-default pull-right" href="{% url 'ietf.meeting.views.upcoming' %}">Back</a>
{% endbuttons %}
</div>
</form>
{% endblock %}
{% block js %}
<script src="{% static 'bootstrap-datepicker/js/bootstrap-datepicker.min.js' %}"></script>
<script src="{% static 'select2/select2.min.js' %}"></script>
<script src="{% static 'ietf/js/meeting-interim-request.js' %}"></script>
{% endblock %}

View file

@ -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' %}
<input class="btn btn-default" type="submit" value="Cancel" name='cancel' />
<a class="btn btn-default" href="{% url 'ietf.meeting.views.interim_request_cancel' number=meeting.number %}">Cancel</a>
{% endif %}
{% endif %}
</form>