Refactoring. Implement iCalendar support

- Legacy-Id: 11178
This commit is contained in:
Ryan Cross 2016-05-11 00:13:55 +00:00
parent 3f3e6f0b14
commit 3366006653
10 changed files with 102 additions and 106 deletions

View file

@ -14,7 +14,7 @@ from ietf.group.models import Group
from ietf.ietfauth.utils import has_role
from ietf.meeting.models import Session, Meeting, Schedule, countries, timezones
from ietf.meeting.helpers import get_next_interim_number, assign_interim_session
from ietf.meeting.helpers import is_meeting_approved
from ietf.meeting.helpers import is_meeting_approved, get_next_agenda_name
from ietf.message.models import Message
from ietf.person.models import Person
from ietf.secr.utils.meeting import get_upload_root
@ -159,6 +159,7 @@ class InterimMeetingModelForm(forms.ModelForm):
self.is_edit = bool(self.instance.pk)
self.fields['group'].widget.attrs['class'] = "select2-field"
self.fields['time_zone'].initial = 'UTC'
self.fields['approved'].initial = True
self.set_group_options()
if self.is_edit:
self.fields['group'].initial = self.instance.session_set.first().group
@ -215,7 +216,7 @@ class InterimSessionModelForm(forms.ModelForm):
requested_duration = DurationField(required=False)
end_time = forms.TimeField(required=False)
end_time_utc = forms.TimeField(required=False)
remote_instructions = forms.CharField(max_length=1024, required=False)
remote_instructions = forms.CharField(max_length=1024, required=True)
agenda = forms.CharField(required=False, widget=forms.Textarea)
agenda_note = forms.CharField(max_length=255, required=False)
@ -263,10 +264,7 @@ class InterimSessionModelForm(forms.ModelForm):
doc.rev = str(int(doc.rev) + 1).zfill(2)
doc.save()
else:
filename = 'agenda-interim-{group}-{date}-{time}'.format(
group=self.group.acronym,
date=self.cleaned_data['date'].strftime("%Y-%m-%d-"),
time=self.cleaned_data['time'].strftime("%H%M"))
filename = get_next_agenda_name(meeting=self.instance.meeting)
doc = Document.objects.create(
type_id='agenda',
group=self.group,

View file

@ -461,8 +461,8 @@ def is_meeting_approved(meeting):
def get_next_interim_number(group, date):
"""Returns a unique number to use for the next interim meeting for
*group*"""
"""Returns a unique string to use for the next interim meeting for
*group*, used for Meeting.number field."""
meetings = Meeting.objects.filter(
number__startswith='interim-{year}-{group}'.format(
year=date.year,
@ -475,9 +475,23 @@ def get_next_interim_number(group, date):
return 'interim-{year}-{group}-{sequence}'.format(
year=date.year,
group=group.acronym,
sequence=last_sequence + 1)
sequence=str(last_sequence + 1).zfill(2))
def get_next_agenda_name(meeting):
"""Returns the next name to use for an agenda document for *meeting*"""
group = meeting.session_set.first().group
documents = Document.objects.filter(type='agenda', session__meeting=meeting)
if documents:
sequences = [int(d.name.split('-')[-1]) for d in documents]
last_sequence = sorted(sequences)[-1]
else:
last_sequence = 0
return 'agenda-{meeting}-{group}-{sequence}'.format(
meeting=meeting.number,
group=group.acronym,
sequence=str(last_sequence + 1).zfill(2))
def sessions_post_save(forms):
"""Helper function to perform various post save operations on each form of a
InterimSessionModelForm formset"""

View file

@ -6,14 +6,12 @@ import urlparse
from django.core.urlresolvers import reverse as urlreverse
from django.conf import settings
from django.contrib.auth.models import User
from django.db import transaction
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 get_announcement_initial
from ietf.meeting.models import Session, TimeSlot, Meeting
from ietf.meeting.test_data import make_meeting_test_data
from ietf.name.models import SessionStatusName
@ -416,20 +414,31 @@ class InterimTests(TestCase):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
today = datetime.date.today()
mars_interim = Meeting.objects.filter(date__gt=today,type='interim',session__group__acronym='mars',session__status='sched').first()
ames_interim = Meeting.objects.filter(date__gt=today,type='interim',session__group__acronym='ames',session__status='canceled').first()
mars_interim = Meeting.objects.filter(date__gt=today, type='interim', session__group__acronym='mars', session__status='sched').first()
ames_interim = Meeting.objects.filter(date__gt=today, type='interim', session__group__acronym='ames', session__status='canceled').first()
self.assertTrue(mars_interim.number in r.content)
self.assertTrue(ames_interim.number in r.content)
self.assertTrue('IETF - 42' in r.content)
# cancelled session
q = PyQuery(r.content)
self.assertTrue('CANCELLED' in q('[id*="-ames"]').text())
self.check_interim_tabs(url)
def test_upcoming_ics(self):
def test_upcoming_ical(self):
make_meeting_test_data()
r = self.client.get("/meeting/upcoming.ics/")
url = urlreverse("ietf.meeting.views.upcoming_ical")
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.get('Content-Type'),"text/calendar")
self.assertEqual(r.get('Content-Type'), "text/calendar")
self.assertEqual(r.content.count('UID'), 5)
# check filtered output
url = url + '?filters=mars'
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.get('Content-Type'), "text/calendar")
# print r.content
self.assertEqual(r.content.count('UID'), 2)
def test_interim_request_permissions(self):
'''Ensure only authorized users see link to request interim meeting'''
@ -469,45 +478,6 @@ class InterimTests(TestCase):
self.assertEqual(Group.objects.filter(type__in=('wg','rg'),state='active').count(),
len(q("#id_group option")) -1 ) # -1 for options placeholder
def test_temp(self):
from django.forms.models import modelform_factory, inlineformset_factory
from ietf.meeting.forms import InterimSessionModelForm
from django.utils.functional import curry
make_meeting_test_data()
group = Group.objects.get(acronym='mars')
date = datetime.date.today() + datetime.timedelta(days=30)
time = datetime.datetime.now().time().replace(microsecond=0,second=0)
dt = datetime.datetime.combine(date, time)
duration = datetime.timedelta(hours=3)
remote_instructions = 'Use webex'
agenda = 'Intro. Slides. Discuss.'
agenda_note = 'On second level'
self.client.login(username="secretary", password="secretary+password")
data = {'group':group.pk,
'meeting_type':'single',
'city':'',
'country':'',
'time_zone':'UTC',
'session_set-0-date':date.strftime("%Y-%m-%d"),
'session_set-0-time':time.strftime('%H:%M'),
'session_set-0-requested_duration':'03:00:00',
'session_set-0-remote_instructions':remote_instructions,
'session_set-0-agenda':agenda,
'session_set-0-agenda_note':agenda_note,
'session_set-TOTAL_FORMS':1,
'session_set-INITIAL_FORMS':0,
'session_set-MIN_NUM_FORMS':0,
'session_set-MAX_NUM_FORMS':1000}
user = User.objects.get(username='secretary')
is_approved = False
meeting = Meeting.objects.order_by('id').last()
SessionFormset = inlineformset_factory(Meeting, Session, form=InterimSessionModelForm, can_delete=False, extra=2)
SessionFormset.form = staticmethod(curry(InterimSessionModelForm, user=user,group=group,is_approved=is_approved))
formset = SessionFormset(instance=meeting, data=data)
#assert False, (formset.management_form)
formset.save()
def test_interim_request_single(self):
make_meeting_test_data()
@ -542,7 +512,7 @@ class InterimTests(TestCase):
meeting = Meeting.objects.order_by('id').last()
self.assertEqual(meeting.type_id,'interim')
self.assertEqual(meeting.date,date)
self.assertEqual(meeting.number,'interim-%s-%s-%s' % (date.year,group.acronym,1))
self.assertEqual(meeting.number,'interim-%s-%s-%s' % (date.year,group.acronym,'01'))
self.assertEqual(meeting.city,'')
self.assertEqual(meeting.country,'')
self.assertEqual(meeting.time_zone,'UTC')
@ -592,7 +562,7 @@ class InterimTests(TestCase):
meeting = Meeting.objects.order_by('id').last()
self.assertEqual(meeting.type_id,'interim')
self.assertEqual(meeting.date,date)
self.assertEqual(meeting.number,'interim-%s-%s-%s' % (date.year,group.acronym,1))
self.assertEqual(meeting.number,'interim-%s-%s-%s' % (date.year,group.acronym,'01'))
self.assertEqual(meeting.city,city)
self.assertEqual(meeting.country,country)
self.assertEqual(meeting.time_zone,time_zone)
@ -645,7 +615,7 @@ class InterimTests(TestCase):
meeting = Meeting.objects.order_by('id').last()
self.assertEqual(meeting.type_id,'interim')
self.assertEqual(meeting.date,date)
self.assertEqual(meeting.number,'interim-%s-%s-%s' % (date.year,group.acronym,1))
self.assertEqual(meeting.number,'interim-%s-%s-%s' % (date.year,group.acronym,'01'))
self.assertEqual(meeting.city,city)
self.assertEqual(meeting.country,country)
self.assertEqual(meeting.time_zone,time_zone)
@ -712,7 +682,7 @@ class InterimTests(TestCase):
meeting = meetings[1]
self.assertEqual(meeting.type_id,'interim')
self.assertEqual(meeting.date,date)
self.assertEqual(meeting.number,'interim-%s-%s-%s' % (date.year,group.acronym,1))
self.assertEqual(meeting.number,'interim-%s-%s-%s' % (date.year,group.acronym,'01'))
self.assertEqual(meeting.city,city)
self.assertEqual(meeting.country,country)
self.assertEqual(meeting.time_zone,time_zone)
@ -727,7 +697,7 @@ class InterimTests(TestCase):
meeting = meetings[0]
self.assertEqual(meeting.type_id,'interim')
self.assertEqual(meeting.date,date2)
self.assertEqual(meeting.number,'interim-%s-%s-%s' % (date.year,group.acronym,2))
self.assertEqual(meeting.number,'interim-%s-%s-%s' % (date.year,group.acronym,'02'))
self.assertEqual(meeting.city,city)
self.assertEqual(meeting.country,country)
self.assertEqual(meeting.time_zone,time_zone)

View file

@ -67,7 +67,7 @@ urlpatterns = [
url(r'^(?:(?P<num>\d+)/)?', include(type_ietf_only_patterns_id_optional)),
url(r'^(?P<num>\d+)/', include(type_ietf_only_patterns)),
url(r'^upcoming/$', views.upcoming),
url(r'^upcoming.ics/$', views.ical_upcoming),
url(r'^upcoming.ics/$', views.upcoming_ical),
url(r'^interim/announce/$', views.interim_announce),
url(r'^interim/announce/(?P<number>[A-Za-z0-9._+-]+)/$', views.interim_send_announcement),
url(r'^interim/request/$', views.interim_request),

View file

@ -21,7 +21,7 @@ from django.core.urlresolvers import reverse
from django.db.models import Min, Max
from django.conf import settings
from django.forms.models import modelform_factory, inlineformset_factory
from django.forms import ModelForm, formset_factory
from django.forms import ModelForm
from django.utils.functional import curry
from django.views.decorators.csrf import ensure_csrf_cookie
@ -40,13 +40,12 @@ from ietf.meeting.helpers import preprocess_assignments_for_agenda, read_agenda_
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 get_interim_initial, get_interim_session_initial
from ietf.meeting.helpers import sessions_post_save, is_meeting_approved
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, InterimSessionForm, InterimAnnounceForm, InterimSessionModelForm
from .forms import InterimMeetingModelForm, InterimAnnounceForm, InterimSessionModelForm
def get_menu_entries(request):
@ -960,7 +959,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')
meetings = Meeting.objects.filter(type='interim', session__status='apprw').distinct()
menu_entries = get_menu_entries(request)
selected_menu_entry = 'pending'
@ -1126,22 +1125,11 @@ def interim_request_edit(request, number):
"formset": formset})
def ical_upcoming(request):
'''ICAL upcoming meetings'''
today = datetime.datetime.today()
meetings = Meeting.objects.filter(date__gt=today)
return render(request, "meeting/upcoming.ics", {
"meetings": meetings,
}, content_type="text/calendar")
def upcoming(request):
'''List of upcoming meetings'''
today = datetime.datetime.today()
meetings = Meeting.objects.filter(
date__gte=today,
session__status__in=('sched', 'canceled')).order_by('date')
meetings = Meeting.objects.filter(date__gte=today).exclude(
session__status__in=('apprw', 'schedpa')).order_by('date')
# extract groups hierarchy for display filter
seen = set()
@ -1170,12 +1158,40 @@ def upcoming(request):
# add menu actions
actions = []
if can_request_interim_meeting(request.user):
actions.append(("Request new interim meeting",
reverse("ietf.meeting.views.interim_request")))
actions.append(('Request new interim meeting',
reverse('ietf.meeting.views.interim_request')))
actions.append(('Download as .ics',
reverse('ietf.meeting.views.upcoming_ical')))
return render(request, "meeting/upcoming.html", {
return render(request, 'meeting/upcoming.html', {
'meetings': meetings,
'menu_actions': actions,
'menu_entries': menu_entries,
'selected_menu_entry': selected_menu_entry,
'group_parents': group_parents})
def upcoming_ical(request):
'''Return Upcoming meetings in iCalendar file'''
filters = request.GET.getlist('filters')
#assert False, filters
today = datetime.datetime.today()
meetings = Meeting.objects.filter(date__gte=today).exclude(
session__status__in=('apprw', 'schedpa')).order_by('date')
assignments = []
for meeting in meetings:
items = meeting.agenda.assignments.order_by(
'session__type__slug', 'timeslot__time')
assignments.extend(items)
# apply filters
if filters:
assignments = [a for a in assignments if
a.session.group.acronym in filters or
a.session.group.parent.acronym in filters]
return render(request, 'meeting/upcoming.ics', {
'assignments': assignments,
}, content_type='text/calendar')

View file

@ -101,7 +101,6 @@ var interimRequest = {
return false;
},
calculateEndTime : function() {
// gets called when either start_time or duration change
var fieldset = $(this).parents(".fieldset");

View file

@ -8,18 +8,14 @@ function toggle_visibility() {
$(".pickviewneg").addClass("active");
if (h) {
// if there are items in the hash, hide all rows that are
// hidden by default, show all rows that are shown by default
// if there are items in the hash, hide all rows
$('[id^="row-"]').hide();
//$.each($(".pickviewneg").text().trim().split(/ +/), function (i, v) {
// v = v.trim().toLowerCase();
// $('[id^="row-"]').filter('[id*="-' + v + '"]').show();
//});
// show the customizer
$("#customize").collapse("show");
// loop through the has items and change the UI element and row visibilities accordingly
var query_array = [];
$.each(h.split(","), function (i, v) {
if (v.indexOf("-") == 0) {
// this is a "negative" item: when present, hide these rows
@ -32,20 +28,21 @@ function toggle_visibility() {
$('[id^="row-"]').filter('[id*="-' + v + '"]').show();
$(".view." + v).find("button").addClass("active disabled");
$("button.pickview." + v).addClass("active");
query_array.push("filters=" + v)
}
});
// show the week view
//$("#weekview").attr("src", "week-view.html" + window.location.hash).removeClass("hidden");
// show the custom .ics link
//$("#ical-link").attr("href",$("#ical-link").attr("href").split("?")[0]+"?"+h);
//$("#ical-link").removeClass("hidden");
// adjust the custom .ics link
var link = $('a[href*="upcoming.ics"]');
var new_href = link.attr("href").split("?")[0]+"?"+query_array.join("&");
link.attr("href",new_href);
} else {
// if the hash is empty, show all and hide weekview
// if the hash is empty, show all
$('[id^="row-"]').show();
//$("#ical-link, #weekview").addClass("hidden");
// adjust the custom .ics link
var link = $('a[href*="upcoming.ics"]');
link.attr("href",link.attr("href").split("?")[0]);
}
}

View file

@ -1,7 +1,7 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load staticfiles bootstrap3 widget_tweaks %}
{% load staticfiles bootstrap3 widget_tweaks ietf_filters %}
{% block title %}Interim Request{% endblock %}
@ -26,9 +26,11 @@
<label class="checkbox-inline">{% render_field form.in_person %}<strong>In Person</strong></label>
</div>
{% if user|has_role:"Secretariat,Area Director,IRTF Chair" %}
<div class="col-md-2">
<label class="checkbox-inline">{% render_field form.approved %}<strong>Preapproved by AD</strong></label>
</div>
{% endif %}
<div class="col-md-2 radio-inline"><strong>Meeting Type:</strong></div>
@ -98,13 +100,13 @@
</div>
<div class="form-group">
<label for="id_session_set-{{ forloop.counter0 }}-remote_instructions" class="col-md-2 control-label">Remote Instructions</label>
<div class="col-md-10">{% render_field form.remote_instructions class="form-control" placeholder="ie. Webex address" %}</div>
<label for="id_session_set-{{ forloop.counter0 }}-remote_instructions" class="col-md-2 control-label required">Remote Instructions</label>
<div class="col-md-10">{% render_field form.remote_instructions class="form-control" placeholder="Webex URL" %}<p class="help-block">"Remote participation is not supported" or "Remote participation information will be obtained at the time of approval" are acceptable values. See <a href="">here</a> for more on remote participation support.</p></div>
</div>
<div class="form-group">
<label for="id_session_set-{{ forloop.counter0 }}-agenda" class="col-md-2 control-label">Agenda</label>
<div class="col-md-10">{% render_field form.agenda class="form-control" rows="6" placeholder="paste agenda here" %}</div>
<div class="col-md-10">{% render_field form.agenda class="form-control" rows="6" placeholder="Paste agenda here" %}</div>
</div>
<div class="form-group">
@ -127,7 +129,7 @@
<a class="btn btn-default pull-right" href="{% url 'ietf.meeting.views.upcoming' %}">Back</a>
{% endbuttons %}
</div>
</form>
{% endblock %}

View file

@ -113,7 +113,7 @@
<tbody>
{% for meeting in meetings %}
{% if meeting.type.slug == 'interim' %}
<tr id="row-{{ forloop.counter }}-{{ meeting.session_set.all.0.group.acronym }}">
<tr id="row-{{ forloop.counter }}-{{ meeting.session_set.first.group.parent.acronym }}-{{ meeting.session_set.first.group.acronym }}">
{% else %}
<tr id="row-{{ forloop.counter }}-ietf">
{% endif %}

View file

@ -2,9 +2,9 @@
VERSION:2.0
METHOD:PUBLISH
PRODID:-//IETF//datatracker.ietf.org ical upcoming//EN
{% for meeting in meetings %}BEGIN:VEVENT
UID:ietf-{{meeting.number}}-{{meeting.session_set.all.0.pk}}
SUMMARY:{% if item.session.name %}{{item.session.name|ics_esc}}{% else %}{% if not item.session.historic_group %}{{item.timeslot.name|ics_esc}}{% else %}{{item.session.historic_group.acronym|lower}} - {{item.session.historic_group.name}}{% endif%}{%endif%}
{% for item in assignments %}BEGIN:VEVENT
UID:ietf-{{item.session.meeting.number}}-{{item.timeslot.pk}}
SUMMARY:{% if item.session.name %}{{item.session.name|ics_esc}}{% else %}{{item.session.group.acronym|lower}} - {{item.session.group.name}}{%endif%}
{% if item.timeslot.show_location %}LOCATION:{{item.timeslot.get_location}}
{% endif %}STATUS:{{item.session.ical_status}}
CLASS:PUBLIC