Skip to content

Commit cecdea3

Browse files
committed
Add utility script to send interim meeting minutes reminder. Add ability to include comments with meeting cancellation
- Legacy-Id: 11199
1 parent 8fef55d commit cecdea3

13 files changed

Lines changed: 287 additions & 17 deletions

File tree

ietf/bin/interim_minutes_reminder

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
# -*- Python -*-
4+
#
5+
'''
6+
This script calls ietf.meeting.helpers.check_interim_minutes() which sends
7+
a reminder email for interim meetings that occurred 10 days ago but still
8+
don't have minutes.
9+
'''
10+
11+
# Set PYTHONPATH and load environment variables for standalone script -----------------
12+
import os, sys
13+
basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
14+
sys.path = [ basedir ] + sys.path
15+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ietf.settings")
16+
17+
import django
18+
django.setup()
19+
# -------------------------------------------------------------------------------------
20+
21+
from ietf.meeting.helpers import check_interim_minutes
22+
23+
check_interim_minutes()
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import unicode_literals
3+
4+
from django.db import migrations
5+
6+
7+
def make_recipients(apps):
8+
Recipient = apps.get_model('mailtrigger', 'Recipient')
9+
10+
rc = Recipient.objects.create
11+
12+
rc(slug='group_secretaries',
13+
desc="The group's secretaries",
14+
template=None)
15+
16+
17+
def make_mailtriggers(apps):
18+
Recipient = apps.get_model('mailtrigger','Recipient')
19+
MailTrigger = apps.get_model('mailtrigger','MailTrigger')
20+
21+
def mt_factory(slug,desc,to_slugs,cc_slugs=[]):
22+
23+
# Try to protect ourselves from typos
24+
all_slugs = to_slugs[:]
25+
all_slugs.extend(cc_slugs)
26+
for recipient_slug in all_slugs:
27+
try:
28+
Recipient.objects.get(slug=recipient_slug)
29+
except Recipient.DoesNotExist:
30+
print "****Some rule tried to use",recipient_slug
31+
raise
32+
33+
m = MailTrigger.objects.create(slug=slug, desc=desc)
34+
m.to = Recipient.objects.filter(slug__in=to_slugs)
35+
m.cc = Recipient.objects.filter(slug__in=cc_slugs)
36+
37+
mt_factory(slug='session_minutes_reminder',
38+
desc="Recipients when a group is sent a reminder "
39+
"to submit minutes for a session",
40+
to_slugs=['group_chairs','group_secretaries'],
41+
cc_slugs=['group_responsible_directors']
42+
)
43+
44+
def forward(apps, schema_editor):
45+
make_recipients(apps)
46+
make_mailtriggers(apps)
47+
48+
49+
class Migration(migrations.Migration):
50+
dependencies = [
51+
('mailtrigger', '0003_merge_request_trigger'),
52+
]
53+
54+
operations = [migrations.RunPython(forward)]

ietf/mailtrigger/models.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,14 @@ def gather_group_responsible_directors(self, **kwargs):
157157
addrs.extend(Recipient.objects.get(slug='stream_managers').gather(**{'streams':['irtf']}))
158158
return addrs
159159

160+
def gather_group_secretaries(self, **kwargs):
161+
addrs = []
162+
if 'group' in kwargs:
163+
group = kwargs['group']
164+
if not group.acronym=='none':
165+
addrs.extend(group.role_set.filter(name='secr').values_list('email__address',flat=True))
166+
return addrs
167+
160168
def gather_doc_group_responsible_directors(self, **kwargs):
161169
addrs = []
162170
if 'doc' in kwargs:

ietf/meeting/forms.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,3 +360,14 @@ def save(self, *args, **kwargs):
360360
message.save()
361361

362362
return message
363+
364+
365+
class InterimCancelForm(forms.Form):
366+
group = forms.CharField(max_length=255,required=False)
367+
date = forms.DateField(required=False)
368+
comments = forms.CharField(required=False, widget=forms.Textarea(attrs={'placeholder': 'enter optional comments here'}))
369+
370+
def __init__(self, *args, **kwargs):
371+
super(InterimCancelForm, self).__init__(*args, **kwargs)
372+
self.fields['group'].widget.attrs['disabled'] = True
373+
self.fields['date'].widget.attrs['disabled'] = True

ietf/meeting/helpers.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,39 @@ def send_interim_cancellation_notice(meeting):
549549
context,
550550
cc=cc_list)
551551

552+
553+
def send_interim_minutes_reminder(meeting):
554+
"""Sends an email reminding chairs to submit minutes of interim *meeting*"""
555+
session = meeting.session_set.first()
556+
group = session.group
557+
(to_email, cc_list) = gather_address_lists('session_minutes_reminder',group=group)
558+
from_email = 'proceedings@ietf.org'
559+
subject = 'Action Required: Minutes from {group} ({acronym}) {type} Interim Meeting on {date}'.format(
560+
group=group.name,
561+
acronym=group.acronym,
562+
type=group.type.slug.upper(),
563+
date=meeting.date.strftime('%Y-%m-%d'))
564+
template = 'meeting/interim_minutes_reminder.txt'
565+
context = locals()
566+
send_mail(None,
567+
to_email,
568+
from_email,
569+
subject,
570+
template,
571+
context,
572+
cc=cc_list)
573+
574+
575+
def check_interim_minutes():
576+
"""Finds interim meetings that occured 10 days ago, if they don't
577+
have minutes send a reminder."""
578+
date = datetime.datetime.today() - datetime.timedelta(days=10)
579+
meetings = Meeting.objects.filter(type='interim', session__status='sched', date=date)
580+
for meeting in meetings:
581+
if not meeting.session_set.first().minutes():
582+
send_interim_minutes_reminder(meeting)
583+
584+
552585
def sessions_post_save(forms):
553586
"""Helper function to perform various post save operations on each form of a
554587
InterimSessionModelForm formset"""

ietf/meeting/test_data.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ def make_interim_meeting(group,date,status='sched'):
1616
attendees=10, requested_by=system_person,
1717
requested_duration=20, status_id=status,
1818
scheduled=datetime.datetime.now(),type_id="session")
19-
#assign_interim_session(session,time)
2019
slot = TimeSlot.objects.create(
2120
meeting=meeting,
2221
type_id="session",

ietf/meeting/tests_views.py

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
from ietf.meeting.helpers import can_approve_interim_request, can_view_interim_request
1515
from ietf.meeting.helpers import send_interim_approval_request
1616
from ietf.meeting.helpers import send_interim_cancellation_notice
17+
from ietf.meeting.helpers import send_interim_minutes_reminder
1718
from ietf.meeting.models import Session, TimeSlot, Meeting
18-
from ietf.meeting.test_data import make_meeting_test_data
19+
from ietf.meeting.test_data import make_meeting_test_data, make_interim_meeting
1920
from ietf.name.models import SessionStatusName
2021
from ietf.utils.test_utils import TestCase, login_testing_unauthorized, unicontent
2122
from ietf.utils.mail import outbox
@@ -794,13 +795,45 @@ def test_interim_request_disapprove(self):
794795

795796
def test_interim_request_cancel(self):
796797
make_meeting_test_data()
797-
meeting = Meeting.objects.filter(type='interim',session__status='apprw',session__group__acronym='mars').first()
798-
url = urlreverse('ietf.meeting.views.interim_request_details',kwargs={'number':meeting.number})
799-
login_testing_unauthorized(self,"secretary",url)
800-
r = self.client.post(url,{'cancel':'Cancel'})
798+
meeting = Meeting.objects.filter(type='interim', session__status='apprw', session__group__acronym='mars').first()
799+
url = urlreverse('ietf.meeting.views.interim_request_details', kwargs={'number': meeting.number})
800+
# ensure no cancel button for unauthorized user
801+
self.client.login(username="ameschairman", password="ameschairman+password")
802+
r = self.client.get(url)
801803
self.assertEqual(r.status_code, 200)
804+
q = PyQuery(r.content)
805+
self.assertEqual(len(q("a.btn:contains('Cancel')")),0)
806+
# ensure cancel button for authorized user
807+
self.client.login(username="marschairman", password="marschairman+password")
808+
r = self.client.get(url)
809+
self.assertEqual(r.status_code, 200)
810+
q = PyQuery(r.content)
811+
self.assertEqual(len(q("a.btn:contains('Cancel')")),1)
812+
# ensure fail unauthorized
813+
url = urlreverse('ietf.meeting.views.interim_request_cancel', kwargs={'number': meeting.number})
814+
comments = 'Bob cannot make it'
815+
self.client.login(username="ameschairman", password="ameschairman+password")
816+
r = self.client.post(url, {'comments': comments})
817+
self.assertEqual(r.status_code, 403)
818+
# test cancelling before announcement
819+
self.client.login(username="marschairman", password="marschairman+password")
820+
length_before = len(outbox)
821+
r = self.client.post(url, {'comments': comments})
822+
self.assertRedirects(r, urlreverse('ietf.meeting.views.upcoming'))
802823
for session in meeting.session_set.all():
803-
self.assertEqual(session.status_id,'canceledpa')
824+
self.assertEqual(session.status_id, 'canceledpa')
825+
self.assertEqual(session.comments, comments)
826+
self.assertEqual(len(outbox), length_before) # no email notice
827+
# test cancelling after announcement
828+
meeting = Meeting.objects.filter(type='interim', session__status='sched', session__group__acronym='mars').first()
829+
url = urlreverse('ietf.meeting.views.interim_request_cancel', kwargs={'number': meeting.number})
830+
r = self.client.post(url, {'comments': comments})
831+
self.assertRedirects(r, urlreverse('ietf.meeting.views.upcoming'))
832+
for session in meeting.session_set.all():
833+
self.assertEqual(session.status_id, 'canceled')
834+
self.assertEqual(session.comments, comments)
835+
self.assertEqual(len(outbox), length_before + 1)
836+
self.assertTrue('Interim Meeting Cancelled' in outbox[-1]['Subject'])
804837

805838
def test_interim_request_edit(self):
806839
make_meeting_test_data()
@@ -834,4 +867,14 @@ def test_send_interim_cancellation_notice(self):
834867
length_before = len(outbox)
835868
send_interim_cancellation_notice(meeting=meeting)
836869
self.assertEqual(len(outbox),length_before+1)
837-
self.assertTrue('Interim Meeting Cancelled' in outbox[-1]['Subject'])
870+
self.assertTrue('Interim Meeting Cancelled' in outbox[-1]['Subject'])
871+
872+
def test_send_interim_minutes_reminder(self):
873+
make_meeting_test_data()
874+
group = Group.objects.get(acronym='mars')
875+
date = datetime.datetime.today() - datetime.timedelta(days=10)
876+
meeting = make_interim_meeting(group=group, date=date, status='sched')
877+
length_before = len(outbox)
878+
send_interim_minutes_reminder(meeting=meeting)
879+
self.assertEqual(len(outbox),length_before+1)
880+
self.assertTrue('Action Required: Minutes' in outbox[-1]['Subject'])

ietf/meeting/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
url(r'^interim/request/$', views.interim_request),
7474
url(r'^interim/request/(?P<number>[A-Za-z0-9._+-]+)/$', views.interim_request_details),
7575
url(r'^interim/request/(?P<number>[A-Za-z0-9._+-]+)/edit/$', views.interim_request_edit),
76+
url(r'^interim/request/(?P<number>[A-Za-z0-9._+-]+)/cancel/$', views.interim_request_cancel),
7677
url(r'^interim/pending/$', views.interim_pending),
7778
url(r'^$', views.current_materials),
7879
]

ietf/meeting/views.py

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@
4747
from ietf.utils.pipe import pipe
4848
from ietf.utils.pdf import pdf_pages
4949

50-
from .forms import InterimMeetingModelForm, InterimAnnounceForm, InterimSessionModelForm
50+
from .forms import (InterimMeetingModelForm, InterimAnnounceForm, InterimSessionModelForm,
51+
InterimCancelForm)
5152

5253

5354
def get_menu_entries(request):
@@ -1056,6 +1057,34 @@ def interim_request(request):
10561057
"formset": formset})
10571058

10581059

1060+
@role_required('Area Director', 'Secretariat', 'IRTF Chair', 'WG Chair',
1061+
'RG Chair')
1062+
def interim_request_cancel(request, number):
1063+
'''View for cancelling an interim meeting request'''
1064+
meeting = get_object_or_404(Meeting, number=number)
1065+
group = meeting.session_set.first().group
1066+
if not can_view_interim_request(meeting, request.user):
1067+
return HttpResponseForbidden("You do not have permissions to cancel this meeting request")
1068+
1069+
if request.method == 'POST':
1070+
form = InterimCancelForm(request.POST)
1071+
if form.is_valid():
1072+
if 'comments' in form.changed_data:
1073+
meeting.session_set.update(comments=form.cleaned_data.get('comments'))
1074+
if meeting.session_set.first().status.slug == 'sched':
1075+
meeting.session_set.update(status_id='canceled')
1076+
send_interim_cancellation_notice(meeting)
1077+
else:
1078+
meeting.session_set.update(status_id='canceledpa')
1079+
messages.success(request, 'Interim meeting cancelled')
1080+
return redirect(upcoming)
1081+
else:
1082+
form = InterimCancelForm(initial={'group': group.acronym, 'date': meeting.date})
1083+
1084+
return render(request, "meeting/interim_request_cancel.html", {
1085+
"form": form})
1086+
1087+
10591088
@role_required('Area Director', 'Secretariat', 'IRTF Chair', 'WG Chair',
10601089
'RG Chair')
10611090
def interim_request_details(request, number):
@@ -1074,13 +1103,6 @@ def interim_request_details(request, number):
10741103
if request.POST.get('disapprove'):
10751104
meeting.session_set.update(status_id='disappr')
10761105
messages.success(request, 'Interim meeting disapproved')
1077-
if request.POST.get('cancel'):
1078-
if meeting.session_set.first().status.slug == 'sched':
1079-
meeting.session_set.update(status_id='canceled')
1080-
send_interim_cancellation_notice(meeting)
1081-
else:
1082-
meeting.session_set.update(status_id='canceledpa')
1083-
messages.success(request, 'Interim meeting cancelled')
10841106

10851107
return render(request, "meeting/interim_request_details.html", {
10861108
"meeting": meeting,

ietf/name/fixtures/names.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4679,6 +4679,14 @@
46794679
"model": "mailtrigger.recipient",
46804680
"pk": "group_chairs"
46814681
},
4682+
{
4683+
"fields": {
4684+
"template": null,
4685+
"desc": "The group's secretaries"
4686+
},
4687+
"model": "mailtrigger.recipient",
4688+
"pk": "group_secretaries"
4689+
},
46824690
{
46834691
"fields": {
46844692
"template": "{{ changed_personnel | join:\", \" }}",
@@ -5902,6 +5910,20 @@
59025910
"model": "mailtrigger.mailtrigger",
59035911
"pk": "session_scheduled"
59045912
},
5913+
{
5914+
"fields": {
5915+
"cc": [
5916+
"group_responsible_directors"
5917+
],
5918+
"to": [
5919+
"group_chairs",
5920+
"group_secretaries"
5921+
],
5922+
"desc": "Recipients when a group is sent a reminder to submit minutes for a session"
5923+
},
5924+
"model": "mailtrigger.mailtrigger",
5925+
"pk": "session_minutes_reminder"
5926+
},
59055927
{
59065928
"fields": {
59075929
"cc": [

0 commit comments

Comments
 (0)