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:
parent
8fef55dc31
commit
cecdea3d72
23
ietf/bin/interim_minutes_reminder
Executable file
23
ietf/bin/interim_minutes_reminder
Executable 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()
|
54
ietf/mailtrigger/migrations/0004_auto_20160516_1659.py
Normal file
54
ietf/mailtrigger/migrations/0004_auto_20160516_1659.py
Normal 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)]
|
|
@ -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:
|
||||
|
|
|
@ -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
|
|
@ -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"""
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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'])
|
|
@ -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),
|
||||
]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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": [
|
||||
|
|
16
ietf/templates/meeting/interim_minutes_reminder.txt
Normal file
16
ietf/templates/meeting/interim_minutes_reminder.txt
Normal 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
|
38
ietf/templates/meeting/interim_request_cancel.html
Normal file
38
ietf/templates/meeting/interim_request_cancel.html
Normal 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 %}
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue