diff --git a/ietf/meeting/helpers.py b/ietf/meeting/helpers.py index 3ea6bf115..a31f5a40c 100644 --- a/ietf/meeting/helpers.py +++ b/ietf/meeting/helpers.py @@ -9,6 +9,7 @@ from django.http import HttpRequest, Http404 from django.db.models import Max, Q, Prefetch, F from django.conf import settings from django.core.cache import cache +from django.core.urlresolvers import reverse from django.utils.cache import get_cache_key from django.shortcuts import get_object_or_404 from django.template.loader import render_to_string @@ -20,9 +21,11 @@ from ietf.doc.utils import get_document_content from ietf.group.models import Group from ietf.ietfauth.utils import has_role, user_is_person from ietf.liaisons.utils import get_person_for_user +from ietf.mailtrigger.utils import gather_address_lists from ietf.person.models import Person from ietf.meeting.models import Meeting, Schedule, TimeSlot, SchedTimeSessAssignment from ietf.utils.history import find_history_active_at, find_history_replacements_active_at +from ietf.utils.mail import send_mail from ietf.utils.pipe import pipe def find_ads_for_meeting(meeting): @@ -278,7 +281,6 @@ def agenda_permissions(meeting, schedule, user): return cansee, canedit, secretariat def session_constraint_expire(request,session): - from django.core.urlresolvers import reverse from ajax import session_constraints path = reverse(session_constraints, args=[session.meeting.number, session.pk]) temp_request = HttpRequest() @@ -492,6 +494,61 @@ def get_next_agenda_name(meeting): group=group.acronym, sequence=str(last_sequence + 1).zfill(2)) +def send_interim_approval_request(meetings): + """Sends an email to the secretariat, group chairs, and resposnible area + director or the IRTF chair noting that approval has been requested for a + new interim meeting. Takes a list of one or more meetings.""" + group = meetings[0].session_set.first().group + requester = meetings[0].session_set.first().requested_by + (to_email, cc_list) = gather_address_lists('session_requested',group=group,person=requester) + from_email = ('"IETF Meeting Session Request Tool"','session_request_developers@ietf.org') + subject = '{group} - New Interim Meeting Request'.format(group=group.acronym) + template = 'meeting/interim_approval_request.txt' + approval_urls = [] + for meeting in meetings: + url = settings.IDTRACKER_BASE_URL + reverse('ietf.meeting.views.interim_request_details', kwargs={'number': meeting.number}) + approval_urls.append(url) + if len(meetings) > 1: + is_series = True + else: + is_series = False + context = locals() + send_mail(None, + to_email, + from_email, + subject, + template, + context, + cc=cc_list) + +def send_interim_cancellation_notice(meeting): + """Sends an email that a scheduled interim meeting has been cancelled.""" + session = meeting.session_set.first() + group = session.group + to_email = settings.INTERIM_ANNOUNCE_TO_EMAIL + (_, cc_list) = gather_address_lists('session_request_cancelled',group=group) + from_email = settings.INTERIM_ANNOUNCE_FROM_EMAIL + subject = '{group} ({acronym}) {type} Interim Meeting Cancelled (was {date})'.format( + group=group.name, + acronym=group.acronym, + type=group.type.slug.upper(), + date=meeting.date.strftime('%Y-%m-%d')) + start_time = session.official_timeslotassignment().timeslot.time + end_time = start_time + session.requested_duration + if meeting.session_set.filter(status='sched').count() > 1: + is_multi_day = True + else: + is_multi_day = False + template = 'meeting/interim_cancellation_notice.txt' + context = locals() + send_mail(None, + to_email, + from_email, + subject, + template, + context, + cc=cc_list) + 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/migrations/0020_migrate_interim_meetings.py b/ietf/meeting/migrations/0020_migrate_interim_meetings.py new file mode 100644 index 000000000..c736add1c --- /dev/null +++ b/ietf/meeting/migrations/0020_migrate_interim_meetings.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import datetime + +from django.db import migrations + + +def migrate_interim_meetings(apps, schema_editor): + """For all existing interim meetings create an official schedule and timeslot assignments""" + Meeting = apps.get_model("meeting", "Meeting") + Schedule = apps.get_model("meeting", "Schedule") + TimeSlot = apps.get_model("meeting", "TimeSlot") + SchedTimeSessAssignment = apps.get_model("meeting", "SchedTimeSessAssignment") + Person = apps.get_model("person", "Person") + system = Person.objects.get(name="(system)") + + meetings = Meeting.objects.filter(type='interim') + for meeting in meetings: + if not meeting.agenda: + meeting.agenda = Schedule.objects.create( + meeting=meeting, + owner=system, + name='Official') + meeting.save() + session = meeting.session_set.first() # all legacy interim meetings have one session + time = datetime.datetime.combine(meeting.date, datetime.time(0)) + slot = TimeSlot.objects.create( + meeting=meeting, + type_id="session", + duration=session.requested_duration, + time=time) + SchedTimeSessAssignment.objects.create( + timeslot=slot, + session=session, + schedule=meeting.agenda) + + +class Migration(migrations.Migration): + + dependencies = [ + ('meeting', '0019_session_remote_instructions'), + ] + + operations = [ + migrations.RunPython(migrate_interim_meetings), + ] diff --git a/ietf/meeting/test_data.py b/ietf/meeting/test_data.py index ead5ac9e1..e18a37bb6 100644 --- a/ietf/meeting/test_data.py +++ b/ietf/meeting/test_data.py @@ -3,7 +3,7 @@ import datetime from ietf.doc.models import Document, State from ietf.group.models import Group from ietf.meeting.models import Meeting, Room, TimeSlot, Session, Schedule, SchedTimeSessAssignment, ResourceAssociation, SessionPresentation -from ietf.meeting.helpers import create_interim_meeting, assign_interim_session +from ietf.meeting.helpers import create_interim_meeting from ietf.name.models import RoomResourceName from ietf.person.models import Person from ietf.utils.test_data import make_test_data diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index 436dcb815..29d23c83a 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -12,6 +12,8 @@ from pyquery import PyQuery from ietf.doc.models import Document 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.models import Session, TimeSlot, Meeting from ietf.meeting.test_data import make_meeting_test_data from ietf.name.models import SessionStatusName @@ -817,3 +819,19 @@ class InterimTests(TestCase): login_testing_unauthorized(self,"plain",url) r = self.client.get(url) self.assertEqual(r.status_code, 403) + + def test_send_interim_approval_request(self): + make_meeting_test_data() + meeting = Meeting.objects.filter(type='interim',session__status='apprw',session__group__acronym='mars').first() + length_before = len(outbox) + send_interim_approval_request(meetings=[meeting]) + self.assertEqual(len(outbox),length_before+1) + self.assertTrue('New Interim Meeting Request' in outbox[-1]['Subject']) + + def test_send_interim_cancellation_notice(self): + make_meeting_test_data() + meeting = Meeting.objects.filter(type='interim',session__status='sched',session__group__acronym='mars').first() + 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 diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index f936acd99..fb382519d 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -41,6 +41,8 @@ from ietf.meeting.helpers import convert_draft_to_pdf, get_earliest_session_date from ietf.meeting.helpers import can_view_interim_request, can_approve_interim_request from ietf.meeting.helpers import can_request_interim_meeting, get_announcement_initial from ietf.meeting.helpers import sessions_post_save, is_meeting_approved +from ietf.meeting.helpers import send_interim_cancellation_notice +from ietf.meeting.helpers import send_interim_approval_request from ietf.utils.mail import send_mail_message from ietf.utils.pipe import pipe from ietf.utils.pdf import pdf_pages @@ -959,7 +961,7 @@ def interim_send_announcement(request, number): 'RG Chair') def interim_pending(request): '''View which shows interim meeting requests pending approval''' - meetings = Meeting.objects.filter(type='interim', session__status='apprw').distinct() + meetings = Meeting.objects.filter(type='interim', session__status='apprw').distinct().order_by('date') menu_entries = get_menu_entries(request) selected_menu_entry = 'pending' @@ -1006,14 +1008,16 @@ def interim_request(request): formset = SessionFormset(instance=meeting, data=request.POST) formset.is_valid() formset.save() - - # post save sessions_post_save(formset) + if not is_approved: + send_interim_approval_request(meetings=[meeting]) + # series require special handling, each session gets it's own # meeting object we won't see this on edit because series are # subsequently dealt with individually elif meeting_type == 'series': + series = [] SessionFormset.form = staticmethod(curry( InterimSessionModelForm, user=request.user, @@ -1032,10 +1036,12 @@ def interim_request(request): session = session_form.save(commit=False) session.meeting = meeting session.save() - - # post save + series.append(meeting) sessions_post_save([session_form]) + if not is_approved: + send_interim_approval_request(meetings=series) + messages.success(request, 'Interim meeting request submitted') return redirect(upcoming) else: @@ -1071,6 +1077,7 @@ def interim_request_details(request, number): 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') @@ -1129,7 +1136,7 @@ def upcoming(request): '''List of upcoming meetings''' today = datetime.datetime.today() meetings = Meeting.objects.filter(date__gte=today).exclude( - session__status__in=('apprw', 'schedpa')).order_by('date') + session__status__in=('apprw', 'schedpa', 'canceledpa')).order_by('date') # extract groups hierarchy for display filter seen = set() diff --git a/ietf/secr/proceedings/proc_utils.py b/ietf/secr/proceedings/proc_utils.py index 00c5b256e..362faccd9 100644 --- a/ietf/secr/proceedings/proc_utils.py +++ b/ietf/secr/proceedings/proc_utils.py @@ -224,7 +224,7 @@ def create_interim_directory(): # produce date sorted output page = 'proceedings.html' - meetings = InterimMeeting.objects.order_by('-date') + meetings = InterimMeeting.objects.filter(session__status='sched').order_by('-date') response = render(HttpRequest(), 'proceedings/interim_directory.html',{'meetings': meetings}) path = os.path.join(settings.SECR_INTERIM_LISTING_DIR, page) f = open(path,'w') @@ -233,7 +233,7 @@ def create_interim_directory(): # produce group sorted output page = 'proceedings-bygroup.html' - qs = InterimMeeting.objects.all() + qs = InterimMeeting.objects.filter(session__status='sched') meetings = sorted(qs, key=lambda a: a.group().acronym) response = render(HttpRequest(), 'proceedings/interim_directory.html',{'meetings': meetings}) path = os.path.join(settings.SECR_INTERIM_LISTING_DIR, page) diff --git a/ietf/secr/proceedings/tests.py b/ietf/secr/proceedings/tests.py index 7cbdfa0e0..5e52a20df 100644 --- a/ietf/secr/proceedings/tests.py +++ b/ietf/secr/proceedings/tests.py @@ -78,10 +78,11 @@ class BluesheetTestCase(TestCase): shutil.rmtree(self.interim_listing_dir) def test_upload(self): - make_test_data() - meeting = Meeting.objects.filter(type='interim').first() + make_meeting_test_data() + meeting = Meeting.objects.filter(type='interim',session__status='sched').first() + #self.assertTrue(meeting) group = Group.objects.get(acronym='mars') - Session.objects.create(meeting=meeting,group=group,requested_by_id=1,status_id='sched',type_id='session') + #Session.objects.create(meeting=meeting,group=group,requested_by_id=1,status_id='sched',type_id='session') url = reverse('proceedings_upload_unified', kwargs={'meeting_num':meeting.number,'acronym':'mars'}) upfile = StringIO('dummy file') upfile.name = "scan1.pdf" diff --git a/ietf/templates/meeting/interim_approval_request.txt b/ietf/templates/meeting/interim_approval_request.txt new file mode 100644 index 000000000..1d895efe8 --- /dev/null +++ b/ietf/templates/meeting/interim_approval_request.txt @@ -0,0 +1,9 @@ +{% load ams_filters %} +A new interim meeting {% if is_series %}series {% endif %}request has just been submitted by {{ requester }}. + +This request requires approval by the Area Director. +The meeting{{ meetings|pluralize }} can be approved here: +{% for url in approval_urls %}{{ url }} +{% endfor %} + +{% for meeting in meetings %}{% if is_series %}Meeting: {{ forloop.counter }}{% endif %}{% include "meeting/interim_info.txt" %}{% endfor %} diff --git a/ietf/templates/meeting/interim_cancellation_notice.txt b/ietf/templates/meeting/interim_cancellation_notice.txt new file mode 100644 index 000000000..b1faade47 --- /dev/null +++ b/ietf/templates/meeting/interim_cancellation_notice.txt @@ -0,0 +1,8 @@ +{% load ams_filters %} +The {{ group.name }} ({{ group.acronym }}) {% if not meeting.city %}virtual {% endif %}{% if is_multi_day %}multi-day {% endif %} +interim meeting for {{ meeting.date|date:"Y-m-d" }} from {{ start_time|time:"H:i" }} to {{ end_time|time:"H:i" }} {{ meeting.time_zone }} +has been cancelled. + +{{ additional_information }} + + diff --git a/ietf/templates/meeting/interim_info.txt b/ietf/templates/meeting/interim_info.txt new file mode 100644 index 000000000..4ed8ffb58 --- /dev/null +++ b/ietf/templates/meeting/interim_info.txt @@ -0,0 +1,20 @@ +{% load ietf_filters %} +--------------------------------------------------------- +Working Group Name: {{ group.name|safe }} +Area Name: {{ group.parent }} +Session Requester: {{ requester }} + +{% if meeting.city %}City: {{ meeting.city }} +Country: {{ meeting.country }} +Timezone: {{ meeting.time_zone }} +{% else %}Meeting Type: Virtual Meeting{% endif %} + +{% for session in meeting.session_set.all %}Session {{ forloop.counter }}: + +Date: {{ session.official_timeslotassignment.timeslot.time|date:"Y-m-d" }} +Start Time: {{ session.official_timeslotassignment.timeslot.time|date:"H:i" }} +Duration: {{ session.requested_duration|format_timedelta }} +Remote Instructions: {{ session.remote_instructions }} +Agenda Note: {{ session.agenda_note }} +{% endfor %} +---------------------------------------------------------