Merged in ^/branch/amsl/interim@11230, which provides interim meeting management support. Also fixes issues #1961, #1962 and #1964.

- Legacy-Id: 11402
This commit is contained in:
Henrik Levkowetz 2016-06-17 14:16:27 +00:00
commit d298785fda
44 changed files with 3242 additions and 68 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

@ -120,7 +120,7 @@ class DocumentInfo(models.Model):
meeting = None
if meeting_related:
meeting = self.name.split("-")[1]
meeting = self.session_set.first().meeting
return format.format(doc=self,meeting=meeting)
@ -388,15 +388,7 @@ class Document(DocumentInfo):
filename = os.path.splitext(self.external_url)[0]
else:
filename = self.external_url
if meeting.type_id == 'ietf':
url = '%sproceedings/%s/%s/%s' % (settings.IETF_HOST_URL,meeting.number,self.type_id,filename)
elif meeting.type_id == 'interim':
url = "%sproceedings/interim/%s/%s/%s/%s" % (
settings.IETF_HOST_URL,
meeting.date.strftime('%Y/%m/%d'),
session.group.acronym,
self.type_id,
filename)
url = '%sproceedings/%s/%s/%s' % (settings.IETF_HOST_URL,meeting.number,self.type_id,filename)
return url
return urlreverse('doc_view', kwargs={ 'name': name }, urlconf="ietf.urls")

View file

@ -3,12 +3,14 @@
import textwrap
import re
import datetime
import os
import types
from email.utils import parseaddr
import debug # pyflakes:ignore
from ietf.doc.models import ConsensusDocEvent
from ietf.doc.utils import get_document_content
from django import template
from django.conf import settings
from django.utils.html import escape, fix_ampersands
@ -590,3 +592,27 @@ def urlize_html(html, autoescape=False):
def emailwrap(email):
email = str(email)
return mark_safe(email.replace('@', '<wbr>@'))
@register.filter
def document_content(doc):
if doc is None:
return None
path = os.path.join(doc.get_file_path(),doc.filename_with_rev())
return get_document_content(doc.name,path,markup=False)
@register.filter
def session_start_time(session):
timeslot = session.official_timeslotassignment().timeslot
return timeslot.time
@register.filter
def session_end_time(session):
timeslot = session.official_timeslotassignment().timeslot
return timeslot.time + timeslot.duration
@register.filter
def format_timedelta(timedelta):
s = timedelta.seconds
hours, remainder = divmod(s, 3600)
minutes, seconds = divmod(remainder, 60)
return '{hours:02d}:{minutes:02d}'.format(hours=hours,minutes=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:

330
ietf/meeting/forms.py Normal file
View file

@ -0,0 +1,330 @@
import datetime
import os
import re
from django import forms
from django.core.validators import ValidationError
from django.forms.fields import Field
from django.utils.encoding import force_text
from django.utils import six
from ietf.doc.models import Document, DocAlias, State, NewRevisionDocEvent
from ietf.doc.utils import get_document_content
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
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.utils.fields import DatepickerDateField
# need to insert empty option for use in ChoiceField
# countries.insert(0, ('', '-'*9 ))
countries.insert(0, ('', ''))
timezones.insert(0, ('', '-' * 9))
# -------------------------------------------------
# DurationField from Django 1.8
# -------------------------------------------------
def duration_string(duration):
days = duration.days
seconds = duration.seconds
microseconds = duration.microseconds
minutes = seconds // 60
seconds = seconds % 60
hours = minutes // 60
minutes = minutes % 60
# string = '{:02d}:{:02d}:{:02d}'.format(hours, minutes, seconds)
string = '{:02d}:{:02d}'.format(hours, minutes)
if days:
string = '{} '.format(days) + string
if microseconds:
string += '.{:06d}'.format(microseconds)
return string
custom_duration_re = re.compile(
r'^(?P<hours>\d+):(?P<minutes>\d+)$'
)
standard_duration_re = re.compile(
r'^'
r'(?:(?P<days>-?\d+) (days?, )?)?'
r'((?:(?P<hours>\d+):)(?=\d+:\d+))?'
r'(?:(?P<minutes>\d+):)?'
r'(?P<seconds>\d+)'
r'(?:\.(?P<microseconds>\d{1,6})\d{0,6})?'
r'$'
)
# Support the sections of ISO 8601 date representation that are accepted by
# timedelta
iso8601_duration_re = re.compile(
r'^P'
r'(?:(?P<days>\d+(.\d+)?)D)?'
r'(?:T'
r'(?:(?P<hours>\d+(.\d+)?)H)?'
r'(?:(?P<minutes>\d+(.\d+)?)M)?'
r'(?:(?P<seconds>\d+(.\d+)?)S)?'
r')?'
r'$'
)
def parse_duration(value):
"""Parses a duration string and returns a datetime.timedelta.
The preferred format for durations in Django is '%d %H:%M:%S.%f'.
Also supports ISO 8601 representation.
"""
match = custom_duration_re.match(value)
if not match:
match = standard_duration_re.match(value)
if not match:
match = iso8601_duration_re.match(value)
if match:
kw = match.groupdict()
if kw.get('microseconds'):
kw['microseconds'] = kw['microseconds'].ljust(6, '0')
kw = {k: float(v) for k, v in six.iteritems(kw) if v is not None}
return datetime.timedelta(**kw)
class DurationField(Field):
default_error_messages = {
'invalid': 'Enter a valid duration.',
}
def prepare_value(self, value):
if isinstance(value, datetime.timedelta):
return duration_string(value)
return value
def to_python(self, value):
if value in self.empty_values:
return None
if isinstance(value, datetime.timedelta):
return value
value = parse_duration(force_text(value))
if value is None:
raise ValidationError(self.error_messages['invalid'], code='invalid')
return value
# -------------------------------------------------
# Helpers
# -------------------------------------------------
class GroupModelChoiceField(forms.ModelChoiceField):
'''
Custom ModelChoiceField, changes the label to a more readable format
'''
def label_from_instance(self, obj):
return obj.acronym
# -------------------------------------------------
# Forms
# -------------------------------------------------
class InterimMeetingModelForm(forms.ModelForm):
group = GroupModelChoiceField(queryset=Group.objects.filter(type__in=('wg', 'rg'), state__in=('active', 'proposed', 'bof')).order_by('acronym'), required=False)
in_person = forms.BooleanField(required=False)
meeting_type = forms.ChoiceField(choices=(
("single", "Single"),
("multi-day", "Multi-Day"),
('series', 'Series')), required=False, initial='single', widget=forms.RadioSelect)
approved = forms.BooleanField(required=False)
city = forms.CharField(max_length=255, required=False)
country = forms.ChoiceField(choices=countries, required=False)
time_zone = forms.ChoiceField(choices=timezones)
class Meta:
model = Meeting
fields = ('group', 'in_person', 'meeting_type', 'approved', 'city', 'country', 'time_zone')
def __init__(self, request, *args, **kwargs):
super(InterimMeetingModelForm, self).__init__(*args, **kwargs)
self.user = request.user
self.person = self.user.person
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
self.fields['group'].widget.attrs['disabled'] = True
if self.instance.city or self.instance.country:
self.fields['in_person'].initial = True
if is_meeting_approved(self.instance):
self.fields['approved'].initial = True
else:
self.fields['approved'].initial = False
self.fields['approved'].widget.attrs['disabled'] = True
def clean(self):
super(InterimMeetingModelForm, self).clean()
cleaned_data = self.cleaned_data
if not cleaned_data.get('group'):
raise forms.ValidationError("You must select a group")
return self.cleaned_data
def set_group_options(self):
'''Set group options based on user accessing the form'''
if has_role(self.user, "Secretariat"):
return # don't reduce group options
if has_role(self.user, "Area Director"):
queryset = Group.objects.filter(type="wg", state__in=("active", "proposed", "bof")).order_by('acronym')
elif has_role(self.user, "IRTF Chair"):
queryset = Group.objects.filter(type="rg", state__in=("active", "proposed")).order_by('acronym')
elif has_role(self.user, "WG Chair"):
queryset = Group.objects.filter(type="wg", state__in=("active", "proposed", "bof"), role__person=self.person, role__name="chair").distinct().order_by('acronym')
elif has_role(self.user, "RG Chair"):
queryset = Group.objects.filter(type="rg", state__in=("active", "proposed"), role__person=self.person, role__name="chair").distinct().order_by('acronym')
self.fields['group'].queryset = queryset
# if there's only one possibility make it the default
if len(queryset) == 1:
self.fields['group'].initial = queryset[0]
def save(self, *args, **kwargs):
'''Save must handle fields not included in the form: date,number,type_id'''
date = kwargs.pop('date')
group = self.cleaned_data.get('group')
meeting = super(InterimMeetingModelForm, self).save(commit=False)
if not meeting.type_id:
meeting.type_id = 'interim'
if not meeting.number:
meeting.number = get_next_interim_number(group, date)
meeting.date = date
if kwargs.get('commit', True):
# create schedule with meeting
meeting.save() # pre-save so we have meeting.pk for schedule
if not meeting.agenda:
meeting.agenda = Schedule.objects.create(
meeting=meeting,
owner=Person.objects.get(name='(System)'))
meeting.save() # save with agenda
return meeting
class InterimSessionModelForm(forms.ModelForm):
date = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1"}, label='Date', required=False)
time = forms.TimeField(widget=forms.TimeInput(format='%H:%M'), required=True)
requested_duration = DurationField(required=True)
end_time = forms.TimeField(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)
class Meta:
model = Session
fields = ('date', 'time', 'requested_duration', 'end_time',
'remote_instructions', 'agenda', 'agenda_note')
def __init__(self, *args, **kwargs):
if 'user' in kwargs:
self.user = kwargs.pop('user')
if 'group' in kwargs:
self.group = kwargs.pop('group')
if 'is_approved' in kwargs:
self.is_approved = kwargs.pop('is_approved')
super(InterimSessionModelForm, self).__init__(*args, **kwargs)
self.is_edit = bool(self.instance.pk)
# setup fields that aren't intrinsic to the Session object
if self.is_edit:
self.initial['date'] = self.instance.official_timeslotassignment().timeslot.time
self.initial['time'] = self.instance.official_timeslotassignment().timeslot.time
if self.instance.agenda():
doc = self.instance.agenda()
path = os.path.join(doc.get_file_path(), doc.filename_with_rev())
self.initial['agenda'] = get_document_content(os.path.basename(path), path, markup=False)
def clean_date(self):
'''Date field validator. We can't use required on the input because
it is a datepicker widget'''
date = self.cleaned_data.get('date')
if not date:
raise forms.ValidationError('Required field')
return date
def save(self, *args, **kwargs):
"""NOTE: as the baseform of an inlineformset self.save(commit=True)
never gets called"""
session = super(InterimSessionModelForm, self).save(commit=kwargs.get('commit', True))
if self.is_approved:
session.status_id = 'scheda'
else:
session.status_id = 'apprw'
session.group = self.group
session.type_id = 'session'
if not self.instance.pk:
session.requested_by = self.user.person
return session
def save_agenda(self):
if self.instance.agenda():
doc = self.instance.agenda()
doc.rev = str(int(doc.rev) + 1).zfill(2)
doc.save()
else:
filename = get_next_agenda_name(meeting=self.instance.meeting)
doc = Document.objects.create(
type_id='agenda',
group=self.group,
name=filename,
rev='00',
external_url='{}-00.txt'.format(filename))
doc.set_state(State.objects.get(type=doc.type, slug='active'))
DocAlias.objects.create(name=doc.name, document=doc)
self.instance.sessionpresentation_set.create(document=doc, rev=doc.rev)
NewRevisionDocEvent.objects.create(
type='new_revision',
by=self.user.person,
doc=doc,
rev=doc.rev,
desc='New revision available')
# write file
path = os.path.join(self.instance.meeting.get_materials_path(), 'agenda', doc.filename_with_rev())
directory = os.path.dirname(path)
if not os.path.exists(directory):
os.makedirs(directory)
with open(path, "w") as file:
file.write(self.cleaned_data['agenda'])
class InterimAnnounceForm(forms.ModelForm):
class Meta:
model = Message
fields = ('to', 'frm', 'cc', 'bcc', 'reply_to', 'subject', 'body')
def save(self, *args, **kwargs):
user = kwargs.pop('user')
message = super(InterimAnnounceForm, self).save(commit=False)
message.by = user.person
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

@ -9,17 +9,23 @@ 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
import debug # pyflakes:ignore
from ietf.doc.models import Document
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
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):
@ -275,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()
@ -285,4 +290,334 @@ def session_constraint_expire(request,session):
if key is not None and cache.has_key(key):
cache.delete(key)
# -------------------------------------------------
# Interim Meeting Helpers
# -------------------------------------------------
def assign_interim_session(form):
"""Helper function to create a timeslot and assign the interim session"""
time = datetime.datetime.combine(
form.cleaned_data['date'],
form.cleaned_data['time'])
session = form.instance
if session.official_timeslotassignment():
slot = session.official_timeslotassignment().timeslot
slot.time = time
slot.save()
else:
slot = TimeSlot.objects.create(
meeting=session.meeting,
type_id="session",
duration=session.requested_duration,
time=time)
SchedTimeSessAssignment.objects.create(
timeslot=slot,
session=session,
schedule=session.meeting.agenda)
def can_approve_interim_request(meeting, user):
'''Returns True if the user has permissions to approve an interim meeting request'''
if meeting.type.slug != 'interim':
return False
if has_role(user, 'Secretariat'):
return True
person = get_person_for_user(user)
session = meeting.session_set.first()
if not session:
return False
group = session.group
if group.type.slug == 'wg' and group.parent.role_set.filter(name='ad', person=person):
return True
if group.type.slug == 'rg' and group.parent.role_set.filter(name='chair', person=person):
return True
return False
def can_edit_interim_request(meeting, user):
'''Returns True if the user can edit the interim meeting request'''
if meeting.type.slug != 'interim':
return False
if has_role(user, 'Secretariat'):
return True
person = get_person_for_user(user)
session = meeting.session_set.first()
if not session:
return False
group = session.group
if group.role_set.filter(name='chair', person=person):
return True
elif can_approve_interim_request(meeting, user):
return True
else:
return False
def can_request_interim_meeting(user):
if has_role(user, ('Secretariat', 'Area Director', 'WG Chair', 'IRTF Chair', 'RG Chair')):
return True
return False
def can_view_interim_request(meeting, user):
'''Returns True if the user can see the pending interim request in the pending interim view'''
if meeting.type.slug != 'interim':
return False
if has_role(user, 'Secretariat'):
return True
person = get_person_for_user(user)
session = meeting.session_set.first()
if not session:
return False
group = session.group
if has_role(user, 'Area Director') and group.type.slug == 'wg':
return True
if has_role(user, 'IRTF Chair') and group.type.slug == 'rg':
return True
if group.role_set.filter(name='chair', person=person):
return True
return False
def create_interim_meeting(group, date, city='', country='', timezone='UTC',
person=None):
"""Helper function to create interim meeting and associated schedule"""
if not person:
person = Person.objects.get(name='(System)')
number = get_next_interim_number(group, date)
meeting = Meeting.objects.create(
number=number,
type_id='interim',
date=date,
city=city,
country=country,
time_zone=timezone)
schedule = Schedule.objects.create(
meeting=meeting,
owner=person,
visible=True,
public=True)
meeting.agenda = schedule
meeting.save()
return meeting
def get_announcement_initial(meeting, is_change=False):
'''Returns a dictionary suitable to initialize an InterimAnnouncementForm
(Message ModelForm)'''
group = meeting.session_set.first().group
in_person = bool(meeting.city)
initial = {}
initial['to'] = settings.INTERIM_ANNOUNCE_TO_EMAIL
initial['cc'] = group.list_email
initial['frm'] = settings.INTERIM_ANNOUNCE_FROM_EMAIL
if in_person:
desc = 'Interim'
else:
desc = 'Virtual'
if is_change:
change = ' CHANGED'
else:
change = ''
if group.type.slug == 'rg':
type = 'RG'
elif group.type.slug == 'wg' and group.state.slug == 'bof':
type = 'BOF'
else:
type = 'WG'
initial['subject'] = '{name} ({acronym}) {type} {desc} Meeting: {date}{change}'.format(
name=group.name,
acronym=group.acronym,
type=type,
desc=desc,
date=meeting.date,
change=change)
body = render_to_string('meeting/interim_announcement.txt', locals())
initial['body'] = body
return initial
def get_earliest_session_date(formset):
'''Return earliest date from InterimSession Formset'''
return sorted([f.cleaned_data['date'] for f in formset.forms if f.cleaned_data.get('date')])[0]
def get_interim_initial(meeting):
'''Returns a dictionary suitable to initialize a InterimRequestForm'''
initial = {}
initial['group'] = meeting.session_set.first().group
if meeting.city:
initial['in_person'] = True
else:
initial['in_person'] = False
if meeting.session_set.count() > 1:
initial['meeting_type'] = 'multi-day'
else:
initial['meeting_type'] = 'single'
if meeting.session_set.first().status.slug == 'apprw':
initial['approved'] = False
else:
initial['approved'] = True
return initial
def get_interim_session_initial(meeting):
'''Returns a list of dictionaries suitable to initialize a InterimSessionForm'''
initials = []
for session in meeting.session_set.all():
initial = {}
initial['date'] = session.official_timeslotassignment().timeslot.time
initial['time'] = session.official_timeslotassignment().timeslot.time
initial['duration'] = session.requested_duration
initial['remote_instructions'] = session.remote_instructions
initial['agenda_note'] = session.agenda_note
doc = session.agenda()
if doc:
path = os.path.join(doc.get_file_path(), doc.filename_with_rev())
initial['agenda'] = get_document_content(os.path.basename(path), path, markup=False)
initials.append(initial)
return initials
def is_meeting_approved(meeting):
"""Returns True if the meeting is approved"""
if meeting.session_set.first().status.slug == 'apprw':
return False
else:
return True
def get_next_interim_number(group, date):
"""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,
group=group.acronym))
if meetings:
sequences = [int(m.number.split('-')[-1]) for m in meetings]
last_sequence = sorted(sequences)[-1]
else:
last_sequence = 0
return 'interim-{year}-{group}-{sequence}'.format(
year=date.year,
group=group.acronym,
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 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 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"""
for form in forms:
if not form.has_changed():
continue
if ('date' in form.changed_data) or ('time' in form.changed_data):
assign_interim_session(form)
if 'agenda' in form.changed_data:
form.save_agenda()

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('meeting', '0022_auto_20160505_0523'),
]
operations = [
migrations.AddField(
model_name='session',
name='remote_instructions',
field=models.CharField(max_length=1024, blank=True),
preserve_default=True,
),
]

View file

@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import datetime
import os
import subprocess
from django.conf import settings
from django.db import migrations
def get_old_path(meeting):
"""Return old path to interim materials file"""
path = os.path.join(settings.AGENDA_PATH,
'interim',
meeting.date.strftime('%Y'),
meeting.date.strftime('%m'),
meeting.date.strftime('%d'),
meeting.session_set.first().group.acronym) + '/'
#doc.type_id,
#doc.external_url)
return path
def get_new_path(meeting):
"""Returns new path to document"""
return os.path.join(settings.AGENDA_PATH,meeting.number) + '/'
def copy_materials(meeting):
"""Copy all materials files to new location on disk"""
source = get_old_path(meeting)
target = get_new_path(meeting)
if not os.path.isdir(target):
os.makedirs(target)
subprocess.call(['rsync','-a',source,target])
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)
def migrate_interim_materials_files(apps, schema_editor):
"""Copy interim materials files to new location"""
Meeting = apps.get_model("meeting", "Meeting")
for meeting in Meeting.objects.filter(type='interim'):
copy_materials(meeting)
class Migration(migrations.Migration):
dependencies = [
('meeting', '0023_session_remote_instructions'),
]
operations = [
migrations.RunPython(migrate_interim_meetings),
migrations.RunPython(migrate_interim_materials_files),
]

View file

@ -143,17 +143,7 @@ class Meeting(models.Model):
return date + datetime.timedelta(days=-date.weekday(), weeks=1)
def get_materials_path(self):
path = ''
if self.type_id == 'ietf':
path = os.path.join(settings.AGENDA_PATH,self.number)
elif self.type_id == 'interim':
path = os.path.join(settings.AGENDA_PATH,
'interim',
self.date.strftime('%Y'),
self.date.strftime('%m'),
self.date.strftime('%d'),
self.session_set.all()[0].group.acronym)
return path
return os.path.join(settings.AGENDA_PATH,self.number)
# the various dates are currently computed
def get_submission_start_date(self):
@ -919,6 +909,7 @@ class Session(models.Model):
status = models.ForeignKey(SessionStatusName)
scheduled = models.DateTimeField(null=True, blank=True)
modified = models.DateTimeField(auto_now=True)
remote_instructions = models.CharField(blank=True,max_length=1024)
materials = models.ManyToManyField(Document, through=SessionPresentation, blank=True)
resources = models.ManyToManyField(ResourceAssociation)
@ -1007,7 +998,7 @@ class Session(models.Model):
return Constraint.objects.filter(target=self.group, meeting=self.meeting).order_by('name__name')
def timeslotassignment_for_agenda(self, schedule):
return self.timeslotassignments.filter(schedule=schedule)[0]
return self.timeslotassignments.filter(schedule=schedule).first()
def official_timeslotassignment(self):
return self.timeslotassignment_for_agenda(self.meeting.agenda)

View file

@ -3,10 +3,30 @@ 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
from ietf.name.models import RoomResourceName
from ietf.person.models import Person
from ietf.utils.test_data import make_test_data
def make_interim_meeting(group,date,status='sched'):
system_person = Person.objects.get(name="(System)")
time = datetime.datetime.combine(date, datetime.time(9))
meeting = create_interim_meeting(group=group,date=date)
session = Session.objects.create(meeting=meeting, group=group,
attendees=10, requested_by=system_person,
requested_duration=20, status_id=status,
remote_instructions='http://webex.com',
scheduled=datetime.datetime.now(),type_id="session")
slot = TimeSlot.objects.create(
meeting=meeting,
type_id="session",
duration=session.requested_duration,
time=time)
SchedTimeSessAssignment.objects.create(
timeslot=slot,
session=session,
schedule=session.meeting.agenda)
return meeting
def make_meeting_test_data():
if not Group.objects.filter(acronym='mars'):
@ -77,6 +97,16 @@ def make_meeting_test_data():
doc.set_state(State.objects.get(type='reuse_policy',slug='single'))
mars_session.sessionpresentation_set.add(SessionPresentation(session=mars_session,document=doc,rev=doc.rev))
# Future Interim Meetings
date = datetime.date.today() + datetime.timedelta(days=365)
date2 = datetime.date.today() + datetime.timedelta(days=1000)
ames = Group.objects.get(acronym="ames")
make_interim_meeting(group=mars,date=date,status='sched')
make_interim_meeting(group=mars,date=date2,status='apprw')
make_interim_meeting(group=ames,date=date,status='canceled')
make_interim_meeting(group=ames,date=date2,status='apprw')
return meeting

View file

@ -72,6 +72,25 @@ class ScheduleEditTests(StaticLiveServerTestCase):
time.sleep(0.1) # The API that modifies the database runs async
self.assertEqual(SchedTimeSessAssignment.objects.filter(session__meeting__number=42,session__group__acronym='mars').count(),0)
@skipIf(skip_selenium, skip_message)
class InterimRequestTests(StaticLiveServerTestCase):
def setUp(self):
condition_data()
self.driver = webdriver.PhantomJS(service_log_path=settings.TEST_GHOSTDRIVER_LOG_PATH)
self.driver.set_window_size(1024,768)
def absreverse(self,*args,**kwargs):
return '%s%s'%(self.live_server_url,urlreverse(*args,**kwargs))
def testInterimRequest(self):
url = self.absreverse('ietf.meeting.views.interim_request')
self.driver.get(url)
element = self.driver.find_element_by_id('id_form-0-date')
self.assertTrue(element)
def testJustSitThere(self):
time.sleep(10000)
# The following are useful debugging tools
# If you add this to a LiveServerTestCase and run just this test, you can browse

View file

@ -1,3 +1,4 @@
import json
import os
import shutil
import datetime
@ -7,13 +8,21 @@ import debug # pyflakes:ignore
from django.core.urlresolvers import reverse as urlreverse
from django.conf import settings
from django.contrib.auth.models import User
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.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
from ietf.person.factories import PersonFactory
from ietf.group.factories import GroupFactory
@ -446,3 +455,599 @@ class EditScheduleListTests(TestCase):
self.assertTrue(r.status_code, 302)
mtg = Meeting.objects.get(number=self.mtg.number)
self.assertEqual(mtg.agenda,schedule)
# -------------------------------------------------
# Interim Meeting Tests
# -------------------------------------------------
class InterimTests(TestCase):
def check_interim_tabs(self, url):
'''Helper function to check interim meeting list tabs'''
# no logged in - no tabs
r = self.client.get(url)
q = PyQuery(r.content)
self.assertEqual(len(q("ul.nav-tabs")), 0)
# plain user - no tabs
username = "plain"
self.client.login(username=username, password=username + "+password")
r = self.client.get(url)
q = PyQuery(r.content)
self.assertEqual(len(q("ul.nav-tabs")), 0)
self.client.logout()
# privileged user
username = "ad"
self.client.login(username=username, password=username + "+password")
r = self.client.get(url)
q = PyQuery(r.content)
self.assertEqual(len(q("a:contains('Pending')")), 1)
self.assertEqual(len(q("a:contains('Announce')")), 0)
self.client.logout()
# secretariat
username = "secretary"
self.client.login(username=username, password=username + "+password")
r = self.client.get(url)
q = PyQuery(r.content)
self.assertEqual(len(q("a:contains('Pending')")), 1)
self.assertEqual(len(q("a:contains('Announce')")), 1)
self.client.logout()
def test_interim_announce(self):
make_meeting_test_data()
url = urlreverse("ietf.meeting.views.interim_announce")
meeting = Meeting.objects.filter(type='interim', session__group__acronym='mars').first()
session = meeting.session_set.first()
session.status = SessionStatusName.objects.get(slug='scheda')
session.save()
login_testing_unauthorized(self, "secretary", url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertTrue(meeting.number in r.content)
def test_interim_send_announcement(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_send_announcement", kwargs={'number': meeting.number})
login_testing_unauthorized(self, "secretary", url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
initial = r.context['form'].initial
# send announcement
len_before = len(outbox)
r = self.client.post(url, initial)
self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_announce'))
self.assertEqual(len(outbox), len_before + 1)
self.assertTrue('WG Virtual Meeting' in outbox[-1]['Subject'])
def test_interim_approve(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, {'approve': 'approve'})
self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_send_announcement', kwargs={'number': meeting.number}))
for session in meeting.session_set.all():
self.assertEqual(session.status.slug, 'scheda')
def test_upcoming(self):
make_meeting_test_data()
url = urlreverse("ietf.meeting.views.upcoming")
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()
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_ical(self):
make_meeting_test_data()
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.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'''
make_meeting_test_data()
# test unauthorized not logged in
upcoming_url = urlreverse("ietf.meeting.views.upcoming")
request_url = urlreverse("ietf.meeting.views.interim_request")
r = self.client.get(upcoming_url)
self.assertNotContains(r,'Request new interim meeting')
# test unauthorized user
login_testing_unauthorized(self,"plain",request_url)
r = self.client.get(upcoming_url)
self.assertNotContains(r,'Request new interim meeting')
r = self.client.get(request_url)
self.assertEqual(r.status_code, 403)
self.client.logout()
# test authorized
for username in ('secretary','ad','marschairman','irtf-chair','irgchairman'):
self.client.login(username=username, password= username + "+password")
r = self.client.get(upcoming_url)
self.assertContains(r,'Request new interim meeting')
r = self.client.get(request_url)
self.assertEqual(r.status_code, 200)
self.client.logout()
def test_interim_request_options(self):
make_meeting_test_data()
# secretariat can request for any group
self.client.login(username="secretary", password="secretary+password")
r = self.client.get("/meeting/interim/request/")
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(Group.objects.filter(type__in=('wg', 'rg'), state__in=('active', 'proposed')).count(),
len(q("#id_group option")) - 1) # -1 for options placeholder
def test_interim_request_single(self):
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}
r = self.client.post(urlreverse("ietf.meeting.views.interim_request"),data)
self.assertRedirects(r,urlreverse('ietf.meeting.views.upcoming'))
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,'01'))
self.assertEqual(meeting.city,'')
self.assertEqual(meeting.country,'')
self.assertEqual(meeting.time_zone,'UTC')
session = meeting.session_set.first()
self.assertEqual(session.remote_instructions,remote_instructions)
self.assertEqual(session.agenda_note,agenda_note)
timeslot = session.official_timeslotassignment().timeslot
self.assertEqual(timeslot.time,dt)
self.assertEqual(timeslot.duration,duration)
# ensure agenda document was created
self.assertEqual(session.materials.count(),1)
doc = session.materials.first()
path = os.path.join(doc.get_file_path(),doc.filename_with_rev())
self.assertTrue(os.path.exists(path))
def test_interim_request_single_in_person(self):
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)
city = 'San Francisco'
country = 'US'
time_zone = 'US/Pacific'
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':city,
'country':country,
'time_zone':time_zone,
'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}
r = self.client.post(urlreverse("ietf.meeting.views.interim_request"),data)
self.assertRedirects(r,urlreverse('ietf.meeting.views.upcoming'))
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,'01'))
self.assertEqual(meeting.city,city)
self.assertEqual(meeting.country,country)
self.assertEqual(meeting.time_zone,time_zone)
session = meeting.session_set.first()
self.assertEqual(session.remote_instructions,remote_instructions)
self.assertEqual(session.agenda_note,agenda_note)
timeslot = session.official_timeslotassignment().timeslot
self.assertEqual(timeslot.time,dt)
self.assertEqual(timeslot.duration,duration)
def test_interim_request_multi_day(self):
make_meeting_test_data()
date = datetime.date.today() + datetime.timedelta(days=30)
date2 = date + datetime.timedelta(days=1)
time = datetime.datetime.now().time().replace(microsecond=0,second=0)
dt = datetime.datetime.combine(date, time)
dt2 = datetime.datetime.combine(date2, time)
duration = datetime.timedelta(hours=3)
group = Group.objects.get(acronym='mars')
city = 'San Francisco'
country = 'US'
time_zone = 'US/Pacific'
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':'multi-day',
'city':city,
'country':country,
'time_zone':time_zone,
'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-1-date':date2.strftime("%Y-%m-%d"),
'session_set-1-time':time.strftime('%H:%M'),
'session_set-1-requested_duration':'03:00:00',
'session_set-1-remote_instructions':remote_instructions,
'session_set-1-agenda':agenda,
'session_set-1-agenda_note':agenda_note,
'session_set-TOTAL_FORMS':2,
'session_set-INITIAL_FORMS':0}
r = self.client.post(urlreverse("ietf.meeting.views.interim_request"),data)
self.assertRedirects(r,urlreverse('ietf.meeting.views.upcoming'))
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,'01'))
self.assertEqual(meeting.city,city)
self.assertEqual(meeting.country,country)
self.assertEqual(meeting.time_zone,time_zone)
self.assertEqual(meeting.session_set.count(),2)
# first sesstion
session = meeting.session_set.all()[0]
self.assertEqual(session.remote_instructions,remote_instructions)
timeslot = session.official_timeslotassignment().timeslot
self.assertEqual(timeslot.time,dt)
self.assertEqual(timeslot.duration,duration)
self.assertEqual(session.agenda_note,agenda_note)
# second sesstion
session = meeting.session_set.all()[1]
self.assertEqual(session.remote_instructions,remote_instructions)
timeslot = session.official_timeslotassignment().timeslot
self.assertEqual(timeslot.time,dt2)
self.assertEqual(timeslot.duration,duration)
self.assertEqual(session.agenda_note,agenda_note)
def test_interim_request_series(self):
make_meeting_test_data()
meeting_count_before = Meeting.objects.filter(type='interim').count()
date = datetime.date.today() + datetime.timedelta(days=30)
date2 = date + datetime.timedelta(days=1)
time = datetime.datetime.now().time().replace(microsecond=0,second=0)
dt = datetime.datetime.combine(date, time)
dt2 = datetime.datetime.combine(date2, time)
duration = datetime.timedelta(hours=3)
group = Group.objects.get(acronym='mars')
city = ''
country = ''
time_zone = 'US/Pacific'
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':'series',
'city':city,
'country':country,
'time_zone':time_zone,
'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-1-date':date2.strftime("%Y-%m-%d"),
'session_set-1-time':time.strftime('%H:%M'),
'session_set-1-requested_duration':'03:00:00',
'session_set-1-remote_instructions':remote_instructions,
'session_set-1-agenda':agenda,
'session_set-1-agenda_note':agenda_note,
'session_set-TOTAL_FORMS':2,
'session_set-INITIAL_FORMS':0}
r = self.client.post(urlreverse("ietf.meeting.views.interim_request"),data)
self.assertRedirects(r,urlreverse('ietf.meeting.views.upcoming'))
meeting_count_after = Meeting.objects.filter(type='interim').count()
self.assertEqual(meeting_count_after,meeting_count_before + 2)
meetings = Meeting.objects.order_by('-id')[:2]
# first meeting
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,'01'))
self.assertEqual(meeting.city,city)
self.assertEqual(meeting.country,country)
self.assertEqual(meeting.time_zone,time_zone)
self.assertEqual(meeting.session_set.count(),1)
session = meeting.session_set.first()
self.assertEqual(session.remote_instructions,remote_instructions)
timeslot = session.official_timeslotassignment().timeslot
self.assertEqual(timeslot.time,dt)
self.assertEqual(timeslot.duration,duration)
self.assertEqual(session.agenda_note,agenda_note)
# second meeting
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,'02'))
self.assertEqual(meeting.city,city)
self.assertEqual(meeting.country,country)
self.assertEqual(meeting.time_zone,time_zone)
self.assertEqual(meeting.session_set.count(),1)
session = meeting.session_set.first()
self.assertEqual(session.remote_instructions,remote_instructions)
timeslot = session.official_timeslotassignment().timeslot
self.assertEqual(timeslot.time,dt2)
self.assertEqual(timeslot.duration,duration)
self.assertEqual(session.agenda_note,agenda_note)
def test_interim_pending(self):
make_meeting_test_data()
url = urlreverse('ietf.meeting.views.interim_pending')
count = Meeting.objects.filter(type='interim',session__status='apprw').distinct().count()
# unpriviledged user
login_testing_unauthorized(self,"plain",url)
r = self.client.get(url)
self.assertEqual(r.status_code, 403)
# secretariat
login_testing_unauthorized(self,"secretary",url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q("#pending-interim-meetings-table tr"))-1, count)
self.client.logout()
def test_can_approve_interim_request(self):
make_meeting_test_data()
# unprivileged user
user = User.objects.get(username='plain')
group = Group.objects.get(acronym='mars')
meeting = Meeting.objects.filter(type='interim',session__status='apprw',session__group=group).first()
self.assertFalse(can_approve_interim_request(meeting=meeting,user=user))
# Secretariat
user = User.objects.get(username='secretary')
self.assertTrue(can_approve_interim_request(meeting=meeting,user=user))
# related AD
user = User.objects.get(username='ad')
self.assertTrue(can_approve_interim_request(meeting=meeting,user=user))
# other AD
user = User.objects.get(username='ops-ad')
self.assertFalse(can_approve_interim_request(meeting=meeting,user=user))
# WG Chair
user = User.objects.get(username='marschairman')
self.assertFalse(can_approve_interim_request(meeting=meeting,user=user))
def test_can_view_interim_request(self):
make_meeting_test_data()
# unprivileged user
user = User.objects.get(username='plain')
group = Group.objects.get(acronym='mars')
meeting = Meeting.objects.filter(type='interim',session__status='apprw',session__group=group).first()
self.assertFalse(can_view_interim_request(meeting=meeting,user=user))
# Secretariat
user = User.objects.get(username='secretary')
self.assertTrue(can_view_interim_request(meeting=meeting,user=user))
# related AD
user = User.objects.get(username='ad')
self.assertTrue(can_view_interim_request(meeting=meeting,user=user))
# other AD
user = User.objects.get(username='ops-ad')
self.assertTrue(can_view_interim_request(meeting=meeting,user=user))
# WG Chair
user = User.objects.get(username='marschairman')
self.assertTrue(can_view_interim_request(meeting=meeting,user=user))
# Other WG Chair
user = User.objects.get(username='ameschairman')
self.assertFalse(can_view_interim_request(meeting=meeting,user=user))
def test_interim_request_details(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.get(url)
self.assertEqual(r.status_code, 200)
def test_interim_request_disapprove(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,{'disapprove':'Disapprove'})
self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_pending'))
for session in meeting.session_set.all():
self.assertEqual(session.status_id,'disappr')
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})
# 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.agenda_note, 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.agenda_note, 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()
meeting = Meeting.objects.filter(type='interim', session__status='apprw', session__group__acronym='mars').first()
group = meeting.session_set.first().group
url = urlreverse('ietf.meeting.views.interim_request_edit', kwargs={'number': meeting.number})
# test unauthorized access
self.client.login(username="ameschairman", password="ameschairman+password")
r = self.client.get(url)
self.assertEqual(r.status_code, 403)
# test authorized use
login_testing_unauthorized(self, "secretary", url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
# post changes
length_before = len(outbox)
form_initial = r.context['form'].initial
formset_initial = r.context['formset'].forms[0].initial
new_time = formset_initial['time'] + datetime.timedelta(hours=1)
data = {'group':group.pk,
'meeting_type':'single',
'session_set-0-id':meeting.session_set.first().id,
'session_set-0-date':formset_initial['date'].strftime('%Y-%m-%d'),
'session_set-0-time':new_time.strftime('%H:%M'),
'session_set-0-requested_duration':formset_initial['requested_duration'],
'session_set-0-remote_instructions':formset_initial['remote_instructions'],
#'session_set-0-agenda':formset_initial['agenda'],
'session_set-0-agenda_note':formset_initial['agenda_note'],
'session_set-TOTAL_FORMS':1,
'session_set-INITIAL_FORMS':1}
data.update(form_initial)
r = self.client.post(url, data)
self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_request_details', kwargs={'number': meeting.number}))
self.assertEqual(len(outbox),length_before+1)
self.assertTrue('CHANGED' in outbox[-1]['Subject'])
session = meeting.session_set.first()
timeslot = session.official_timeslotassignment().timeslot
self.assertEqual(timeslot.time,new_time)
def test_interim_request_details_permissions(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})
# unprivileged user
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'])
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'])
class AjaxTests(TestCase):
def test_ajax_get_utc(self):
# test bad queries
url = urlreverse('ietf.meeting.views.ajax_get_utc') + "?date=2016-1-1&time=badtime&timezone=UTC"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
data = json.loads(r.content)
self.assertEqual(data["error"], True)
url = urlreverse('ietf.meeting.views.ajax_get_utc') + "?date=2016-1-1&time=25:99&timezone=UTC"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
data = json.loads(r.content)
self.assertEqual(data["error"], True)
# test good query
url = urlreverse('ietf.meeting.views.ajax_get_utc') + "?date=2016-1-1&time=12:00&timezone=US/Pacific"
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
data = json.loads(r.content)
self.assertTrue('timezone' in data)
self.assertTrue('time' in data)
self.assertTrue('utc' in data)
self.assertTrue('error' not in data)
self.assertEqual(data['utc'], '20:00')

View file

@ -63,13 +63,22 @@ type_ietf_only_patterns_id_optional = [
]
urlpatterns = [
url(r'^ajax/get-utc/$', views.ajax_get_utc),
url(r'^requests.html$', RedirectView.as_view(url='/meeting/requests', permanent=True)),
url(r'^(?P<num>\d+)/requests.html$', RedirectView.as_view(url='/meeting/%(num)s/requests', permanent=True)),
url(r'^(?P<num>[A-Za-z0-9._+-]+)/', include(safe_for_all_meeting_types)),
# The optionals have to go first, otherwise the agenda/(owner)/(name)/ patterns match things they shouldn't
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.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),
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

@ -9,6 +9,7 @@ from tempfile import mkstemp
from collections import OrderedDict, Counter
import csv
import json
import pytz
import debug # pyflakes:ignore
@ -19,10 +20,13 @@ from django.contrib import messages
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
from django.forms.models import modelform_factory, inlineformset_factory
from django.forms import ModelForm
from django.template.loader import render_to_string
from django.utils.functional import curry
from django.views.decorators.csrf import ensure_csrf_cookie
from ietf.doc.fields import SearchableDocumentsField
from ietf.doc.models import Document, State, DocEvent
from ietf.group.models import Group
from ietf.group.utils import can_manage_materials
@ -35,11 +39,42 @@ from ietf.meeting.helpers import get_modified_from_assignments
from ietf.meeting.helpers import get_wg_list, find_ads_for_meeting
from ietf.meeting.helpers import get_meeting, get_schedule, agenda_permissions, get_meetings
from ietf.meeting.helpers import preprocess_assignments_for_agenda, read_agenda_file
from ietf.meeting.helpers import convert_draft_to_pdf
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_edit_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
from ietf.doc.fields import SearchableDocumentsField
from .forms import (InterimMeetingModelForm, InterimAnnounceForm, InterimSessionModelForm,
InterimCancelForm)
def get_menu_entries(request):
'''Setup menu entries for interim meeting view tabs'''
entries = []
if has_role(request.user, ('Area Director','Secretariat','IRTF Chair','WG Chair', 'RG Chair')):
entries.append(("Upcoming", reverse("ietf.meeting.views.upcoming")))
entries.append(("Pending", reverse("ietf.meeting.views.interim_pending")))
if has_role(request.user, "Secretariat"):
entries.append(("Announce", reverse("ietf.meeting.views.interim_announce")))
return entries
def send_interim_change_notice(request, meeting):
"""Sends an email notifying changes to a previously scheduled / announced meeting"""
group = meeting.session_set.first().group
form = InterimAnnounceForm(get_announcement_initial(meeting, is_change=True))
message = form.save(user=request.user)
message.related_groups.add(group)
send_mail_message(request, message)
# -------------------------------------------------
# View Functions
# -------------------------------------------------
def materials(request, num=None):
meeting = get_meeting(num)
@ -986,3 +1021,369 @@ def delete_schedule(request, num, owner, name):
}
)
# -------------------------------------------------
# Interim Views
# -------------------------------------------------
def ajax_get_utc(request):
'''Ajax view that takes arguments time, timezone, date and returns UTC data'''
time = request.GET.get('time')
timezone = request.GET.get('timezone')
date = request.GET.get('date')
time_re = re.compile(r'^\d{2}:\d{2}')
# validate input
if not time_re.match(time) or not date:
return HttpResponse(json.dumps({'error': True}),
content_type='application/json')
hour, minute = time.split(':')
if not (int(hour) <= 23 and int(minute) <= 59):
return HttpResponse(json.dumps({'error': True}),
content_type='application/json')
year, month, day = date.split('-')
dt = datetime.datetime(int(year), int(month), int(day), int(hour), int(minute))
tz = pytz.timezone(timezone)
aware_dt = tz.localize(dt, is_dst=None)
utc_dt = aware_dt.astimezone(pytz.utc)
utc = utc_dt.strftime('%H:%M')
# calculate utc day offset
naive_utc_dt = utc_dt.replace(tzinfo=None)
utc_day_offset = (naive_utc_dt.date() - dt.date()).days
html = "<span>{utc} UTC</span>".format(utc=utc)
if utc_day_offset != 0:
html = html + "<span class='day-offset'> {0:+d} Day</span>".format(utc_day_offset)
context_data = {'timezone': timezone,
'time': time,
'utc': utc,
'utc_day_offset': utc_day_offset,
'html': html}
return HttpResponse(json.dumps(context_data),
content_type='application/json')
@role_required('Secretariat',)
def interim_announce(request):
'''View which shows interim meeting requests awaiting announcement'''
meetings = Meeting.objects.filter(type='interim', session__status='scheda').distinct()
menu_entries = get_menu_entries(request)
selected_menu_entry = 'announce'
return render(request, "meeting/interim_announce.html", {
'menu_entries': menu_entries,
'selected_menu_entry': selected_menu_entry,
'meetings': meetings})
@role_required('Secretariat',)
def interim_send_announcement(request, number):
'''View for sending the announcement of a new interim meeting'''
meeting = get_object_or_404(Meeting, number=number)
group = meeting.session_set.first().group
if request.method == 'POST':
form = InterimAnnounceForm(request.POST,
initial=get_announcement_initial(meeting))
if form.is_valid():
message = form.save(user=request.user)
message.related_groups.add(group)
meeting.session_set.update(status_id='sched')
send_mail_message(request, message)
messages.success(request, 'Interim meeting announcement sent')
return redirect(interim_announce)
form = InterimAnnounceForm(initial=get_announcement_initial(meeting))
return render(request, "meeting/interim_send_announcement.html", {
'meeting': meeting,
'form': form})
@role_required('Area Director', 'Secretariat', 'IRTF Chair', 'WG Chair',
'RG Chair')
def interim_pending(request):
'''View which shows interim meeting requests pending approval'''
meetings = Meeting.objects.filter(type='interim', session__status='apprw').distinct().order_by('date')
menu_entries = get_menu_entries(request)
selected_menu_entry = 'pending'
meetings = [m for m in meetings if can_view_interim_request(
m, request.user)]
for meeting in meetings:
if can_approve_interim_request(meeting, request.user):
meeting.can_approve = True
return render(request, "meeting/interim_pending.html", {
'menu_entries': menu_entries,
'selected_menu_entry': selected_menu_entry,
'meetings': meetings})
@role_required('Area Director', 'Secretariat', 'IRTF Chair', 'WG Chair',
'RG Chair')
def interim_request(request):
'''View for requesting an interim meeting'''
SessionFormset = inlineformset_factory(
Meeting,
Session,
form=InterimSessionModelForm,
can_delete=False, extra=2)
if request.method == 'POST':
form = InterimMeetingModelForm(request, data=request.POST)
formset = SessionFormset(instance=Meeting(), data=request.POST)
if form.is_valid() and formset.is_valid():
group = form.cleaned_data.get('group')
is_approved = form.cleaned_data.get('approved', False)
meeting_type = form.cleaned_data.get('meeting_type')
# pre create meeting
if meeting_type in ('single', 'multi-day'):
meeting = form.save(date=get_earliest_session_date(formset))
# need to use curry here to pass custom variable to form init
SessionFormset.form = staticmethod(curry(
InterimSessionModelForm,
user=request.user,
group=group,
is_approved=is_approved))
formset = SessionFormset(instance=meeting, data=request.POST)
formset.is_valid()
formset.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,
group=group,
is_approved=is_approved))
formset = SessionFormset(instance=Meeting(), data=request.POST)
formset.is_valid() # re-validate
for session_form in formset.forms:
if not session_form.has_changed():
continue
# create meeting
form = InterimMeetingModelForm(request, data=request.POST)
form.is_valid()
meeting = form.save(date=session_form.cleaned_data['date'])
# create save session
session = session_form.save(commit=False)
session.meeting = meeting
session.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:
form = InterimMeetingModelForm(request=request,
initial={'meeting_type': 'single'})
formset = SessionFormset()
return render(request, "meeting/interim_request.html", {
"form": form,
"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(agenda_note=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,
"meeting": meeting})
@role_required('Area Director', 'Secretariat', 'IRTF Chair', 'WG Chair',
'RG Chair')
def interim_request_details(request, number):
'''View details of an interim meeting reqeust'''
meeting = get_object_or_404(Meeting, number=number)
sessions = meeting.session_set.all()
can_edit = can_edit_interim_request(meeting, request.user)
can_approve = can_approve_interim_request(meeting, request.user)
if request.method == 'POST':
if request.POST.get('approve') and can_approve_interim_request(meeting, request.user):
meeting.session_set.update(status_id='scheda')
messages.success(request, 'Interim meeting approved')
if has_role(request.user, 'Secretariat'):
return redirect(interim_send_announcement, number=number)
else:
return redirect(interim_pending)
if request.POST.get('disapprove') and can_approve_interim_request(meeting, request.user):
meeting.session_set.update(status_id='disappr')
messages.success(request, 'Interim meeting disapproved')
return redirect(interim_pending)
return render(request, "meeting/interim_request_details.html", {
"meeting": meeting,
"sessions": sessions,
"can_edit": can_edit,
"can_approve": can_approve})
@role_required('Area Director', 'Secretariat', 'IRTF Chair', 'WG Chair',
'RG Chair')
def interim_request_edit(request, number):
'''Edit details of an interim meeting reqeust'''
meeting = get_object_or_404(Meeting, number=number)
if not can_edit_interim_request(meeting, request.user):
return HttpResponseForbidden("You do not have permissions to edit this meeting request")
SessionFormset = inlineformset_factory(
Meeting,
Session,
form=InterimSessionModelForm,
can_delete=False,
extra=1)
if request.method == 'POST':
form = InterimMeetingModelForm(request=request, instance=meeting,
data=request.POST)
group = Group.objects.get(pk=form.data['group'])
is_approved = is_meeting_approved(meeting)
SessionFormset.form = staticmethod(curry(
InterimSessionModelForm,
user=request.user,
group=group,
is_approved=is_approved))
formset = SessionFormset(instance=meeting,
data=request.POST)
if form.is_valid() and formset.is_valid():
meeting = form.save(date=get_earliest_session_date(formset))
formset.save()
sessions_post_save(formset)
message = 'Interim meeting request saved'
if form.has_changed() or formset.has_changed():
send_interim_change_notice(request, meeting)
message = message + ' and change announcement sent'
messages.success(request, message)
return redirect(interim_request_details, number=number)
else:
assert False, (form.errors, formset.errors)
else:
form = InterimMeetingModelForm(request=request, instance=meeting)
formset = SessionFormset(instance=meeting)
return render(request, "meeting/interim_request_edit.html", {
"meeting": meeting,
"form": form,
"formset": formset})
def upcoming(request):
'''List of upcoming meetings'''
today = datetime.datetime.today()
meetings = Meeting.objects.filter(date__gte=today).exclude(
session__status__in=('apprw', 'scheda', 'canceledpa')).order_by('date')
# extract groups hierarchy for display filter
seen = set()
groups = [m.session_set.first().group for m
in meetings.filter(type='interim')]
group_parents = []
for g in groups:
if g.parent.acronym not in seen:
group_parents.append(g.parent)
seen.add(g.parent.acronym)
seen = set()
for p in group_parents:
p.group_list = []
for g in groups:
if g.acronym not in seen and g.parent == p:
p.group_list.append(g)
seen.add(g.acronym)
p.group_list.sort(key=lambda g: g.acronym)
# add menu entries
menu_entries = get_menu_entries(request)
selected_menu_entry = 'upcoming'
# 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(('Download as .ics',
reverse('ietf.meeting.views.upcoming_ical')))
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')
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]
# gather vtimezones
vtimezones = set()
for meeting in meetings:
if meeting.vtimezone():
vtimezones.add(meeting.vtimezone())
vtimezones = ''.join(vtimezones)
# icalendar response file should have '\r\n' line endings per RFC5545
response = render_to_string('meeting/upcoming.ics', {
'vtimezones': vtimezones,
'assignments': assignments})
response = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", response)
response = HttpResponse(response, content_type='text/calendar')
response['Content-Disposition'] = 'attachment; filename="upcoming.ics"'
return response

View file

@ -2022,6 +2022,16 @@
"model": "name.sessionstatusname",
"pk": "sched"
},
{
"fields": {
"order": 0,
"used": true,
"name": "Scheduled - Announcement to be sent",
"desc": ""
},
"model": "name.sessionstatusname",
"pk": "scheda"
},
{
"fields": {
"order": 0,
@ -2032,6 +2042,16 @@
"model": "name.sessionstatusname",
"pk": "canceled"
},
{
"fields": {
"order": 0,
"used": true,
"name": "Cancelled - Pre Announcement",
"desc": ""
},
"model": "name.sessionstatusname",
"pk": "canceledpa"
},
{
"fields": {
"order": 0,
@ -4659,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:\", \" }}",
@ -5882,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,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
def populate_names(apps, schema_editor):
SessionStatusName = apps.get_model("name", "SessionStatusName")
SessionStatusName.objects.create(slug="scheda",name="Scheduled - Announcement to be sent")
SessionStatusName.objects.create(slug="canceledpa",name="Cancelled - Pre Announcement")
class Migration(migrations.Migration):
dependencies = [
('name', '0010_new_liaison_names'),
]
operations = [
migrations.RunPython(populate_names),
]

View file

@ -4,7 +4,6 @@ from django.conf import settings
from django.db import models
from ietf.meeting.models import Meeting
from ietf.secr.utils.meeting import get_upload_root
class InterimManager(models.Manager):
@ -43,8 +42,7 @@ class InterimMeeting(Meeting):
return None
def get_proceedings_path(self, group=None):
path = os.path.join(get_upload_root(self),'proceedings.html')
return path
return os.path.join(self.get_materials_path(),'proceedings.html')
def get_proceedings_url(self, group=None):
'''

View file

@ -26,7 +26,7 @@ from ietf.secr.proceedings.models import InterimMeeting # proxy model
from ietf.secr.proceedings.models import Registration
from ietf.secr.utils.document import get_rfc_num
from ietf.secr.utils.group import groups_by_session
from ietf.secr.utils.meeting import get_upload_root, get_proceedings_path, get_materials, get_session
from ietf.secr.utils.meeting import get_proceedings_path, get_materials, get_session
# -------------------------------------------------
@ -227,7 +227,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')
@ -236,7 +236,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)
@ -267,7 +267,7 @@ def create_proceedings(meeting, group, is_final=False):
docs = Document.objects.filter(group=group,type='draft').order_by('time')
meeting_root = get_upload_root(meeting)
meeting_root = meeting.get_materials_path()
if meeting.type.slug == 'ietf':
url_root = "%sproceedings/%s/" % (settings.IETF_HOST_URL,meeting.number)
else:

View file

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

View file

@ -22,7 +22,7 @@ from ietf.secr.sreq.forms import GroupSelectForm
from ietf.secr.utils.decorators import check_permissions, sec_only
from ietf.secr.utils.document import get_full_path
from ietf.secr.utils.group import get_my_groups, groups_by_session
from ietf.secr.utils.meeting import get_upload_root, get_materials, get_timeslot, get_proceedings_path, get_proceedings_url
from ietf.secr.utils.meeting import get_materials, get_timeslot, get_proceedings_path, get_proceedings_url
from ietf.doc.models import Document, DocAlias, DocEvent, State, NewRevisionDocEvent
from ietf.group.models import Group
from ietf.ietfauth.utils import has_role, role_required
@ -68,9 +68,9 @@ def get_doc_filename(doc):
session = doc.session_set.all()[0]
meeting = session.meeting
if doc.external_url:
return os.path.join(get_upload_root(meeting),doc.type.slug,doc.external_url)
return os.path.join(meeting.get_materials_path(),doc.type.slug,doc.external_url)
else:
path = os.path.join(get_upload_root(meeting),doc.type.slug,doc.name)
path = os.path.join(meeting.get_materials_path(),doc.type.slug,doc.name)
files = glob.glob(path + '.*')
# TODO we might want to choose from among multiple files using some logic
return files[0]
@ -156,18 +156,18 @@ def get_next_order_num(session):
def handle_upload_file(file,filename,meeting,subdir):
'''
This function takes a file object, a filename and a meeting object and subdir as string.
It saves the file to the appropriate directory, get_upload_root() + subdir.
It saves the file to the appropriate directory, get_materials_path() + subdir.
If the file is a zip file, it creates a new directory in 'slides', which is the basename of the
zip file and unzips the file in the new directory.
'''
base, extension = os.path.splitext(filename)
if extension == '.zip':
path = os.path.join(get_upload_root(meeting),subdir,base)
path = os.path.join(meeting.get_materials_path(),subdir,base)
if not os.path.exists(path):
os.mkdir(path)
else:
path = os.path.join(get_upload_root(meeting),subdir)
path = os.path.join(meeting.get_materials_path(),subdir)
if not os.path.exists(path):
os.makedirs(path)
@ -191,7 +191,7 @@ def make_directories(meeting):
'''
This function takes a meeting object and creates the appropriate materials directories
'''
path = get_upload_root(meeting)
path = meeting.get_materials_path()
os.umask(0)
for leaf in ('slides','agenda','minutes','id','rfc','bluesheets'):
target = os.path.join(path,leaf)
@ -374,7 +374,7 @@ def delete_interim_meeting(request, meeting_num):
group = sessions[0].group
# delete directories
path = get_upload_root(meeting)
path = meeting.get_materials_path()
# do a quick sanity check on this path before we go and delete it
parts = path.split('/')

View file

@ -36,8 +36,8 @@ class SessionRequestTestCase(TestCase):
self.assertEqual(r.status_code, 200)
sched = r.context['scheduled_groups']
unsched = r.context['unscheduled_groups']
self.failUnless(len(unsched) == 0)
self.failUnless(len(sched) > 0)
self.assertEqual(len(unsched),2)
self.assertEqual(len(sched),2)
class SubmitRequestCase(TestCase):
def test_submit_request(self):

View file

@ -29,9 +29,9 @@ def get_materials(group,meeting):
def get_proceedings_path(meeting,group):
if meeting.type_id == 'ietf':
path = os.path.join(get_upload_root(meeting),group.acronym + '.html')
path = os.path.join(meeting.get_materials_path(),group.acronym + '.html')
elif meeting.type_id == 'interim':
path = os.path.join(get_upload_root(meeting),'proceedings.html')
path = os.path.join(meeting.get_materials_path(),'proceedings.html')
return path
def get_proceedings_url(meeting,group=None):
@ -75,15 +75,3 @@ def get_timeslot(session, schedule=None):
else:
return None
def get_upload_root(meeting):
path = ''
if meeting.type.slug == 'ietf':
path = os.path.join(settings.AGENDA_PATH,meeting.number)
elif meeting.type.slug == 'interim':
path = os.path.join(settings.AGENDA_PATH,
'interim',
meeting.date.strftime('%Y'),
meeting.date.strftime('%m'),
meeting.date.strftime('%d'),
meeting.session_set.all()[0].group.acronym)
return path

View file

@ -447,9 +447,9 @@ DOC_HREFS = {
}
MEETING_DOC_HREFS = {
"agenda": "/meeting/{meeting}/agenda/{doc.group.acronym}/",
"minutes": "https://www.ietf.org/proceedings/{meeting}/minutes/{doc.external_url}",
"slides": "https://www.ietf.org/proceedings/{meeting}/slides/{doc.external_url}",
"agenda": "/meeting/{meeting.number}/agenda/{doc.group.acronym}/",
"minutes": "https://www.ietf.org/proceedings/{meeting.number}/minutes/{doc.external_url}",
"slides": "https://www.ietf.org/proceedings/{meeting.number}/slides/{doc.external_url}",
"recording": "{doc.external_url}",
}
@ -495,6 +495,10 @@ IDSUBMIT_FROM_EMAIL = 'IETF I-D Submission Tool <idsubmission@ietf.org>'
IDSUBMIT_ANNOUNCE_FROM_EMAIL = 'internet-drafts@ietf.org'
IDSUBMIT_ANNOUNCE_LIST_EMAIL = 'i-d-announce@ietf.org'
# Interim Meeting Tool settings
INTERIM_ANNOUNCE_FROM_EMAIL = 'IESG Secretary <iesg-secretary@ietf.org>'
INTERIM_ANNOUNCE_TO_EMAIL = 'IETF Announcement List <ietf-announce@ietf.org>'
# Days from meeting to day of cut off dates on submit -- cutoff_time_utc is added to this
IDSUBMIT_DEFAULT_CUTOFF_DAY_OFFSET_00 = 13
IDSUBMIT_DEFAULT_CUTOFF_DAY_OFFSET_01 = 13

View file

@ -460,6 +460,8 @@ label#list-feeds {
margin-left: 3em;
}
/* === Photo pages ========================================================== */
.photo-name {
height: 3em;
}
@ -482,3 +484,54 @@ ul.list-inline li {
background-color: #f8f8f8;
border: 1px solid #e0e0e0;
}
/* === Interim Meetings ===================================================== */
#meeting-type-options {
display: inline-block;
margin-left: 4em;
}
.fieldset {
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12) !important;
background-color: #f1f1f1;
padding: 16px 16px;
margin-bottom: 20px;
}
#interim-request-form .fieldset.template {
display: none;
}
#interim-request-form .time-field {
width: 100px;
}
ul.errorlist {
list-style-type: none;
padding-left:0;
}
#s2id_id_country {
display: inline-block;
}
#timezone-field {
display: inline-block;
}
.form-inline .form-control.location {
vertical-align: top;
}
.day-offset {
color: #d60000;
}
.utc-time {
display: inline-block;
vertical-align: middle;
color: #555;
}
/* ========================================================================== */

View file

@ -0,0 +1,219 @@
var interimRequest = {
// functions for Interim Meeting Request
init : function() {
// get elements
interimRequest.form = $(this);
interimRequest.addButton = $('#add_session');
interimRequest.inPerson = $('#id_in_person');
interimRequest.timezone = $('#id_time_zone');
// bind functions
$('.select2-field').select2();
interimRequest.addButton.click(interimRequest.addSession);
$('.btn-delete').click(interimRequest.deleteSession);
interimRequest.inPerson.change(interimRequest.toggleLocation);
$('input[name="meeting_type"]').change(interimRequest.meetingTypeChanged);
$('input[name$="-requested_duration"]').blur(interimRequest.calculateEndTime);
$('input[name$="-time"]').blur(interimRequest.calculateEndTime);
$('input[name$="-time"]').blur(interimRequest.updateInfo);
$('input[name$="-end_time"]').change(interimRequest.updateInfo);
interimRequest.timezone.change(interimRequest.timezoneChange);
// init
interimRequest.inPerson.each(interimRequest.toggleLocation);
interimRequest.checkAddButton();
interimRequest.checkHelpText();
interimRequest.checkTimezone();
$('input[name$="-time"]').each(interimRequest.calculateEndTime);
$('input[name$="-time"]').each(interimRequest.updateInfo);
$('#id_country').select2({placeholder:"Country"});
},
addSession : function() {
var template = interimRequest.form.find('.fieldset.template');
var el = template.clone(true);
var totalField = $('#id_session_set-TOTAL_FORMS');
var total = +totalField.val();
var meeting_type = $('input[name="meeting_type"]:checked').val();
el.find(':input').each(function() {
var name = $(this).attr('name').replace('-' + (total-1) + '-','-' + total + '-');
var id = 'id_' + name;
$(this).attr({'name': name, 'id': id}).val('');
});
el.find('label').each(function() {
var newFor = $(this).attr('for').replace('-' + (total-1) + '-','-' + total + '-');
$(this).attr('for', newFor);
});
el.find('div.utc-time').each(function() {
var newId = $(this).attr('id').replace('-' + (total-1) + '-','-' + total + '-');
$(this).attr('id', newId);
});
++total;
totalField.val(total);
template.before(el);
el.removeClass("template");
el.find(".select2-field").each(function () {
setupSelect2Field($(this));
});
// copy field contents
var first_session = $(".fieldset:first");
el.find("input[name$='remote_instructions']").val(first_session.find("input[name$='remote_instructions']").val());
$('.btn-delete').removeClass("hidden");
},
updateInfo : function() {
// makes ajax call to server and sets UTC field
var time = $(this).val();
if(!time){
return;
}
var url = "/meeting/ajax/get-utc";
var fieldset = $(this).parents(".fieldset");
var date = fieldset.find("input[name$='-date']").val();
var timezone = interimRequest.timezone.val();
var name = $(this).attr("id") + "_utc";
var utc = fieldset.find("#" + name);
//console.log(name,utc.attr("id"));
$.ajax({
url: url,
type: 'GET',
cache: false,
async: true,
dataType: 'json',
data: {date: date,
time: time,
timezone: timezone},
success: function(response){
if (!response.error && response.html) {
utc.html(response.html);
}
}
});
return false;
},
calculateEndTime : function() {
// gets called when either start_time or duration change
var fieldset = $(this).parents(".fieldset");
var start_time = fieldset.find("input[name$='-time']");
var end_time = fieldset.find("input[name$='-end_time']");
var duration = fieldset.find("input[name$='-requested_duration']");
if(!start_time.val() || !duration.val()){
return;
}
var start_values = start_time.val().split(":");
var duration_values = duration.val().split(":");
var d = new Date(2000,1,1,start_values[0],start_values[1]);
var d1 = new Date(d.getTime() + (duration_values[0]*60*60*1000));
var d2 = new Date(d1.getTime() + (duration_values[1]*60*1000));
end_time.val(interimRequest.get_formatted_time(d2));
end_time.trigger('change');
},
checkAddButton : function() {
var meeting_type = $('input[name="meeting_type"]:checked').val();
if(meeting_type == 'single'){
interimRequest.addButton.hide();
} else {
interimRequest.addButton.show();
}
},
checkHelpText : function() {
var meeting_type = $('input[name="meeting_type"]:checked').val();
if(meeting_type == 'single'){
$('.meeting-type-help').hide();
} else if(meeting_type == 'multi-day') {
$('.meeting-type-help').hide();
$('.mth-multi').show();
} else if(meeting_type == 'series') {
$('.meeting-type-help').hide();
$('.mth-series').show();
}
},
checkInPerson : function() {
var meeting_type = $('input[name="meeting_type"]:checked').val();
if(meeting_type == 'series'){
interimRequest.inPerson.prop('disabled', true);
interimRequest.inPerson.prop('checked', false);
interimRequest.toggleLocation();
} else {
interimRequest.inPerson.prop('disabled', false);
}
},
checkTimezone : function() {
if(window.Intl && typeof window.Intl === "object"){
var tzname = Intl.DateTimeFormat().resolvedOptions().timeZone;
if($('#id_time_zone option[value="'+tzname+'"]').length > 0){
$('#id_time_zone').val(tzname);
}
}
},
get_formatted_time : function (d) {
// returns time from Date object as HH:MM
var minutes = d.getMinutes().toString();
var hours = d.getHours().toString();
return interimRequest.pad(hours) + ":" + interimRequest.pad(minutes);
},
deleteSession : function() {
var fieldset = $(this).parents(".fieldset");
fieldset.remove();
var totalField = $('#id_form-TOTAL_FORMS');
var total = +totalField.val();
--total;
totalField.val(total);
if(total == 2){
$(".btn-delete").addClass("hidden");
}
},
get_formatted_utc_time : function (d) {
// returns time from Date object as HH:MM
var minutes = d.getUTCMinutes().toString();
var hours = d.getUTCHours().toString();
return interimRequest.pad(hours) + ":" + interimRequest.pad(minutes);
},
meetingTypeChanged : function () {
interimRequest.checkAddButton();
interimRequest.checkInPerson();
interimRequest.checkHelpText();
},
pad : function(str) {
// zero pads string 00
if(str.length == 1){
str = "0" + str;
}
return str;
},
timezoneChange : function() {
$("input[name$='-time']").trigger('blur');
$("input[name$='-end_time']").trigger('change');
},
toggleLocation : function() {
if(this.checked){
$(".location").prop('disabled', false);
} else {
$(".location").prop('disabled', true);
}
}
}
$(document).ready(function () {
$('#interim-request-form').each(interimRequest.init);
});

View file

@ -0,0 +1,74 @@
function toggle_visibility() {
var h = window.location.hash;
h = h.replace(/^#?,?/, '');
// reset UI elements to default state
$(".pickview").removeClass("active disabled");
$(".pickviewneg").addClass("active");
if (h) {
// if there are items in the hash, hide all rows
$('[id^="row-"]').hide();
// 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
v = v.replace(/^-/, '');
$('[id^="row-"]').filter('[id*="-' + v + '"]').hide();
$(".view." + v).find("button").removeClass("active disabled");
$("button.pickviewneg." + v).removeClass("active");
} else {
// this is a regular item: when present, show these rows
$('[id^="row-"]').filter('[id*="-' + v + '"]').show();
$(".view." + v).find("button").addClass("active disabled");
$("button.pickview." + v).addClass("active");
query_array.push("filters=" + v)
}
});
// 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
$('[id^="row-"]').show();
// adjust the custom .ics link
var link = $('a[href*="upcoming.ics"]');
link.attr("href",link.attr("href").split("?")[0]);
}
}
$(".pickview, .pickviewneg").click(function () {
var h = window.location.hash;
var item = $(this).text().trim().toLowerCase();
if ($(this).hasClass("pickviewneg")) {
item = "-" + item;
}
re = new RegExp('(^|#|,)' + item + "(,|$)");
if (h.match(re) == null) {
if (h.replace("#", "").length == 0) {
h = item;
} else {
h += "," + item;
}
h = h.replace(/^#?,/, '');
} else {
h = h.replace(re, "$2").replace(/^#?,/, '');
}
window.location.hash = h.replace(/^#$/, '');
toggle_visibility();
});
$(document).ready(function () {
toggle_visibility();
});

View file

@ -85,7 +85,7 @@
<li><a href="/meeting/agenda/">Agenda</a></li>
<li><a href="/meeting/">Materials</a></li>
<li><a href="https://www.ietf.org/meeting/proceedings.html">Past proceedings</a></li>
<li><a href="https://www.ietf.org/meeting/upcoming.html">Upcoming</a></li>
<li><a href="/meeting/upcoming">Upcoming</a></li>
<li><a href="/secr/sreq/">Request a session</a></li>
<li><a href="/meeting/requests">Session requests</a></li>
{% if flavor == "top" %}</ul>{% endif %}

View file

@ -0,0 +1,65 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load staticfiles bootstrap3 widget_tweaks %}
{% block title %}Announce Interim Meeting{% endblock %}
{% block pagehead %}
<link rel="stylesheet" href="{% static 'select2/select2.css' %}">
<link rel="stylesheet" href="{% static 'select2-bootstrap-css/select2-bootstrap.min.css' %}">
{% endblock %}
{% block content %}
{% origin %}
<h1>Announce Interim Meeting</h1>
{% if menu_entries %}
<ul class="nav nav-tabs" role="tablist">
{% for name, url in menu_entries %}
<li {% if selected_menu_entry == name.lower %}class="active"{% endif %}>
<a href="{{ url }}">{{ name }}</a>
</li>
{% endfor %}
</ul>
{% endif %}
{% if meetings %}
<table id="announce-interim-meetings-table" class="table table-condensed table-striped">
<thead>
<tr>
<th>Date</th>
<th>Group</th>
<th>Name</th>
</tr>
</thead>
<tbody>
{% for meeting in meetings %}
{% if meeting.type.slug == 'interim' %}
<tr id="row-{{ forloop.counter }}-{{ meeting.session_set.all.0.group.acronym }}">
{% else %}
<tr id="row-{{ forloop.counter }}-ietf">
{% endif %}
<td>{{ meeting.date }}</td>
{% if meeting.type.slug == 'interim' %}
<td>{{ meeting.session_set.all.0.group.acronym }}</td>
{% else %}
<td>ietf</td>
{% endif %}
<td>
<a href="{% url 'ietf.meeting.views.interim_request_details' number=meeting.number %}">{{ meeting.number }}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<h3>No interim meetings waiting announcement</h3>
{% endif %}
{% endblock %}
{% block js %}
<script src="{% static 'select2/select2.min.js' %}"></script>
<script src="{% static 'ietf/js/meeting-interim-request.js' %}"></script>
{% endblock %}

View file

@ -0,0 +1,19 @@
{% load ietf_filters %}{% if is_change %}MEETING DETAILS HAVE CHANGED. SEE LATEST DETAILS BELOW.
{% endif %}The {{ group.name }} ({{ group.acronym }}) {% if group.type.slug == "rg" %}Research Group{% elif group.state.slug == "active" %}Working Group{% elif group.state.slug == 'bof' %}BOF{% endif %} will hold
{% if meeting.session_set.count == 1 %}a{% if meeting.city %}n {% else %} virtual {% endif %}interim meeting on {{ meeting.date }} from {{ meeting.session_set.first | session_start_time | date:"H:i" }} to {{ meeting.session_set.first | session_end_time | date:"H:i" }} {{ meeting.time_zone }}.
{% else %}a multi-day {% if not meeting.city %}virtual {% endif %}interim meeting.
{% for session in meeting.session_set.all %}Session {{ forloop.counter }}:
{{ session | session_start_time | date:"Y-m-d" }} {{ session | session_start_time | date:"H:i" }} to {{ session | session_end_time | date:"H:i" }} {{ meeting.time_zone }}
{% endfor %}{% endif %}
{% if meeting.city %}Meeting Location:
{{ meeting.city }}, {{ meeting.country }}
{% endif %}Agenda:
{{ meeting.session_set.first.agenda | document_content | default_if_none:"(No agenda submitted)" }}
To participate remotely in the interim meeting:
{{ meeting.session_set.first.remote_instructions }}
{{ meeting.session_set.first.agenda_note }}

View file

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

View file

@ -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.
{{ meeting.session_set.0.agenda_note }}

View file

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

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,71 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load staticfiles bootstrap3 widget_tweaks %}
{% block title %}Interim Pending{% endblock %}
{% block pagehead %}
<link rel="stylesheet" href="{% static 'select2/select2.css' %}">
<link rel="stylesheet" href="{% static 'select2-bootstrap-css/select2-bootstrap.min.css' %}">
{% endblock %}
{% block content %}
{% origin %}
<h1>Pending Interim Meetings</h1>
{% if menu_entries %}
<ul class="nav nav-tabs" role="tablist">
{% for name, url in menu_entries %}
<li {% if selected_menu_entry == name.lower %}class="active"{% endif %}>
<a href="{{ url }}">{{ name }}</a>
</li>
{% endfor %}
</ul>
{% endif %}
{% if meetings %}
<table id="pending-interim-meetings-table" class="table table-condensed table-striped">
<thead>
<tr>
<th>Date</th>
<th>Group</th>
<th>Name</th>
<th></th>
</tr>
</thead>
<tbody>
{% for meeting in meetings %}
{% if meeting.type.slug == 'interim' %}
<tr id="row-{{ forloop.counter }}-{{ meeting.session_set.all.0.group.acronym }}">
{% else %}
<tr id="row-{{ forloop.counter }}-ietf">
{% endif %}
<td>{{ meeting.date }}</td>
{% if meeting.type.slug == 'interim' %}
<td>{{ meeting.session_set.all.0.group.acronym }}</td>
{% else %}
<td>ietf</td>
{% endif %}
<td>
{% if meeting.type.slug == "interim" %}
<a href="{% url 'ietf.meeting.views.interim_request_details' number=meeting.number %}">{{ meeting.number }}{% if meeting.session_set.all.0.status.slug == "canceled" %} -- CANCELLED --{% endif %}</a>
{% else %}
<a href="{% url 'ietf.meeting.views.interim_request_details' number=meeting.number %}">IETF - {{ meeting.number }}</a>
{% endif %}
</td>
<td>{% if meeting.can_approve %}<span class="label label-success">can approve</span>{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<h3>No pending interim meetings</h3>
{% endif %}
{% endblock %}
{% block js %}
<script src="{% static 'select2/select2.min.js' %}"></script>
<script src="{% static 'ietf/js/meeting-interim-request.js' %}"></script>
{% endblock %}

View file

@ -0,0 +1,151 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load staticfiles bootstrap3 widget_tweaks ietf_filters %}
{% block title %}Interim Request{% 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>Interim Meeting Request</h1>
<form id="interim-request-form" role="form" method="post" class="form-horizontal">
{% csrf_token %}
{% if form.non_field_errors %}
<div class="form-group alert alert-danger">
{{ form.non_field_errors }}
</div>
{% endif %}
{% bootstrap_field form.group layout='horizontal' %}
<div class="form-group form-inline">
<div class="col-md-offset-2">
<div class="col-md-2">
<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>
<label class="radio-inline">
<input type="radio" value="single" checked="checked" name="meeting_type">Single
</label>
<label class="radio-inline">
<input type="radio" value="multi-day" name="meeting_type">Multi-Day
</label>
<label class="radio-inline">
<input type="radio" value="series" name="meeting_type">Series
</label>
</div> <!-- col-md-offset-2 -->
</div> <!-- form-group form-inline -->
<div class="form-group meeting-type-help mth-multi">
<div class="col-md-offset-2">
<div class="col-md-10">
<p class="help-block">Use Multi-Day to request one meeting with sessions over multiple days.</p>
</div>
</div>
</div>
<div class="form-group meeting-type-help mth-series">
<div class="col-md-offset-2">
<div class="col-md-10">
<p class="help-block">Use Series to request a number of separate meetings. In Person series is not supported.</p>
</div>
</div>
</div>
<div class="form-group">
<label for="id_session_set-{{ forloop.counter0 }}-city" class="col-md-2 control-label">Location</label>
<div class="col-md-10 form-inline">
{% render_field form.city class="form-control location" placeholder="City" %}
{% render_field form.country class="form-control location" style="width: 30%" %}
<div id="timezone-field">
{% render_field form.time_zone class="form-control" %}
<span class="help-block">Local Timezone</span></div>
</div>
</div>
{{ formset.management_form }}
{% for form in formset %}
<div class="fieldset{% if forloop.last %} template{% endif %}" >
<div class="form-group {% if form.date.errors %}alert alert-danger{% endif %}">
<label for="id_session_set-{{ forloop.counter0 }}-date" class="col-md-2 control-label required">Date</label>
<div class="col-md-2">{% render_field form.date class="form-control" %}</div>
{% if form.date.errors %}<span class="help-inline">{{ form.date.errors }}</span>{% endif %}
</div>
<div class="form-group {% if form.time.errors or form.requested_duration.errors %}alert alert-danger{% endif %}">
<label for="id_session_set-{{ forloop.counter0 }}-time" class="col-md-2 control-label required">Start Time</label>
<div class="col-md-3 form-inline">
{% render_field form.time class="form-control time-field" placeholder="HH:MM" %}
<div id="id_session_set-{{ forloop.counter0 }}-time_utc" class="utc-time"></div>
<span class="help-block">Local Time</span>
<span class="help-block">Local Time</span>
{% if form.time.errors %}<span class="help-inline">{{ form.time.errors }}</span>{% endif %}
</div>
<label for="id_session_set-{{ forloop.counter0 }}-requested_duration" class="col-md-1 control-label required">Duration</label>
<div class="col-md-1">{% render_field form.requested_duration class="form-control time-field" placeholder="HH:MM" %}{% if form.requested_duration.errors %}<span class="help-inline">{{ form.requested_duration.errors }}</span>{% endif %}</div>
<label for="id_session_set-{{ forloop.counter0 }}-end_time" class="col-md-2 control-label">End Time</label>
<div class="col-md-3 form-inline">
{% render_field form.end_time class="form-control time-field computed" placeholder="HH:MM" disabled="disabled" %}
<div id="id_session_set-{{ forloop.counter0 }}-end_time_utc" class="utc-time"></div>
</div>
</div>
<div class="form-group{% if form.remote_instructions.errors %} alert alert-danger{% endif %}">
<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 (or other) URL or descriptive information (see below)" %}<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="http://ietf.org/private/webex-request.html">here</a> for more on remote participation support.</p></div>
{% if form.remote_instructions.errors %}<span class="help-inline">{{ form.remote_instructions.errors }}</span>{% endif %}
</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>
<div class="form-group">
<label for="id_session_set-{{ forloop.counter0 }}-agenda_note" class="col-md-2 control-label">Additional Information</label>
<div class="col-md-10">{% render_field form.agenda_note class="form-control" %}</div>
</div>
<button name="id_session_set-{{ forloop.counter0 }}-delete-button" type="button" class="btn btn-default hidden btn-delete">Delete</button>
</div> <!-- fieldset -->
{% endfor %}
<div class="form-group">
<div class="col-md-10">
<button id="add_session" type="button" class="btn btn-default"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span>Add Session</button>
</div>
</div>
<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

@ -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.interim_request_details' number=meeting.number %}">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

@ -0,0 +1,77 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load staticfiles bootstrap3 widget_tweaks ietf_filters %}
{% block title %}Interim Request Details{% endblock %}
{% block pagehead %}
<link rel="stylesheet" href="{% static 'select2/select2.css' %}">
<link rel="stylesheet" href="{% static 'select2-bootstrap-css/select2-bootstrap.min.css' %}">
{% endblock %}
{% block content %}
{% origin %}
<h1>Interim Meeting Request Details</h1>
<dl class="dl-horizontal">
<dt>Group</dt>
<dd>{{ sessions.0.group.acronym }}
<dt>Requested By</dt>
<dd>{{ sessions.0.requested_by }}
<dt>Status</dt>
<dd>{{ sessions.0.status }}</dd>
<dt>City</dt>
<dd>{{ meeting.city }}</dd>
<dt>Country</dt>
<dd>{{ meeting.country }}</dd>
<dt>Timezone</dt>
<dd>{{ meeting.time_zone }}</dd>
{% for session in sessions %}
<br>
<dt>Date</dt>
<dd>{{ session.official_timeslotassignment.timeslot.time|date:"Y-m-d" }}
<dt>Start Time</dt>
<dd>{{ session.official_timeslotassignment.timeslot.time|date:"H:i" }}
<dt>Duration</dt>
<dd>{{ session.requested_duration|format_timedelta }}
<dt>Remote Instructions</dt>
<dd>{{ session.remote_instructions }}
<dt>Additional Info</dt>
<dd>{{ session.agenda_note }}</dd>
{% endfor %}
</dl>
<form method="post">
{% csrf_token %}
{% if can_edit %}
<a class="btn btn-default" href="{% url 'ietf.meeting.views.interim_request_edit' number=meeting.number %}">Edit</a>
{% endif %}
{% if can_approve and sessions.0.status.slug == 'apprw' %}
<input class="btn btn-default" type="submit" value="Approve" name='approve' />
<input class="btn btn-default" type="submit" value="Disapprove" name='disapprove' />
{% endif %}
{% if user|has_role:"Secretariat" and sessions.0.status.slug == 'scheda' %}
<a class="btn btn-default" href="{% url 'ietf.meeting.views.interim_send_announcement' number=meeting.number %}">Announce</a>
{% endif %}
{% if can_edit %}
{% if sessions.0.status.slug == 'apprw' or sessions.0.status.slug == 'scheda' or sessions.0.status.slug == 'sched' %}
<a class="btn btn-default" href="{% url 'ietf.meeting.views.interim_request_cancel' number=meeting.number %}">Cancel Meeting</a>
{% endif %}
{% endif %}
{% if sessions.0.status.slug == "apprw" %}
<a class="btn btn-default" href="{% url 'ietf.meeting.views.interim_pending' %}">Back</a>
{% elif sessions.0.status.slug == "scheda" %}
<a class="btn btn-default" href="{% url 'ietf.meeting.views.interim_announce' %}">Back</a>
{% elif sessions.0.status.slug == "sched" %}
<a class="btn btn-default" href="{% url 'ietf.meeting.views.session_details' num=meeting.number acronym=meeting.session_set.first.group.acronym %}">Back</a>
{% else %}
<a class="btn btn-default" href="{% url 'ietf.meeting.views.upcoming' %}">Back</a>
{% endif %}
</form>
{% endblock %}
{% block js %}
<script src="{% static 'select2/select2.min.js' %}"></script>
<script src="{% static 'ietf/js/meeting-interim-request.js' %}"></script>
{% endblock %}

View file

@ -0,0 +1,117 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load staticfiles bootstrap3 widget_tweaks %}
{% block title %}Edit Interim Request{% 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>Edit Interim Meeting Request</h1>
<form id="interim-request-form" role="form" method="post" class="form-horizontal">
{% csrf_token %}
{% bootstrap_field form.group layout='horizontal' %}
<input type="hidden" name="group" value="{{ form.group.value }}">
<div class="form-group form-inline">
<div class="col-md-offset-2">
<div class="col-md-2">
<label class="checkbox-inline">{% render_field form.in_person %}<strong>In Person</strong></label>
</div>
<div class="col-md-2">
<label class="checkbox-inline">{% render_field form.approved %}<strong>Preapproved by AD</strong></label>
</div>
</div> <!-- col-md-offset-2 -->
</div> <!-- form-group form-inline -->
<div class="form-group">
<label for="id_session_set-{{ forloop.counter0 }}-city" class="col-md-2 control-label">Location</label>
<div class="col-md-10 form-inline">
{% render_field form.city class="form-control location" placeholder="City" %}
{% render_field form.country class="form-control location" style="width: 30%" %}
<div id="timezone-field">
{% render_field form.time_zone class="form-control" %}
<span class="help-block">Local Timezone</span></div>
</div>
</div>
{{ formset.management_form }}
{% for form in formset %}
<div class="fieldset{% if forloop.last %} template{% endif %}" >
<input id="id_session_set-{{ forloop.counter0 }}-id" name="session_set-{{ forloop.counter0 }}-id" type="hidden" value="{{ form.instance.pk|default_if_none:"" }}">
<div class="form-group{% if form.date.errors %} alert alert-danger{% endif %}">
<label for="id_session_set-{{ forloop.counter0 }}-date" class="col-md-2 control-label required">Date</label>
<div class="col-md-2">{% render_field form.date class="form-control" %}</div>
{% if form.date.errors %}<span class="help-inline">{{ form.date.errors }}</span>{% endif %}
</div>
<div class="form-group {% if form.time.errors or form.requested_duration.errors %}alert alert-danger{% endif %}">
<label for="id_session_set-{{ forloop.counter0 }}-time" class="col-md-2 control-label required">Start Time</label>
<div class="col-md-3 form-inline">
{% render_field form.time class="form-control time-field" placeholder="HH:MM" %}
<div id="id_session_set-{{ forloop.counter0 }}-time_utc" class="utc-time"></div>
<span class="help-block">Local Time</span>
{% if form.time.errors %}<span class="help-inline">{{ form.time.errors }}</span>{% endif %}
</div>
<label for="id_session_set-{{ forloop.counter0 }}-requested_duration" class="col-md-1 control-label required">Duration</label>
<div class="col-md-1">{% render_field form.requested_duration class="form-control time-field" placeholder="HH:MM" %}{% if form.requested_duration.errors %}<span class="help-inline">{{ form.requested_duration.errors }}</span>{% endif %}</div>
<label for="id_session_set-{{ forloop.counter0 }}-end_time" class="col-md-2 control-label">End Time</label>
<div class="col-md-3 form-inline">
{% render_field form.end_time class="form-control time-field computed" placeholder="HH:MM" disabled="disabled" %}
<div id="id_session_set-{{ forloop.counter0 }}-end_time_utc" class="utc-time"></div>
</div>
</div>
<div class="form-group{% if form.remote_instructions.errors %} alert alert-danger{% endif %}">
<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="ie. Webex address" %}</div>
{% if form.remote_instructions.errors %}<span class="help-inline">{{ form.remote_instructions.errors }}</span>{% endif %}
</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>
<div class="form-group">
<label for="id_session_set-{{ forloop.counter0 }}-agenda_note" class="col-md-2 control-label">Additional Information</label>
<div class="col-md-10">{% render_field form.agenda_note class="form-control" %}</div>
</div>
<button name="id_session_set-{{ forloop.counter0 }}-delete-button" type="button" class="btn btn-default hidden btn-delete">Delete</button>
</div> <!-- fieldset -->
{% endfor %}
<div class="form-group">
<div class="col-md-10">
<button id="add_session" type="button" class="btn btn-default"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span>Add Session</button>
</div>
</div>
<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.interim_request_details' number=meeting.number %}">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

@ -0,0 +1,55 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load staticfiles bootstrap3 widget_tweaks %}
{% block title %}Announce Interim Meeting{% endblock %}
{% block pagehead %}
<link rel="stylesheet" href="{% static 'select2/select2.css' %}">
<link rel="stylesheet" href="{% static 'select2-bootstrap-css/select2-bootstrap.min.css' %}">
{% endblock %}
{% block content %}
{% origin %}
<h1>Announce Interim Meeting</h1>
<form method="post" role="form" class="form-horizontal">
{% csrf_token %}
<div class="fieldset" >
<div class="form-group">
<label for="id_to" class="col-md-2 control-label">To</label>
<div class="col-md-10">{% render_field form.to class="form-control" readonly="readonly" %}</div>
</div>
<div class="form-group">
<label for="id_cc" class="col-md-2 control-label">Cc</label>
<div class="col-md-10">{% render_field form.cc class="form-control" %}</div>
</div>
<div class="form-group">
<label for="id_from" class="col-md-2 control-label">From</label>
<div class="col-md-10">{% render_field form.frm class="form-control" readonly="readonly" %}</div>
</div>
<div class="form-group">
<label for="id_subject" class="col-md-2 control-label">Subject</label>
<div class="col-md-10">{% render_field form.subject class="form-control" readonly="readonly" %}</div>
</div>
<div class="form-group">
<label for="id_body" class="col-md-2 control-label">Body</label>
<div class="col-md-10">{% render_field form.body class="form-control" %}</div>
</div>
</div> <!-- fieldset -->
<input class="btn btn-default" type="submit" value="Send" name='send' />
<a class="btn btn-default pull-right" href="{% url 'ietf.meeting.views.interim_request_details' number=meeting.number %}">Back</a>
</form>
{% endblock %}
{% block js %}
<script src="{% static 'select2/select2.min.js' %}"></script>
<script src="{% static 'ietf/js/meeting-interim-request.js' %}"></script>
{% endblock %}

View file

@ -1,6 +1,6 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load origin ietf_filters %}
{% block title %}{{ meeting }} : {{ acronym }}{% endblock %}
@ -14,6 +14,9 @@
{% if can_manage_materials %}
{% if session.status.slug == 'sched' or session.status.slug == 'schedw' %}
<div class="buttonlist">
{% if meeting.type.slug == 'interim' and user|has_role:"Secretariat" %}
<a class="btn btn-default" href="{% url 'ietf.meeting.views.interim_request_details' number=meeting.number %}">Meeting Details</a>
{% endif %}
<a class="btn btn-default" href="{% url 'ietf.secr.proceedings.views.upload_unified' meeting_num=session.meeting.number acronym=session.group.acronym %}">
Upload/Edit materials
</a>

View file

@ -0,0 +1,148 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load ietf_filters staticfiles %}
{% block pagehead %}
<link rel="stylesheet" href="{% static "jquery.tablesorter/css/theme.bootstrap.min.css" %}">
{% endblock %}
{% block bodyAttrs %}data-spy="scroll" data-target="#affix"{% endblock %}
{% block title %}IETF Upcoming Meetings{% endblock %}
{% block content %}
{% origin %}
<div class="row">
<div class="col-md-10">
<h1>IETF Upcoming Meetings</h1>
<p>For more on regular IETF meetings see <a href="https://www.ietf.org/meeting/upcoming.html">here</a></p>
<div class="panel-group" id="accordion">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" data-parent="#accordion" href="#customize">
<span class="fa fa-caret-down"></span> Customize the meeting list...
</a>
</h4>
</div> <!-- panel-heading -->
<div id="customize" class="panel-collapse collapse">
<div class="panel-body">
<p>
You can customize the list to show only selected groups
by clicking on groups and areas in the table below.
To be able to return to the customized view later, bookmark the resulting URL.
</p>
{% if group_parents|length %}
<p>Groups displayed in <b><i>italics</i></b> are BOFs.</p>
<table class="table table-condensed">
<thead>
<tr>
{% for p in group_parents %}
<th style="width:{% widthratio 1 group_parents|length 100 %}%">
<button class="btn btn-default btn-block pickview {{p.acronym|lower}}">{{p.acronym|upper}}</button>
</th>
{% endfor %}
</tr>
</thead>
<tbody>
<tr>
{% for p in group_parents %}
<td class="view {{p.acronym|lower}}">
<div class="btn-group-vertical btn-block">
{% for group in p.group_list %}
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickview {{group.acronym}}">
{% if group.is_bof %}
<i>{{group.acronym}}</i>
{% else %}
{{group.acronym}}
{% endif %}
</button>
</div> <!-- button-group -->
{% endfor %}
</div> <!-- button-group-vertical -->
</td>
{% endfor %}
</tr>
</tbody>
</table>
{% else %}
<blockquote><i>No meetings have been scheduled yet.</i></blockquote>
{% endif %}
</div> <!-- panel-body -->
</div> <!-- panel-collapse -->
</div> <!-- panel -->
</div> <!-- panel-group -->
{% if menu_entries %}
<ul class="nav nav-tabs" role="tablist">
{% for name, url in menu_entries %}
<li {% if selected_menu_entry == name.lower %}class="active"{% endif %}>
<a href="{{ url }}">{{ name }}</a>
</li>
{% endfor %}
</ul>
{% endif %}
{% if menu_actions %}
<div class="buttonlist">
{% for name, url in menu_actions %}
<a class="btn btn-default" href="{{ url }}">{{ name }}</a>
{% endfor %}
</div>
{% endif %}
{% if meetings %}
<h3></h3>
<table class="table table-condensed table-striped tablesorter">
<thead>
<tr>
<th>Date</th>
<th>Group</th>
<th>Name</th>
</tr>
</thead>
<tbody>
{% for meeting in meetings %}
{% if meeting.type.slug == 'interim' %}
<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 %}
<td>{{ meeting.date }}</td>
{% if meeting.type.slug == 'interim' %}
<td>{{ meeting.session_set.all.0.group.acronym }}</td>
{% else %}
<td>ietf</td>
{% endif %}
<td>
{% if meeting.type.slug == "interim" %}
<a href="{% url 'ietf.meeting.views.session_details' num=meeting.number acronym=meeting.session_set.all.0.group.acronym %}">{{ meeting.number }}{% if meeting.session_set.all.0.status.slug == "canceled" %}&nbsp&nbsp<span class="label label-warning">CANCELLED</span>{% endif %}</a>
{% else %}
<a href="{% url 'ietf.meeting.views.agenda' num=meeting.number %}">IETF - {{ meeting.number }}</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<h3>No upcoming meetings</h3>
{% endif %}
</div>
</div>
{% endblock %}
{% block js %}
<script src="{% static "jquery.tablesorter/js/jquery.tablesorter.combined.min.js" %}"></script>
<script src="{% static 'ietf/js/toggle-visibility.js' %}"></script>
{% endblock %}

View file

@ -0,0 +1,21 @@
{% load humanize %}{% autoescape off %}{% load ietf_filters %}BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
PRODID:-//IETF//datatracker.ietf.org ical upcoming//EN
{{vtimezones}}{% 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.schedule.meeting.city %}LOCATION:{{item.schedule.meeting.city}},{{item.schedule.meeting.country}}
{% endif %}STATUS:{{item.session.ical_status}}
CLASS:PUBLIC
DTSTART{% if item.schedule.meeting.time_zone %};TZID="{{item.schedule.meeting.time_zone}}"{%endif%}:{{ item.timeslot.time|date:"Ymd" }}T{{item.timeslot.time|date:"Hi"}}00
DTEND{% if item.schedule.meeting.time_zone %};TZID="{{item.schedule.meeting.time_zone}}"{%endif%}:{{ item.timeslot.end_time|date:"Ymd" }}T{{item.timeslot.end_time|date:"Hi"}}00
DTSTAMP:{{ item.timeslot.modified|date:"Ymd" }}T{{ item.timeslot.modified|date:"His" }}Z
{% if item.session.agenda %}URL:{{item.session.agenda.get_absolute_url}}
DESCRIPTION:{{item.timeslot.name|ics_esc}}\n{% if item.session.agenda_note %}
Note: {{item.session.agenda_note|ics_esc}}\n{% endif %}{% for material in item.session.materials.all %}
\n{{material.type}}{% if material.type.name != "Agenda" %}
({{material.title|ics_esc}}){% endif %}:
{{material.get_absolute_url}}\n{% endfor %}
{% endif %}END:VEVENT
{% endfor %}END:VCALENDAR{% endautoescape %}

View file

@ -89,6 +89,13 @@ def make_immutable_base_data():
area = create_group(name="Far Future", acronym="farfut", type_id="area", parent=ietf)
create_person(area, "ad", name="Areað Irector", username="ad", email_address="aread@ietf.org")
# second area
opsarea = create_group(name="Operations", acronym="ops", type_id="area", parent=ietf)
create_person(opsarea, "ad")
sops = create_group(name="Server Operations", acronym="sops", type_id="wg", parent=opsarea)
create_person(sops, "chair", name="Sops Chairman", username="sopschairman")
create_person(sops, "secr", name="Sops Secretary", username="sopssecretary")
# create a bunch of ads for swarm tests
for i in range(1, 10):
u = User.objects.create(username="ad%s" % i)
@ -116,6 +123,7 @@ def make_immutable_base_data():
def make_test_data():
area = Group.objects.get(acronym="farfut")
ad = Person.objects.get(user__username="ad")
irtf = Group.objects.get(acronym='irtf')
# mars WG
group = Group.objects.create(
@ -166,6 +174,30 @@ def make_test_data():
group.charter = charter
group.save()
# irg RG
irg_rg = Group.objects.create(
name="Internet Research Group",
acronym="irg",
state_id="active",
type_id="rg",
parent=irtf,
list_email="irg-rg@ietf.org",
)
#charter = Document.objects.create(
# name="charter-ietf-" + group.acronym,
# type_id="charter",
# title=group.name,
# group=group,
# rev="00",
# )
#charter.set_state(State.objects.get(used=True, slug="infrev", type="charter"))
#DocAlias.objects.create(
# name=charter.name,
# document=charter
# )
#group.charter = charter
#group.save()
# plain IETF'er
u = User.objects.create(username="plain")
u.set_password("plain+password")
@ -187,6 +219,8 @@ def make_test_data():
ames_wg.role_set.get_or_create(name_id='ad',person=ad,email=ad.role_email('ad'))
ames_wg.save()
create_person(irg_rg, "chair", name="Irg Chair Man", username="irgchairman")
# old draft
old_draft = Document.objects.create(
name="draft-foo-mars-test",