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:
commit
d298785fda
23
ietf/bin/interim_minutes_reminder
Executable file
23
ietf/bin/interim_minutes_reminder
Executable file
|
@ -0,0 +1,23 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# -*- Python -*-
|
||||
#
|
||||
'''
|
||||
This script calls ietf.meeting.helpers.check_interim_minutes() which sends
|
||||
a reminder email for interim meetings that occurred 10 days ago but still
|
||||
don't have minutes.
|
||||
'''
|
||||
|
||||
# Set PYTHONPATH and load environment variables for standalone script -----------------
|
||||
import os, sys
|
||||
basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
|
||||
sys.path = [ basedir ] + sys.path
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ietf.settings")
|
||||
|
||||
import django
|
||||
django.setup()
|
||||
# -------------------------------------------------------------------------------------
|
||||
|
||||
from ietf.meeting.helpers import check_interim_minutes
|
||||
|
||||
check_interim_minutes()
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
54
ietf/mailtrigger/migrations/0004_auto_20160516_1659.py
Normal file
54
ietf/mailtrigger/migrations/0004_auto_20160516_1659.py
Normal file
|
@ -0,0 +1,54 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def make_recipients(apps):
|
||||
Recipient = apps.get_model('mailtrigger', 'Recipient')
|
||||
|
||||
rc = Recipient.objects.create
|
||||
|
||||
rc(slug='group_secretaries',
|
||||
desc="The group's secretaries",
|
||||
template=None)
|
||||
|
||||
|
||||
def make_mailtriggers(apps):
|
||||
Recipient = apps.get_model('mailtrigger','Recipient')
|
||||
MailTrigger = apps.get_model('mailtrigger','MailTrigger')
|
||||
|
||||
def mt_factory(slug,desc,to_slugs,cc_slugs=[]):
|
||||
|
||||
# Try to protect ourselves from typos
|
||||
all_slugs = to_slugs[:]
|
||||
all_slugs.extend(cc_slugs)
|
||||
for recipient_slug in all_slugs:
|
||||
try:
|
||||
Recipient.objects.get(slug=recipient_slug)
|
||||
except Recipient.DoesNotExist:
|
||||
print "****Some rule tried to use",recipient_slug
|
||||
raise
|
||||
|
||||
m = MailTrigger.objects.create(slug=slug, desc=desc)
|
||||
m.to = Recipient.objects.filter(slug__in=to_slugs)
|
||||
m.cc = Recipient.objects.filter(slug__in=cc_slugs)
|
||||
|
||||
mt_factory(slug='session_minutes_reminder',
|
||||
desc="Recipients when a group is sent a reminder "
|
||||
"to submit minutes for a session",
|
||||
to_slugs=['group_chairs','group_secretaries'],
|
||||
cc_slugs=['group_responsible_directors']
|
||||
)
|
||||
|
||||
def forward(apps, schema_editor):
|
||||
make_recipients(apps)
|
||||
make_mailtriggers(apps)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('mailtrigger', '0003_merge_request_trigger'),
|
||||
]
|
||||
|
||||
operations = [migrations.RunPython(forward)]
|
|
@ -157,6 +157,14 @@ class Recipient(models.Model):
|
|||
addrs.extend(Recipient.objects.get(slug='stream_managers').gather(**{'streams':['irtf']}))
|
||||
return addrs
|
||||
|
||||
def gather_group_secretaries(self, **kwargs):
|
||||
addrs = []
|
||||
if 'group' in kwargs:
|
||||
group = kwargs['group']
|
||||
if not group.acronym=='none':
|
||||
addrs.extend(group.role_set.filter(name='secr').values_list('email__address',flat=True))
|
||||
return addrs
|
||||
|
||||
def gather_doc_group_responsible_directors(self, **kwargs):
|
||||
addrs = []
|
||||
if 'doc' in kwargs:
|
||||
|
|
330
ietf/meeting/forms.py
Normal file
330
ietf/meeting/forms.py
Normal 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
|
|
@ -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()
|
||||
|
|
20
ietf/meeting/migrations/0023_session_remote_instructions.py
Normal file
20
ietf/meeting/migrations/0023_session_remote_instructions.py
Normal 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,
|
||||
),
|
||||
]
|
81
ietf/meeting/migrations/0024_migrate_interim_meetings.py
Normal file
81
ietf/meeting/migrations/0024_migrate_interim_meetings.py
Normal 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),
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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),
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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": [
|
||||
|
|
19
ietf/name/migrations/0011_add_session_status.py
Normal file
19
ietf/name/migrations/0011_add_session_status.py
Normal 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),
|
||||
]
|
|
@ -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):
|
||||
'''
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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('/')
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/* ========================================================================== */
|
||||
|
|
219
ietf/static/ietf/js/meeting-interim-request.js
Normal file
219
ietf/static/ietf/js/meeting-interim-request.js
Normal 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);
|
||||
});
|
74
ietf/static/ietf/js/toggle-visibility.js
Normal file
74
ietf/static/ietf/js/toggle-visibility.js
Normal 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();
|
||||
});
|
||||
|
|
@ -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 %}
|
||||
|
|
65
ietf/templates/meeting/interim_announce.html
Normal file
65
ietf/templates/meeting/interim_announce.html
Normal 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 %}
|
19
ietf/templates/meeting/interim_announcement.txt
Normal file
19
ietf/templates/meeting/interim_announcement.txt
Normal 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 }}
|
9
ietf/templates/meeting/interim_approval_request.txt
Normal file
9
ietf/templates/meeting/interim_approval_request.txt
Normal 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 %}
|
8
ietf/templates/meeting/interim_cancellation_notice.txt
Normal file
8
ietf/templates/meeting/interim_cancellation_notice.txt
Normal 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 }}
|
||||
|
||||
|
20
ietf/templates/meeting/interim_info.txt
Normal file
20
ietf/templates/meeting/interim_info.txt
Normal 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 %}
|
||||
---------------------------------------------------------
|
16
ietf/templates/meeting/interim_minutes_reminder.txt
Normal file
16
ietf/templates/meeting/interim_minutes_reminder.txt
Normal file
|
@ -0,0 +1,16 @@
|
|||
{% load ams_filters %}
|
||||
|
||||
Please note that we have not yet received minutes from the
|
||||
{{ group.name }} ({{ group.acronym }}) interim meeting held
|
||||
on {{ meeting.date|date:"Y-m-d"}}. As per the IESG Guidence on Interim Meetings,
|
||||
Conference Calls and Jabber Sessions [1], detailed minutes must
|
||||
be provided within 10 days of the event.
|
||||
|
||||
At your earliest convenience, please upload meeting minutes, as
|
||||
well as any presentations from your sessions by using the Meeting
|
||||
Materials Manager found here:
|
||||
https://datatracker.ietf.org/secr/proceedings/.
|
||||
Alternatively, you are welcome to send them to proceedings@ietf.org
|
||||
for manual posting.
|
||||
|
||||
[1] http://www.ietf.org/iesg/statement/interim-meetings.html
|
71
ietf/templates/meeting/interim_pending.html
Normal file
71
ietf/templates/meeting/interim_pending.html
Normal 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 %}
|
151
ietf/templates/meeting/interim_request.html
Normal file
151
ietf/templates/meeting/interim_request.html
Normal 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 %}
|
38
ietf/templates/meeting/interim_request_cancel.html
Normal file
38
ietf/templates/meeting/interim_request_cancel.html
Normal file
|
@ -0,0 +1,38 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
||||
{% load origin %}
|
||||
{% load staticfiles bootstrap3 widget_tweaks %}
|
||||
|
||||
{% block title %}Cancel Interim Meeting {% if meeting.session_set.first.status.slug != "sched" %}Request{% endif %}{% endblock %}
|
||||
|
||||
{% block pagehead %}
|
||||
<link rel="stylesheet" href="{% static 'select2/select2.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'select2-bootstrap-css/select2-bootstrap.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'bootstrap-datepicker/css/bootstrap-datepicker3.min.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
<h1>Cancel Interim Meeting {% if meeting.session_set.first.status.slug != "sched" %}Request{% endif %}</h1>
|
||||
|
||||
<form id="interim-request-cancel-form" role="form" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
|
||||
{% bootstrap_form form layout='horizontal' %}
|
||||
|
||||
<div class="form-group"
|
||||
{% buttons %}
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
<a class="btn btn-default pull-right" href="{% url 'ietf.meeting.views.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 %}
|
77
ietf/templates/meeting/interim_request_details.html
Normal file
77
ietf/templates/meeting/interim_request_details.html
Normal 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 %}
|
117
ietf/templates/meeting/interim_request_edit.html
Normal file
117
ietf/templates/meeting/interim_request_edit.html
Normal 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 %}
|
55
ietf/templates/meeting/interim_send_announcement.html
Normal file
55
ietf/templates/meeting/interim_send_announcement.html
Normal 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 %}
|
|
@ -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>
|
||||
|
|
148
ietf/templates/meeting/upcoming.html
Normal file
148
ietf/templates/meeting/upcoming.html
Normal 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" %}  <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 %}
|
21
ietf/templates/meeting/upcoming.ics
Normal file
21
ietf/templates/meeting/upcoming.ics
Normal 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 %}
|
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue