Merged in branch/amsl/trunkmerge@5449 from rcross@amsl.com, with some tweaks. This provides the secretariat apps.

- Legacy-Id: 5482
This commit is contained in:
Henrik Levkowetz 2013-03-05 22:43:31 +00:00
parent bf7b128ef0
commit 7e67b40a87
360 changed files with 314803 additions and 3 deletions

0
form_utils/__init__.py Normal file
View file

12
form_utils/admin.py Normal file
View file

@ -0,0 +1,12 @@
from django.contrib import admin
from django import forms
from form_utils.fields import ClearableFileField
class ClearableFileFieldsAdmin(admin.ModelAdmin):
def formfield_for_dbfield(self, db_field, **kwargs):
field = super(ClearableFileFieldsAdmin, self).formfield_for_dbfield(
db_field, **kwargs)
if isinstance(field, forms.FileField):
field = ClearableFileField(field)
return field

51
form_utils/fields.py Normal file
View file

@ -0,0 +1,51 @@
from django import forms
from form_utils.widgets import ClearableFileInput
class FakeEmptyFieldFile(object):
"""
A fake FieldFile that will convice a FileField model field to
actually replace an existing file name with an empty string.
FileField.save_form_data only overwrites its instance data if the
incoming form data evaluates to True in a boolean context (because
an empty file input is assumed to mean "no change"). We want to be
able to clear it without requiring the use of a model FileField
subclass (keeping things at the form level only). In order to do
this we need our form field to return a value that evaluates to
True in a boolean context, but to the empty string when coerced to
unicode. This object fulfills that requirement.
It also needs the _committed attribute to satisfy the test in
FileField.pre_save.
This is, of course, hacky and fragile, and depends on internal
knowledge of the FileField and FieldFile classes. But it will
serve until Django FileFields acquire a native ability to be
cleared (ticket 7048).
"""
def __unicode__(self):
return u''
_committed = True
class ClearableFileField(forms.MultiValueField):
default_file_field_class = forms.FileField
widget = ClearableFileInput
def __init__(self, file_field=None, template=None, *args, **kwargs):
file_field = file_field or self.default_file_field_class(*args,
**kwargs)
fields = (file_field, forms.BooleanField(required=False))
kwargs['required'] = file_field.required
kwargs['widget'] = self.widget(file_widget=file_field.widget,
template=template)
super(ClearableFileField, self).__init__(fields, *args, **kwargs)
def compress(self, data_list):
if data_list[1] and not data_list[0]:
return FakeEmptyFieldFile()
return data_list[0]
class ClearableImageField(ClearableFileField):
default_file_field_class = forms.ImageField

278
form_utils/forms.py Normal file
View file

@ -0,0 +1,278 @@
"""
forms for django-form-utils
Time-stamp: <2010-04-28 02:57:16 carljm forms.py>
"""
from copy import deepcopy
from django import forms
from django.forms.util import flatatt, ErrorDict
from django.utils.safestring import mark_safe
class Fieldset(object):
"""
An iterable Fieldset with a legend and a set of BoundFields.
"""
def __init__(self, form, name, boundfields, legend='', classes='', description=''):
self.form = form
self.boundfields = boundfields
if legend is None: legend = name
self.legend = legend and mark_safe(legend)
self.classes = classes
self.description = mark_safe(description)
self.name = name
def _errors(self):
return ErrorDict(((k, v) for (k, v) in self.form.errors.iteritems()
if k in [f.name for f in self.boundfields]))
errors = property(_errors)
def __iter__(self):
for bf in self.boundfields:
yield _mark_row_attrs(bf, self.form)
def __repr__(self):
return "%s('%s', %s, legend='%s', classes='%s', description='%s')" % (
self.__class__.__name__, self.name,
[f.name for f in self.boundfields], self.legend, self.classes, self.description)
class FieldsetCollection(object):
def __init__(self, form, fieldsets):
self.form = form
self.fieldsets = fieldsets
self._cached_fieldsets = []
def __len__(self):
return len(self.fieldsets) or 1
def __iter__(self):
if not self._cached_fieldsets:
self._gather_fieldsets()
for field in self._cached_fieldsets:
yield field
def __getitem__(self, key):
if not self._cached_fieldsets:
self._gather_fieldsets()
for field in self._cached_fieldsets:
if field.name == key:
return field
raise KeyError
def _gather_fieldsets(self):
if not self.fieldsets:
self.fieldsets = (('main', {'fields': self.form.fields.keys(),
'legend': ''}),)
for name, options in self.fieldsets:
try:
field_names = [n for n in options['fields']
if n in self.form.fields]
except KeyError:
raise ValueError("Fieldset definition must include 'fields' option." )
boundfields = [forms.forms.BoundField(self.form, self.form.fields[n], n)
for n in field_names]
self._cached_fieldsets.append(Fieldset(self.form, name,
boundfields, options.get('legend', None),
' '.join(options.get('classes', ())),
options.get('description', '')))
def _get_meta_attr(attrs, attr, default):
try:
ret = getattr(attrs['Meta'], attr)
except (KeyError, AttributeError):
ret = default
return ret
def _set_meta_attr(attrs, attr, value):
try:
setattr(attrs['Meta'], attr, value)
return True
except KeyError:
return False
def get_fieldsets(bases, attrs):
"""
Get the fieldsets definition from the inner Meta class.
"""
fieldsets = _get_meta_attr(attrs, 'fieldsets', None)
if fieldsets is None:
#grab the fieldsets from the first base class that has them
for base in bases:
fieldsets = getattr(base, 'base_fieldsets', None)
if fieldsets is not None:
break
fieldsets = fieldsets or []
return fieldsets
def get_fields_from_fieldsets(fieldsets):
"""
Get a list of all fields included in a fieldsets definition.
"""
fields = []
try:
for name, options in fieldsets:
fields.extend(options['fields'])
except (TypeError, KeyError):
raise ValueError('"fieldsets" must be an iterable of two-tuples, '
'and the second tuple must be a dictionary '
'with a "fields" key')
return fields
def get_row_attrs(bases, attrs):
"""
Get the row_attrs definition from the inner Meta class.
"""
return _get_meta_attr(attrs, 'row_attrs', {})
def _mark_row_attrs(bf, form):
row_attrs = deepcopy(form._row_attrs.get(bf.name, {}))
if bf.field.required:
req_class = 'required'
else:
req_class = 'optional'
if 'class' in row_attrs:
row_attrs['class'] = row_attrs['class'] + ' ' + req_class
else:
row_attrs['class'] = req_class
bf.row_attrs = mark_safe(flatatt(row_attrs))
return bf
class BetterFormBaseMetaclass(type):
def __new__(cls, name, bases, attrs):
attrs['base_fieldsets'] = get_fieldsets(bases, attrs)
fields = get_fields_from_fieldsets(attrs['base_fieldsets'])
if (_get_meta_attr(attrs, 'fields', None) is None and
_get_meta_attr(attrs, 'exclude', None) is None):
_set_meta_attr(attrs, 'fields', fields)
attrs['base_row_attrs'] = get_row_attrs(bases, attrs)
new_class = super(BetterFormBaseMetaclass,
cls).__new__(cls, name, bases, attrs)
return new_class
class BetterFormMetaclass(BetterFormBaseMetaclass,
forms.forms.DeclarativeFieldsMetaclass):
pass
class BetterModelFormMetaclass(BetterFormBaseMetaclass,
forms.models.ModelFormMetaclass):
pass
class BetterBaseForm(object):
"""
``BetterForm`` and ``BetterModelForm`` are subclasses of Form
and ModelForm that allow for declarative definition of fieldsets
and row_attrs in an inner Meta class.
The row_attrs declaration is a dictionary mapping field names to
dictionaries of attribute/value pairs. The attribute/value
dictionaries will be flattened into HTML-style attribute/values
(i.e. {'style': 'display: none'} will become ``style="display:
none"``), and will be available as the ``row_attrs`` attribute of
the ``BoundField``. Also, a CSS class of "required" or "optional"
will automatically be added to the row_attrs of each
``BoundField``, depending on whether the field is required.
There is no automatic inheritance of ``row_attrs``.
The fieldsets declaration is a list of two-tuples very similar to
the ``fieldsets`` option on a ModelAdmin class in
``django.contrib.admin``.
The first item in each two-tuple is a name for the fieldset, and
the second is a dictionary of fieldset options.
Valid fieldset options in the dictionary include:
``fields`` (required): A tuple of field names to display in this
fieldset.
``classes``: A list of extra CSS classes to apply to the fieldset.
``legend``: This value, if present, will be the contents of a ``legend``
tag to open the fieldset.
``description``: A string of optional extra text to be displayed
under the ``legend`` of the fieldset.
When iterated over, the ``fieldsets`` attribute of a
``BetterForm`` (or ``BetterModelForm``) yields ``Fieldset``s.
Each ``Fieldset`` has a ``name`` attribute, a ``legend``
attribute, , a ``classes`` attribute (the ``classes`` tuple
collapsed into a space-separated string), and a description
attribute, and when iterated over yields its ``BoundField``s.
Subclasses of a ``BetterForm`` will inherit their parent's
fieldsets unless they define their own.
A ``BetterForm`` or ``BetterModelForm`` can still be iterated over
directly to yield all of its ``BoundField``s, regardless of
fieldsets.
"""
def __init__(self, *args, **kwargs):
self._fieldsets = deepcopy(self.base_fieldsets)
self._row_attrs = deepcopy(self.base_row_attrs)
self._fieldset_collection = None
super(BetterBaseForm, self).__init__(*args, **kwargs)
@property
def fieldsets(self):
if not self._fieldset_collection:
self._fieldset_collection = FieldsetCollection(self,
self._fieldsets)
return self._fieldset_collection
def __iter__(self):
for bf in super(BetterBaseForm, self).__iter__():
yield _mark_row_attrs(bf, self)
def __getitem__(self, name):
bf = super(BetterBaseForm, self).__getitem__(name)
return _mark_row_attrs(bf, self)
class BetterForm(BetterBaseForm, forms.Form):
__metaclass__ = BetterFormMetaclass
__doc__ = BetterBaseForm.__doc__
class BetterModelForm(BetterBaseForm, forms.ModelForm):
__metaclass__ = BetterModelFormMetaclass
__doc__ = BetterBaseForm.__doc__
class BasePreviewForm (object):
"""
Mixin to add preview functionality to a form. If the form is submitted with
the following k/v pair in its ``data`` dictionary:
'submit': 'preview' (value string is case insensitive)
Then ``PreviewForm.preview`` will be marked ``True`` and the form will
be marked invalid (though this invalidation will not put an error in
its ``errors`` dictionary).
"""
def __init__(self, *args, **kwargs):
super(BasePreviewForm, self).__init__(*args, **kwargs)
self.preview = self.check_preview(kwargs.get('data', None))
def check_preview(self, data):
if data and data.get('submit', '').lower() == u'preview':
return True
return False
def is_valid(self, *args, **kwargs):
if self.preview:
return False
return super(BasePreviewForm, self).is_valid()
class PreviewModelForm(BasePreviewForm, BetterModelForm):
pass
class PreviewForm(BasePreviewForm, BetterForm):
pass

View file

@ -0,0 +1,3 @@
$(document).ready(function() {
$('textarea.autoresize').autogrow();
});

View file

@ -0,0 +1,132 @@
/*
* Auto Expanding Text Area (1.2.2)
* by Chrys Bader (www.chrysbader.com)
* chrysb@gmail.com
*
* Special thanks to:
* Jake Chapa - jake@hybridstudio.com
* John Resig - jeresig@gmail.com
*
* Copyright (c) 2008 Chrys Bader (www.chrysbader.com)
* Licensed under the GPL (GPL-LICENSE.txt) license.
*
*
* NOTE: This script requires jQuery to work. Download jQuery at www.jquery.com
*
*/
(function(jQuery) {
var self = null;
jQuery.fn.autogrow = function(o)
{
return this.each(function() {
new jQuery.autogrow(this, o);
});
};
/**
* The autogrow object.
*
* @constructor
* @name jQuery.autogrow
* @param Object e The textarea to create the autogrow for.
* @param Hash o A set of key/value pairs to set as configuration properties.
* @cat Plugins/autogrow
*/
jQuery.autogrow = function (e, o)
{
this.options = o || {};
this.dummy = null;
this.interval = null;
this.line_height = this.options.lineHeight || parseInt(jQuery(e).css('line-height'));
this.min_height = this.options.minHeight || parseInt(jQuery(e).css('min-height'));
this.max_height = this.options.maxHeight || parseInt(jQuery(e).css('max-height'));;
this.textarea = jQuery(e);
if(this.line_height == NaN)
this.line_height = 0;
// Only one textarea activated at a time, the one being used
this.init();
};
jQuery.autogrow.fn = jQuery.autogrow.prototype = {
autogrow: '1.2.2'
};
jQuery.autogrow.fn.extend = jQuery.autogrow.extend = jQuery.extend;
jQuery.autogrow.fn.extend({
init: function() {
var self = this;
this.textarea.css({overflow: 'hidden', display: 'block'});
this.textarea.bind('focus', function() { self.startExpand() } ).bind('blur', function() { self.stopExpand() });
this.checkExpand();
},
startExpand: function() {
var self = this;
this.interval = window.setInterval(function() {self.checkExpand()}, 400);
},
stopExpand: function() {
clearInterval(this.interval);
},
checkExpand: function() {
if (this.dummy == null)
{
this.dummy = jQuery('<div></div>');
this.dummy.css({
'font-size' : this.textarea.css('font-size'),
'font-family': this.textarea.css('font-family'),
'width' : this.textarea.css('width'),
'padding' : this.textarea.css('padding'),
'line-height': this.line_height + 'px',
'overflow-x' : 'hidden',
'position' : 'absolute',
'top' : 0,
'left' : -9999
}).appendTo('body');
}
// Strip HTML tags
var html = this.textarea.val().replace(/(<|>)/g, '');
// IE is different, as per usual
if ($.browser.msie)
{
html = html.replace(/\n/g, '<BR>new');
}
else
{
html = html.replace(/\n/g, '<br>new');
}
if (this.dummy.html() != html)
{
this.dummy.html(html);
if (this.max_height > 0 && (this.dummy.height() + this.line_height > this.max_height))
{
this.textarea.css('overflow-y', 'auto');
}
else
{
this.textarea.css('overflow-y', 'hidden');
if (this.textarea.height() < this.dummy.height() + this.line_height || (this.dummy.height() < this.textarea.height()))
{
this.textarea.animate({height: (this.dummy.height() + this.line_height) + 'px'}, 100);
}
}
}
}
});
})(jQuery);

0
form_utils/models.py Normal file
View file

12
form_utils/settings.py Normal file
View file

@ -0,0 +1,12 @@
import posixpath
from django.conf import settings
JQUERY_URL = getattr(
settings, 'JQUERY_URL',
'http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js')
if not ((':' in JQUERY_URL) or (JQUERY_URL.startswith('/'))):
JQUERY_URL = posixpath.join(settings.MEDIA_URL, JQUERY_URL)
FORM_UTILS_MEDIA_URL = getattr(settings, 'FORM_UTILS_MEDIA_URL', settings.MEDIA_URL)

View file

@ -0,0 +1,16 @@
{% extends "form_utils/form.html" %}
{% block fields %}
{% for fieldset in form.fieldsets %}
<fieldset class="{{ fieldset.classes }}">
{% if fieldset.legend %}
<legend>{{ fieldset.legend }}</legend>
{% endif %}
<ul>
{% with fieldset as fields %}
{% include "form_utils/fields_as_lis.html" %}
{% endwith %}
</ul>
</fieldset>
{% endfor %}
{% endblock %}

View file

@ -0,0 +1,11 @@
{% for field in fields %}
{% if field.is_hidden %}
{{ field }}
{% else %}
<li{{ field.row_attrs }}>
{{ field.errors }}
{{ field.label_tag }}
{{ field }}
</li>
{% endif %}
{% endfor %}

View file

@ -0,0 +1,13 @@
{% block errors %}
{% if form.non_field_errors %}{{ form.non_field_errors }}{% endif %}
{% endblock %}
{% block fields %}
<fieldset class="fieldset_main">
<ul>
{% with form as fields %}
{% include "form_utils/fields_as_lis.html" %}
{% endwith %}
</ul>
</fieldset>
{% endblock %}

View file

@ -0,0 +1,3 @@
/__init__.py/1.1/Fri Jan 28 21:08:54 2011//
/form_utils_tags.py/1.1/Fri Jan 28 21:08:54 2011//
D

View file

@ -0,0 +1 @@
ietfsec/form_utils/templatetags

View file

@ -0,0 +1 @@
/a/cvs

View file

@ -0,0 +1,6 @@
"""
__init__.py for django-form-utils - templatetags
Time-stamp: <2008-10-13 12:14:37 carljm __init__.py>
"""

View file

@ -0,0 +1,42 @@
"""
templatetags for django-form-utils
Time-stamp: <2009-03-26 12:32:08 carljm form_utils_tags.py>
"""
from django import template
from form_utils.forms import BetterForm, BetterModelForm
from form_utils.utils import select_template_from_string
register = template.Library()
@register.filter
def render(form, template_name=None):
"""
Renders a ``django.forms.Form`` or
``form_utils.forms.BetterForm`` instance using a template.
The template name(s) may be passed in as the argument to the
filter (use commas to separate multiple template names for
template selection).
If not provided, the default template name is
``form_utils/form.html``.
If the form object to be rendered is an instance of
``form_utils.forms.BetterForm`` or
``form_utils.forms.BetterModelForm``, the template
``form_utils/better_form.html`` will be used instead if present.
"""
default = 'form_utils/form.html'
if isinstance(form, (BetterForm, BetterModelForm)):
default = ','.join(['form_utils/better_form.html', default])
tpl = select_template_from_string(template_name or default)
return tpl.render(template.Context({'form': form}))

20
form_utils/utils.py Normal file
View file

@ -0,0 +1,20 @@
"""
utility functions for django-form-utils
Time-stamp: <2009-03-26 12:32:41 carljm utils.py>
"""
from django.template import loader
def select_template_from_string(arg):
"""
Select a template from a string, which can include multiple
template paths separated by commas.
"""
if ',' in arg:
tpl = loader.select_template(
[tn.strip() for tn in arg.split(',')])
else:
tpl = loader.get_template(arg)
return tpl

112
form_utils/widgets.py Normal file
View file

@ -0,0 +1,112 @@
"""
widgets for django-form-utils
parts of this code taken from http://www.djangosnippets.org/snippets/934/
- thanks baumer1122
"""
import os
import posixpath
from django import forms
from django.conf import settings
from django.utils.functional import curry
from django.utils.safestring import mark_safe
from django.core.files.uploadedfile import SimpleUploadedFile as UploadedFile
from form_utils.settings import JQUERY_URL, FORM_UTILS_MEDIA_URL
try:
from sorl.thumbnail.main import DjangoThumbnail
def thumbnail(image_path, width, height):
t = DjangoThumbnail(relative_source=image_path, requested_size=(width,height))
return u'<img src="%s" alt="%s" />' % (t.absolute_url, image_path)
except ImportError:
def thumbnail(image_path, width, height):
absolute_url = posixpath.join(settings.MEDIA_URL, image_path)
return u'<img src="%s" alt="%s" />' % (absolute_url, image_path)
class ImageWidget(forms.FileInput):
template = '%(input)s<br />%(image)s'
def __init__(self, attrs=None, template=None, width=200, height=200):
if template is not None:
self.template = template
self.width = width
self.height = height
super(ImageWidget, self).__init__(attrs)
def render(self, name, value, attrs=None):
input_html = super(forms.FileInput, self).render(name, value, attrs)
if hasattr(value, 'width') and hasattr(value, 'height'):
image_html = thumbnail(value.name, self.width, self.height)
output = self.template % {'input': input_html,
'image': image_html}
else:
output = input_html
return mark_safe(output)
class ClearableFileInput(forms.MultiWidget):
default_file_widget_class = forms.FileInput
template = '%(input)s Clear: %(checkbox)s'
def __init__(self, file_widget=None,
attrs=None, template=None):
if template is not None:
self.template = template
file_widget = file_widget or self.default_file_widget_class()
super(ClearableFileInput, self).__init__(
widgets=[file_widget, forms.CheckboxInput()],
attrs=attrs)
def render(self, name, value, attrs=None):
if isinstance(value, list):
self.value = value[0]
else:
self.value = value
return super(ClearableFileInput, self).render(name, value, attrs)
def decompress(self, value):
# the clear checkbox is never initially checked
return [value, None]
def format_output(self, rendered_widgets):
if self.value:
return self.template % {'input': rendered_widgets[0],
'checkbox': rendered_widgets[1]}
return rendered_widgets[0]
root = lambda path: posixpath.join(FORM_UTILS_MEDIA_URL, path)
class AutoResizeTextarea(forms.Textarea):
"""
A Textarea widget that automatically resizes to accomodate its contents.
"""
class Media:
js = (JQUERY_URL,
root('form_utils/js/jquery.autogrow.js'),
root('form_utils/js/autoresize.js'))
def __init__(self, *args, **kwargs):
attrs = kwargs.setdefault('attrs', {})
try:
attrs['class'] = "%s autoresize" % (attrs['class'],)
except KeyError:
attrs['class'] = 'autoresize'
attrs.setdefault('cols', 80)
attrs.setdefault('rows', 5)
super(AutoResizeTextarea, self).__init__(*args, **kwargs)
class InlineAutoResizeTextarea(AutoResizeTextarea):
def __init__(self, *args, **kwargs):
attrs = kwargs.setdefault('attrs', {})
try:
attrs['class'] = "%s inline" % (attrs['class'],)
except KeyError:
attrs['class'] = 'inline'
attrs.setdefault('cols', 40)
attrs.setdefault('rows', 2)
super(InlineAutoResizeTextarea, self).__init__(*args, **kwargs)

8
ietf/secr/__init__.py Normal file
View file

@ -0,0 +1,8 @@
__version__ = "1.33"
__date__ = "$Date: 2011/07/26 14:29:17 $"
__rev__ = "$Rev: 3113 $"
__id__ = "$Id: __init__.py,v 1.5 2011/07/26 14:29:17 rcross Exp $"

View file

View file

@ -0,0 +1,172 @@
from django import forms
from django.core.validators import validate_email
from models import *
from ietf.secr.utils.mail import MultiEmailField
from ietf.secr.utils.group import current_nomcom
from ietf.message.models import Message
from ietf.ietfauth.decorators import has_role
from ietf.wgchairs.accounts import get_person_for_user
# ---------------------------------------------
# Globals
# ---------------------------------------------
#ANNOUNCE_FROM_GROUPS = ['ietf','rsoc','iab',current_nomcom().acronym]
ANNOUNCE_TO_GROUPS= ['ietf']
# this list isn't currently available as a Role query so it's hardcoded
FROM_LIST = ('IETF Secretariat <ietf-secretariat@ietf.org>',
'IESG Secretary <iesg-secretary@ietf.org>',
'The IESG <iesg@ietf.org>',
'Internet-Drafts Administrator <internet-drafts@ietf.org>',
'IETF Agenda <agenda@ietf.org>',
'IETF Chair <chair@ietf.org>',
'IAB Chair <iab-chair@ietf.org> ',
'NomCom Chair <nomcom-chair@ietf.org>',
'IETF Registrar <ietf-registrar@ietf.org>',
'IETF Administrative Director <iad@ietf.org>',
'IETF Executive Director <exec-director@ietf.org>',
'The IAOC <bob.hinden@gmail.com>',
'The IETF Trust <tme@multicasttech.com>',
'RSOC Chair <rsoc-chair@iab.org>',
'ISOC Board of Trustees <eburger@standardstrack.com>',
'RFC Series Editor <rse@rfc-editor.org>')
TO_LIST = ('IETF Announcement List <ietf-announce@ietf.org>',
'I-D Announcement List <i-d-announce@ietf.org>',
'The IESG <iesg@ietf.org>',
'Working Group Chairs <wgchairs@ietf.org>',
'BoF Chairs <bofchairs@ietf.org>',
'Other...')
# ---------------------------------------------
# Custom Fields
# ---------------------------------------------
class MultiEmailField(forms.Field):
def to_python(self, value):
"Normalize data to a list of strings."
# Return an empty list if no input was given.
if not value:
return []
import types
if isinstance(value, types.StringTypes):
values = value.split(',')
return [ x.strip() for x in values ]
else:
return value
def validate(self, value):
"Check if value consists only of valid emails."
# Use the parent's handling of required fields, etc.
super(MultiEmailField, self).validate(value)
for email in value:
validate_email(email)
# ---------------------------------------------
# Helper Functions
# ---------------------------------------------
def get_from_choices(user):
'''
This function returns a choices tuple containing
all the Announced From choices. Including
leadership chairs and other entities.
'''
person = user.get_profile()
if has_role(user,'Secretariat'):
f = FROM_LIST
elif has_role(user,'IETF Chair'):
f = (FROM_LIST[2],FROM_LIST[5])
elif has_role(user,'IAB Chair'):
f = (FROM_LIST[6],)
elif has_role(user,'IAD'):
f = (FROM_LIST[9],)
# NomCom, RSOC Chair, IAOC Chair aren't supported by has_role()
elif Role.objects.filter(name="chair",
group__acronym__startswith="nomcom",
group__state="active",
group__type="ietf",
person=person):
f = (FROM_LIST[7],)
elif Role.objects.filter(person=person,
group__acronym='rsoc',
name="chair"):
f = (FROM_LIST[13],)
elif Role.objects.filter(person=person,
group__acronym='iaoc',
name="chair"):
f = (FROM_LIST[11],)
elif Role.objects.filter(person=person,
group__acronym='rse',
name="chair"):
f = (FROM_LIST[15],)
return zip(f,f)
def get_to_choices():
#groups = Group.objects.filter(acronym__in=ANNOUNCE_TO_GROUPS)
#roles = Role.objects.filter(group__in=(groups),name="Announce")
#choices = [ (r.email, r.person.name) for r in roles ]
#choices.append(('Other...','Other...'),)
return zip(TO_LIST,TO_LIST)
# ---------------------------------------------
# Select Choices
# ---------------------------------------------
#TO_CHOICES = tuple(AnnouncedTo.objects.values_list('announced_to_id','announced_to'))
TO_CHOICES = get_to_choices()
#FROM_CHOICES = get_from_choices()
# ---------------------------------------------
# Forms
# ---------------------------------------------
class AnnounceForm(forms.ModelForm):
nomcom = forms.BooleanField(required=False)
to_custom = MultiEmailField(required=False,label='')
#cc = MultiEmailField(required=False)
class Meta:
model = Message
fields = ('nomcom', 'to','to_custom','frm','cc','bcc','reply_to','subject','body')
def __init__(self, *args, **kwargs):
user = kwargs.pop('user')
super(AnnounceForm, self).__init__(*args, **kwargs)
self.fields['to'].widget = forms.Select(choices=TO_CHOICES)
self.fields['to'].help_text = 'Select name OR select Other... and enter email below'
self.fields['cc'].help_text = 'Use comma separated lists for emails (Cc, Bcc, Reply To)'
self.fields['frm'].widget = forms.Select(choices=get_from_choices(user))
self.fields['frm'].label = 'From'
self.fields['nomcom'].label = 'NomCom message?'
def clean(self):
super(AnnounceForm, self).clean()
data = self.cleaned_data
if self.errors:
return self.cleaned_data
if data['to'] == 'Other...' and not data['to_custom']:
raise forms.ValidationError('You must enter a "To" email address')
return data
def save(self, *args, **kwargs):
user = kwargs.pop('user')
message = super(AnnounceForm, self).save(commit=False)
message.by = get_person_for_user(user)
if self.cleaned_data['to'] == 'Other...':
message.to = self.cleaned_data['to_custom']
if kwargs['commit']:
message.save()
# add nomcom to related groups if checked
if self.cleaned_data.get('nomcom', False):
nomcom = current_nomcom()
message.related_groups.add(nomcom)
return message

View file

@ -0,0 +1,4 @@
from django.db import models
from ietf.announcements.models import AnnouncedTo
#from ietf.message.models import Message
from ietf.group.models import Group, Role

View file

@ -0,0 +1,80 @@
from django.db import connection
from django.core.urlresolvers import reverse
from django.test import TestCase
from django.contrib.auth.models import User
from ietf.group.models import Group
from ietf.ietfauth.decorators import has_role
from ietf.person.models import Person
from ietf.utils.mail import outbox
from ietf.utils.test_data import make_test_data
from ietf.utils.test_utils import SimpleUrlTestCase, RealDatabaseTest
from pyquery import PyQuery
SECR_USER='secretary'
WG_USER=''
AD_USER=''
#class AnnouncementUrlTestCase(SimpleUrlTestCase):
# def testUrls(self):
# self.doTestUrls(__file__)
class MainTestCase(TestCase):
fixtures = ['names']
# ------- Test View -------- #
def test_main(self):
"Main Test"
draft = make_test_data()
url = reverse('announcement')
r = self.client.get(url, REMOTE_USER=SECR_USER)
self.assertEquals(r.status_code, 200)
class DummyCase(TestCase):
name = connection.settings_dict['NAME']
print name
class UnauthorizedCase(TestCase):
fixtures = ['names']
def test_unauthorized(self):
"Unauthorized Test"
draft = make_test_data()
url = reverse('announcement')
# get random working group chair
person = Person.objects.filter(role__group__type='wg')[0]
r = self.client.get(url,REMOTE_USER=person.user)
self.assertEquals(r.status_code, 403)
class SubmitCase(TestCase):
fixtures = ['names']
def test_invalid_submit(self):
"Invalid Submit"
draft = make_test_data()
url = reverse('announcement')
post_data = {'id_subject':''}
#self.client.login(remote_user='rcross')
r = self.client.post(url,post_data, REMOTE_USER=SECR_USER)
self.assertEquals(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
def test_valid_submit(self):
"Valid Submit"
draft = make_test_data()
#ietf.utils.mail.test_mode = True
url = reverse('announcement')
redirect = reverse('announcement_confirm')
post_data = {'to':'Other...',
'to_custom':'rcross@amsl.com',
'frm':'IETF Secretariat &lt;ietf-secretariat@ietf.org&gt;',
'subject':'Test Subject',
'body':'This is a test.'}
r = self.client.post(url,post_data,follow=True, REMOTE_USER=SECR_USER)
self.assertRedirects(r, redirect)
# good enough if we get to confirm page
#self.assertEqual(len(outbox), 1)
#self.assertTrue(len(outbox) > mailbox_before)

View file

@ -0,0 +1 @@
200 /secr/announcement/

View file

@ -0,0 +1,6 @@
from django.conf.urls.defaults import *
urlpatterns = patterns('ietf.secr.announcement.views',
url(r'^$', 'main', name='announcement'),
url(r'^confirm/$', 'confirm', name='announcement_confirm'),
)

View file

@ -0,0 +1,107 @@
from django.contrib import messages
from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect, HttpResponse, HttpResponseForbidden
from django.shortcuts import render_to_response, get_object_or_404
from django.template import RequestContext
from ietf.ietfauth.decorators import has_role
from ietf.utils.mail import send_mail_text
from ietf.wgchairs.accounts import get_person_for_user
from ietf.group.models import Group
from ietf.secr.utils.group import current_nomcom
from ietf.secr.utils.decorators import check_for_cancel
from forms import *
# -------------------------------------------------
# Helper Functions
# -------------------------------------------------
def check_access(user):
'''
This function takes a Django User object and returns true if the user has access to the
Announcement app. Accepted roles are:
Secretariat, IAD, IAB Chair, IETF Chair, RSOC Chair, IAOC Chair, NomCom Chair, RSE Chair
'''
person = user.get_profile()
groups_with_access = ("iab", "rsoc", "ietf", "iaoc", "rse")
if Role.objects.filter(person=person,
group__acronym__in=groups_with_access,
name="chair") or has_role(user, ["Secretariat","IAD"]):
return True
if Role.objects.filter(name="chair",
group__acronym__startswith="nomcom",
group__state="active",
group__type="ietf",
person=person):
return True
return False
# --------------------------------------------------
# STANDARD VIEW FUNCTIONS
# --------------------------------------------------
# this seems to cause some kind of circular problem
# @check_for_cancel(reverse('home'))
@check_for_cancel('../')
def main(request):
'''
Main view for Announcement tool. Authrozied users can fill out email details: header, body, etc
and send.
'''
if not check_access(request.user):
return HttpResponseForbidden('Restricted to: Secretariat, IAD, or chair of IETF, IAB, RSOC, RSE, IAOC, NomCom.')
form = AnnounceForm(request.POST or None,user=request.user)
if form.is_valid():
request.session['data'] = form.cleaned_data
url = reverse('announcement_confirm')
return HttpResponseRedirect(url)
return render_to_response('announcement/main.html', {
'form': form},
RequestContext(request, {}),
)
@check_for_cancel('../')
def confirm(request):
# testing
#assert False, (request.session.get_expiry_age(),request.session.get_expiry_date())
if request.method == 'POST':
form = AnnounceForm(request.session['data'],user=request.user)
message = form.save(user=request.user,commit=True)
send_mail_text(None,
message.to,
message.frm,
message.subject,
message.body,
cc=message.cc,
bcc=message.bcc)
# clear session
request.session.clear()
messages.success(request, 'The announcement was sent.')
url = reverse('announcement')
return HttpResponseRedirect(url)
if request.session.get('data',None):
data = request.session['data']
else:
messages.error(request, 'No session data. Your session may have expired or cookies are disallowed.')
redirect_url = reverse('announcement')
return HttpResponseRedirect(redirect_url)
if data['to'] == 'Other...':
to = ','.join(data['to_custom'])
else:
to = data['to']
return render_to_response('announcement/confirm.html', {
'message': data,
'to': to},
RequestContext(request, {}),
)

View file

196
ietf/secr/areas/forms.py Normal file
View file

@ -0,0 +1,196 @@
from django import forms
from ietf.person.models import Person, Email
from ietf.group.models import Group, GroupURL
from ietf.name.models import GroupTypeName, GroupStateName
import datetime
import re
STATE_CHOICES = (
(1, 'Active'),
(2, 'Concluded')
)
class AWPForm(forms.ModelForm):
class Meta:
model = GroupURL
def __init__(self, *args, **kwargs):
super(AWPForm, self).__init__(*args,**kwargs)
self.fields['url'].widget.attrs['width'] = 40
self.fields['name'].widget.attrs['width'] = 40
self.fields['url'].required = False
self.fields['name'].required = False
# Validation: url without description and vice-versa
def clean(self):
super(AWPForm, self).clean()
cleaned_data = self.cleaned_data
url = cleaned_data.get('url')
name = cleaned_data.get('name')
if (url and not name) or (name and not url):
raise forms.ValidationError('You must fill out URL and Name')
# Always return the full collection of cleaned data.
return cleaned_data
class AreaForm(forms.ModelForm):
class Meta:
model = Group
fields = ('acronym','name','state','comments')
# use this method to set attrs which keeps other meta info from model.
def __init__(self, *args, **kwargs):
super(AreaForm, self).__init__(*args, **kwargs)
self.fields['state'].queryset = GroupStateName.objects.filter(slug__in=('active','conclude'))
self.fields['state'].empty_label = None
self.fields['comments'].widget.attrs['rows'] = 2
"""
# Validation: status and conclude_date must agree
def clean(self):
super(AreaForm, self).clean()
cleaned_data = self.cleaned_data
concluded_date = cleaned_data.get('concluded_date')
state = cleaned_data.get('state')
concluded_status_object = AreaStatus.objects.get(status_id=2)
if concluded_date and status != concluded_status_object:
raise forms.ValidationError('Concluded Date set but status is %s' % (status.status_value))
if status == concluded_status_object and not concluded_date:
raise forms.ValidationError('Status is Concluded but Concluded Date not set.')
# Always return the full collection of cleaned data.
return cleaned_data
"""
class AWPAddModelForm(forms.ModelForm):
class Meta:
model = GroupURL
fields = ('url', 'name')
# for use with Add view, ModelForm doesn't work because the parent type hasn't been created yet
# when initial screen is displayed
class AWPAddForm(forms.Form):
url = forms.CharField(
max_length=50,
required=False,
widget=forms.TextInput(attrs={'size':'40'}))
description = forms.CharField(
max_length=50,
required=False,
widget=forms.TextInput(attrs={'size':'40'}))
# Validation: url without description and vice-versa
def clean(self):
super(AWPAddForm, self).clean()
cleaned_data = self.cleaned_data
url = cleaned_data.get('url')
description = cleaned_data.get('description')
if (url and not description) or (description and not url):
raise forms.ValidationError('You must fill out URL and Description')
# Always return the full collection of cleaned data.
return cleaned_data
class AddAreaModelForm(forms.ModelForm):
start_date = forms.DateField()
class Meta:
model = Group
fields = ('acronym','name','state','start_date','comments')
def __init__(self, *args, **kwargs):
super(AddAreaModelForm, self).__init__(*args, **kwargs)
self.fields['acronym'].required = True
self.fields['name'].required = True
self.fields['start_date'].required = True
self.fields['start_date'].initial = datetime.date.today
def clean_acronym(self):
acronym = self.cleaned_data['acronym']
if Group.objects.filter(acronym=acronym):
raise forms.ValidationError("This acronym already exists. Enter a unique one.")
r1 = re.compile(r'[a-zA-Z\-\. ]+$')
if not r1.match(acronym):
raise forms.ValidationError("Enter a valid acronym (only letters,period,hyphen allowed)")
return acronym
def clean_name(self):
name = self.cleaned_data['name']
if Group.objects.filter(name=name):
raise forms.ValidationError("This name already exists. Enter a unique one.")
r1 = re.compile(r'[a-zA-Z\-\. ]+$')
if name and not r1.match(name):
raise forms.ValidationError("Enter a valid name (only letters,period,hyphen allowed)")
return name
def save(self, force_insert=False, force_update=False, commit=True):
area = super(AddAreaModelForm, self).save(commit=False)
area_type = GroupTypeName.objects.get(name='area')
area.type = area_type
if commit:
area.save()
return area
class AddAreaForm(forms.Form):
acronym = forms.CharField(max_length=16,required=True)
name = forms.CharField(max_length=80,required=True)
status = forms.IntegerField(widget=forms.Select(choices=STATE_CHOICES),required=True)
start_date = forms.DateField()
comments = forms.CharField(widget=forms.Textarea(attrs={'rows':'1'}),required=False)
def clean_acronym(self):
# get name, strip leading and trailing spaces
name = self.cleaned_data.get('acronym', '').strip()
# check for invalid characters
r1 = re.compile(r'[a-zA-Z\-\. ]+$')
if name and not r1.match(name):
raise forms.ValidationError("Enter a valid acronym (only letters,period,hyphen allowed)")
# ensure doesn't already exist
if Acronym.objects.filter(acronym=name):
raise forms.ValidationError("This acronym already exists. Enter a unique one.")
return name
class AreaDirectorForm(forms.Form):
ad_name = forms.CharField(max_length=100,label='Name',help_text="To see a list of people type the first name, or last name, or both.")
#login = forms.EmailField(max_length=75,help_text="This should be the person's primary email address.")
#email = forms.ChoiceField(help_text="This should be the person's primary email address.")
email = forms.CharField(help_text="Select the email address to associate with this AD Role")
# set css class=name-autocomplete for name field (to provide select list)
def __init__(self, *args, **kwargs):
super(AreaDirectorForm, self).__init__(*args, **kwargs)
self.fields['ad_name'].widget.attrs['class'] = 'name-autocomplete'
self.fields['email'].widget = forms.Select(choices=[])
def clean_ad_name(self):
name = self.cleaned_data.get('ad_name', '')
# check for tag within parenthesis to ensure name was selected from the list
m = re.search(r'\((\d+)\)', name)
if not name or not m:
raise forms.ValidationError("You must select an entry from the list!")
try:
id = m.group(1)
person = Person.objects.get(id=id)
except Person.DoesNotExist:
raise forms.ValidationError("ERROR finding Person with ID: %s" % id)
return person
def clean_email(self):
# this ChoiceField gets populated by javascript so skip regular validation
# which raises an error
email = self.cleaned_data['email']
if not email:
raise forms.ValidationError("You must select an email. If none are listed you'll need to add one first.")
try:
obj = Email.objects.get(address=email)
except Email.DoesNotExist:
raise forms.ValidationError("Can't find this email.")
return obj

View file

@ -0,0 +1 @@
from django.db import models

View file

View file

@ -0,0 +1,10 @@
from django import template
from ietf.secr.areas import models
register = template.Library()
@register.inclusion_tag('areas/directors.html')
def display_directors(area_id):
area = models.Area.objects.get(area_acronym__exact=area_id)
directors = models.AreaDirector.objects.filter(area=area)
return { 'directors': directors }

39
ietf/secr/areas/tests.py Normal file
View file

@ -0,0 +1,39 @@
from django.core.urlresolvers import reverse
from django.test import TestCase
from django.contrib.auth.models import User
from ietf.group.models import Group, GroupEvent
from ietf.ietfauth.decorators import has_role
from ietf.person.models import Person
from ietf.utils.test_data import make_test_data
from pyquery import PyQuery
import datetime
SECR_USER='secretary'
def augment_data():
area = Group.objects.get(acronym='farfut')
GroupEvent.objects.create(group=area,
type='started',
by_id=0)
class MainTestCase(TestCase):
fixtures = ['names']
def test_main(self):
"Main Test"
draft = make_test_data()
url = reverse('areas')
response = self.client.get(url, REMOTE_USER=SECR_USER)
self.assertEquals(response.status_code, 200)
def test_view(self):
"View Test"
draft = make_test_data()
augment_data()
areas = Group.objects.filter(type='area',state='active')
url = reverse('areas_view', kwargs={'name':areas[0].acronym})
response = self.client.get(url, REMOTE_USER=SECR_USER)
self.assertEquals(response.status_code, 200)

12
ietf/secr/areas/urls.py Normal file
View file

@ -0,0 +1,12 @@
from django.conf.urls.defaults import *
urlpatterns = patterns('ietf.secr.areas.views',
url(r'^$', 'list_areas', name='areas'),
url(r'^add/$', 'add', name='areas_add'),
url(r'^getemails', 'getemails', name='areas_emails'),
url(r'^getpeople', 'getpeople', name='areas_getpeople'),
url(r'^(?P<name>[A-Za-z0-9.-]+)/$', 'view', name='areas_view'),
url(r'^(?P<name>[A-Za-z0-9.-]+)/edit/$', 'edit', name='areas_edit'),
url(r'^(?P<name>[A-Za-z0-9.-]+)/people/$', 'people', name='areas_people'),
url(r'^(?P<name>[A-Za-z0-9.-]+)/people/modify/$', 'modify', name='areas_modify'),
)

321
ietf/secr/areas/views.py Normal file
View file

@ -0,0 +1,321 @@
from django.contrib import messages
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
from django.core.urlresolvers import reverse
from django.forms.formsets import formset_factory
from django.forms.models import inlineformset_factory, modelformset_factory
from django.http import HttpResponseRedirect, HttpResponse
from django.shortcuts import render_to_response, get_object_or_404
from django.template import RequestContext
from django.utils import simplejson
from ietf.group.models import Group, GroupEvent, GroupURL, Role
from ietf.group.utils import save_group_in_history
from ietf.name.models import RoleName
from ietf.person.models import Person, Email
from forms import *
import re
# --------------------------------------------------
# AJAX FUNCTIONS
# --------------------------------------------------
def getpeople(request):
"""
Ajax function to find people. Takes one or two terms (ignores rest) and
returns JSON format response: first name, last name, primary email, tag
"""
result = []
term = request.GET.get('term','')
qs = Person.objects.filter(name__icontains=term)
for item in qs:
full = '%s - (%s)' % (item.name,item.id)
result.append(full)
return HttpResponse(simplejson.dumps(result), mimetype='application/javascript')
def getemails(request):
"""
Ajax function to get emails for given Person Id. Used for adding Area ADs.
returns JSON format response: [{id:email, value:email},...]
"""
results=[]
id = request.GET.get('id','')
person = Person.objects.get(id=id)
for item in person.email_set.filter(active=True):
d = {'id': item.address, 'value': item.address}
results.append(d)
return HttpResponse(simplejson.dumps(results), mimetype='application/javascript')
# --------------------------------------------------
# STANDARD VIEW FUNCTIONS
# --------------------------------------------------
def add(request):
"""
Add a new IETF Area
**Templates:**
* ``areas/add.html``
**Template Variables:**
* area_form
"""
AWPFormSet = formset_factory(AWPAddModelForm, extra=2)
if request.method == 'POST':
area_form = AddAreaModelForm(request.POST)
awp_formset = AWPFormSet(request.POST, prefix='awp')
if area_form.is_valid() and awp_formset.is_valid():
area = area_form.save()
#save groupevent 'started' record
start_date = area_form.cleaned_data.get('start_date')
login = request.user.get_profile()
group_event = GroupEvent(group=area,time=start_date,type='started',by=login)
group_event.save()
# save AWPs
for item in awp_formset.cleaned_data:
if item.get('url', 0):
group_url = GroupURL(group=area,name=item['name'],url=item['url'])
group_url.save()
messages.success(request, 'The Area was created successfully!')
url = reverse('areas')
return HttpResponseRedirect(url)
else:
# display initial forms
area_form = AddAreaModelForm()
awp_formset = AWPFormSet(prefix='awp')
return render_to_response('areas/add.html', {
'area_form': area_form,
'awp_formset': awp_formset},
RequestContext(request, {}),
)
def edit(request, name):
"""
Edit IETF Areas
**Templates:**
* ``areas/edit.html``
**Template Variables:**
* acronym, area_formset, awp_formset, acronym_form
"""
area = get_object_or_404(Group, acronym=name, type='area')
AWPFormSet = inlineformset_factory(Group, GroupURL, form=AWPForm, max_num=2)
if request.method == 'POST':
button_text = request.POST.get('submit', '')
if button_text == 'Save':
form = AreaForm(request.POST, instance=area)
awp_formset = AWPFormSet(request.POST, instance=area)
if form.is_valid() and awp_formset.is_valid():
state = form.cleaned_data['state']
# save group
save_group_in_history(area)
new_area = form.save()
new_area.time = datetime.datetime.now()
new_area.save()
awp_formset.save()
# create appropriate GroupEvent
if 'state' in form.changed_data:
ChangeStateGroupEvent.objects.create(group=new_area,
type='changed_state',
by=request.user.get_profile(),
state=state,
time=new_area.time)
form.changed_data.remove('state')
# if anything else was changed
if form.changed_data:
GroupEvent.objects.create(group=new_area,
type='info_changed',
by=request.user.get_profile(),
time=new_area.time)
messages.success(request, 'The Area entry was changed successfully')
url = reverse('areas_view', kwargs={'name':name})
return HttpResponseRedirect(url)
else:
url = reverse('areas_view', kwargs={'name':name})
return HttpResponseRedirect(url)
else:
form = AreaForm(instance=area)
awp_formset = AWPFormSet(instance=area)
return render_to_response('areas/edit.html', {
'area': area,
'form': form,
'awp_formset': awp_formset,
},
RequestContext(request,{}),
)
def list_areas(request):
"""
List IETF Areas
**Templates:**
* ``areas/list.html``
**Template Variables:**
* results
"""
results = Group.objects.filter(type="area").order_by('name')
return render_to_response('areas/list.html', {
'results': results},
RequestContext(request, {}),
)
def people(request, name):
"""
Edit People associated with Areas, Area Directors.
# Legacy ------------------
When a new Director is first added they get a user_level of 4, read-only.
Then, when Director is made active (Enable Voting) user_level = 1.
# New ---------------------
First Director's are assigned the Role 'pre-ad' Incoming Area director
Then they get 'ad' role
**Templates:**
* ``areas/people.html``
**Template Variables:**
* directors, area
"""
area = get_object_or_404(Group, type='area', acronym=name)
if request.method == 'POST':
if request.POST.get('submit', '') == "Add":
form = AreaDirectorForm(request.POST)
if form.is_valid():
email = form.cleaned_data['email']
person = form.cleaned_data['ad_name']
# save group
save_group_in_history(area)
# create role
Role.objects.create(name_id='pre-ad',group=area,email=email,person=person)
messages.success(request, 'New Area Director added successfully!')
url = reverse('areas_view', kwargs={'name':name})
return HttpResponseRedirect(url)
else:
form = AreaDirectorForm()
directors = area.role_set.filter(name__slug__in=('ad','pre-ad'))
return render_to_response('areas/people.html', {
'area': area,
'form': form,
'directors': directors},
RequestContext(request, {}),
)
def modify(request, name):
"""
Handle state changes of Area Directors (enable voting, retire)
# Legacy --------------------------
Enable Voting actions
- user_level = 1
- create TelechatUser object
Per requirements, the Retire button shall perform the following DB updates
- update iesg_login row, user_level = 2 (per Matt Feb 7, 2011)
- remove telechat_user row (to revoke voting rights)
- update IETFWG(groups) set area_director = TBD
- remove area_director row
# New ------------------------------
Enable Voting: change Role from 'pre-ad' to 'ad'
Retire: save in history, delete role record, set group assn to TBD
**Templates:**
* none
Redirects to view page on success.
"""
area = get_object_or_404(Group, type='area', acronym=name)
# should only get here with POST method
if request.method == 'POST':
# setup common request variables
tag = request.POST.get('tag', '')
person = Person.objects.get(id=tag)
# save group
save_group_in_history(area)
# handle retire request
if request.POST.get('submit', '') == "Retire":
role = Role.objects.get(group=area,name__in=('ad','pre-ad'),person=person)
role.delete()
# update groups that have this AD as primary AD
for group in Group.objects.filter(ad=person,type='wg',state__in=('active','bof')):
group.ad = None
group.save()
messages.success(request, 'The Area Director has been retired successfully!')
# handle voting request
if request.POST.get('submit', '') == "Enable Voting":
role = Role.objects.get(group=area,name__slug='pre-ad',person=person)
role.name_id = 'ad'
role.save()
messages.success(request, 'Voting rights have been granted successfully!')
url = reverse('areas_view', kwargs={'name':name})
return HttpResponseRedirect(url)
def view(request, name):
"""
View Area information.
**Templates:**
* ``areas/view.html``
**Template Variables:**
* area, directors
"""
area = get_object_or_404(Group, type='area', acronym=name)
try:
area.start_date = area.groupevent_set.order_by('time')[0].time
area.concluded_date = area.groupevent_set.get(type='concluded').time
except GroupEvent.DoesNotExist:
pass
directors = area.role_set.filter(name__slug__in=('ad','pre-ad'))
return render_to_response('areas/view.html', {
'area': area,
'directors': directors},
RequestContext(request, {}),
)

View file

View file

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View file

@ -0,0 +1,23 @@
"""
This file demonstrates two different styles of tests (one doctest and one
unittest). These will both pass when you run "manage.py test".
Replace these with more appropriate tests for your application.
"""
from django.test import TestCase
class SimpleTest(TestCase):
def test_basic_addition(self):
"""
Tests that 1 + 1 always equals 2.
"""
self.failUnlessEqual(1 + 1, 2)
__test__ = {"doctest": """
Another way to test that 1 + 1 is equal to 2.
>>> 1 + 1 == 2
True
"""}

View file

@ -0,0 +1,5 @@
from django.conf.urls.defaults import *
urlpatterns = patterns('ietf.secr.console.views',
url(r'^$', 'main', name='console'),
)

View file

@ -0,0 +1,17 @@
from django.http import HttpResponseRedirect, HttpResponse, HttpResponseForbidden
from django.shortcuts import render_to_response, get_object_or_404
from django.template import RequestContext
from ietf.doc.models import *
def main(request):
'''
Main view for the Console
'''
latest_docevent = DocEvent.objects.all().order_by('-time')[0]
return render_to_response('console/main.html', {
'latest_docevent': latest_docevent},
RequestContext(request, {}),
)

View file

@ -0,0 +1,13 @@
# Copyright The IETF Trust 2007, All Rights Reserved
from django.conf import settings
from ietf.secr import __date__, __rev__, __version__, __id__
def server_mode(request):
return {'server_mode': settings.SERVER_MODE}
def secr_revision_info(request):
return {'secr_revision_time': __date__[7:32], 'secr_revision_date': __date__[7:17], 'secr_revision_num': __rev__[6:-2], "secr_revision_id": __id__[5:-2], "secr_version_num": __version__ }
def static(request):
return {'SECR_STATIC_URL': settings.SECR_STATIC_URL}

View file

282
ietf/secr/drafts/email.py Normal file
View file

@ -0,0 +1,282 @@
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.template.loader import render_to_string
from ietf.message.models import Message, SendQueue
from ietf.announcements.send_scheduled import send_scheduled_announcement
from ietf.doc.models import DocumentAuthor
from ietf.person.models import Person
from ietf.secr.utils.document import get_start_date
import datetime
import glob
import os
import time
def announcement_from_form(data, **kwargs):
'''
This function creates a new message record. Taking as input EmailForm.data
and key word arguments used to override some of the message fields
'''
# possible overrides
by = kwargs.get('by',Person.objects.get(name='(System)'))
from_val = kwargs.get('from_val','ID Tracker <internet-drafts-reply@ietf.org>')
content_type = kwargs.get('content_type','')
# from the form
subject = data['subject']
to_val = data['to']
cc_val = data['cc']
body = data['body']
message = Message.objects.create(by=by,
subject=subject,
frm=from_val,
to=to_val,
cc=cc_val,
body=body,
content_type=content_type)
# create SendQueue
send_queue = SendQueue.objects.create(by=by,message=message)
# uncomment for testing
send_scheduled_announcement(send_queue)
return message
def get_authors(draft):
"""
Takes a draft object and returns a list of authors suitable for a tombstone document
"""
authors = []
for a in draft.authors.all():
initial = ''
prefix, first, middle, last, suffix = a.person.name_parts()
if first:
initial = first + '. '
entry = '%s%s <%s>' % (initial,last,a.address)
authors.append(entry)
return authors
def get_abbr_authors(draft):
"""
Takes a draft object and returns a string of first author followed by "et al"
for use in New Revision email body.
"""
initial = ''
result = ''
authors = DocumentAuthor.objects.filter(document=draft)
if authors:
prefix, first, middle, last, suffix = authors[0].author.person.name_parts()
if first:
initial = first[0] + '. '
result = '%s%s' % (initial,last)
if len(authors) > 1:
result += ', et al'
return result
def get_authors_email(draft):
"""
Takes a draft object and returns a string of authors suitable for an email to or cc field
"""
authors = []
for a in draft.authors.all():
initial = ''
if a.person.first_name:
initial = a.person.first_name[0] + '. '
entry = '%s%s <%s>' % (initial,a.person.last_name,a.person.email())
authors.append(entry)
return ', '.join(authors)
def get_last_revision(filename):
"""
This function takes a filename, in the same form it appears in the InternetDraft record,
no revision or extension (ie. draft-ietf-alto-reqs) and returns a string which is the
reivision number of the last active version of the document, the highest revision
txt document in the archive directory. If no matching file is found raise exception.
"""
files = glob.glob(os.path.join(settings.INTERNET_DRAFT_ARCHIVE_DIR,filename) + '-??.txt')
if files:
sorted_files = sorted(files)
return get_revision(sorted_files[-1])
else:
raise Exception('last revision not found in archive')
def get_revision(name):
"""
Takes a draft filename and returns the revision, as a string.
"""
#return name[-6:-4]
base,ext = os.path.splitext(name)
return base[-2:]
def get_revision_emails(draft):
"""
Dervied from the ColdFusion legacy app, we accumulate To: emails for a new
revision by adding:
1) the conents of id_internal.state_change_notice_to, this appears to be largely
custom mail lists for the document or group
2) the main AD, via id_internal.job_owner
3) any ad who has marked "discuss" in the ballot associated with this id_internal
"""
# from legacy
if not draft.get_state('draft-iesg'):
return ''
emails = []
if draft.notify:
emails.append(draft.notify)
if draft.ad:
emails.append(draft.ad.role_email("ad").address)
if draft.active_ballot():
for ad, pos in draft.active_ballot().active_ad_positions().iteritems():
if pos and pos.pos_id == "discuss":
emails.append(ad.role_email("ad").address)
return ', '.join(emails)
def add_email(emails,person):
if person.email() not in emails:
emails[person.email()] = '"%s %s"' % (person.first_name,person.last_name)
def get_fullcc_list(draft):
"""
This function takes a draft object and returns a string of emails to use in cc field
of a standard notification. Uses an intermediate "emails" dictionary, emails are the
key, name is the value, to prevent adding duplicate emails to the list.
"""
emails = {}
# get authors
for author in draft.authors.all():
if author.address not in emails:
emails[author.address] = '"%s"' % (author.person.name)
if draft.group.acronym != 'none':
# add chairs
for role in draft.group.role_set.filter(name='chair'):
if role.email.address not in emails:
emails[role.email.address] = '"%s"' % (role.person.name)
# add AD
if draft.group.type.slug == 'wg':
emails['%s-ads@tools.ietf.org' % draft.group.acronym] = '"%s-ads"' % (draft.group.acronym)
elif draft.group.type.slug == 'rg':
email = draft.group.parent.role_set.filter(name='chair')[0].email
emails[email.address] = '"%s"' % (email.person.name)
# add sheperd
if draft.shepherd:
emails[draft.shepherd.email_address()] = '"%s"' % (draft.shepherd.name)
"""
# add wg advisor
try:
advisor = IETFWG.objects.get(group_acronym=draft.group.acronym_id).area_director
if advisor:
add_email(emails,advisor.person)
except ObjectDoesNotExist:
pass
# add shepherding ad
try:
id = IDInternal.objects.get(draft=draft.id_document_tag)
add_email(emails,id.job_owner.person)
except ObjectDoesNotExist:
pass
# add state_change_notice to
try:
id = IDInternal.objects.get(draft=draft.id_document_tag)
for email in id.state_change_notice_to.split(','):
if email.strip() not in emails:
emails[email.strip()] = ''
except ObjectDoesNotExist:
pass
"""
# use sort so we get consistently ordered lists
result_list = []
for key in sorted(emails):
if emails[key]:
result_list.append('%s <%s>' % (emails[key],key))
else:
result_list.append('<%s>' % key)
return ','.join(result_list)
def get_email_initial(draft, type=None, input=None):
"""
Takes a draft object, a string representing the email type:
(extend,new,replace,resurrect,revision,update,withdraw) and
a dictonary of the action form input data (for use with replace, update, extend).
Returns a dictionary containing initial field values for a email notification.
The dictionary consists of to, cc, subject, body.
NOTE: for type=new we are listing all authors in the message body to match legacy app.
It appears datatracker abbreviates the list with "et al". Datatracker scheduled_announcement
entries have "Action" in subject whereas this app uses "ACTION"
"""
# assert False, (draft, type, input)
expiration_date = (datetime.date.today() + datetime.timedelta(185)).strftime('%B %d, %Y')
new_revision = str(int(draft.rev)+1).zfill(2)
new_filename = draft.name + '-' + new_revision + '.txt'
curr_filename = draft.name + '-' + draft.rev + '.txt'
data = {}
data['cc'] = get_fullcc_list(draft)
data['to'] = ''
if type == 'extend':
context = {'doc':curr_filename,'expire_date':input['expiration_date']}
data['subject'] = 'Extension of Expiration Date for %s' % (curr_filename)
data['body'] = render_to_string('drafts/message_extend.txt', context)
elif type == 'new':
# if the ID belongs to a group other than "none" add line to message body
if draft.group.type.slug == 'wg':
wg_message = 'This draft is a work item of the %s Working Group of the IETF.' % draft.group.name
else:
wg_message = ''
context = {'wg_message':wg_message,
'draft':draft,
'authors':get_abbr_authors(draft),
'revision_date':draft.latest_event(type='new_revision').time.date(),
'timestamp':time.strftime("%Y-%m-%d%H%M%S", time.localtime())}
data['to'] = 'i-d-announce@ietf.org'
data['cc'] = draft.group.list_email
data['subject'] = 'I-D ACTION:%s' % (curr_filename)
data['body'] = render_to_string('drafts/message_new.txt', context)
elif type == 'replace':
'''
input['replaced'] is a DocAlias
input['replaced_by'] is a Document
'''
context = {'doc':input['replaced'].name,'replaced_by':input['replaced_by'].name}
data['subject'] = 'Replacement of %s with %s' % (input['replaced'].name,input['replaced_by'].name)
data['body'] = render_to_string('drafts/message_replace.txt', context)
elif type == 'resurrect':
last_revision = get_last_revision(draft.name)
last_filename = draft.name + '-' + last_revision + '.txt'
context = {'doc':last_filename,'expire_date':expiration_date}
data['subject'] = 'Resurrection of %s' % (last_filename)
data['body'] = render_to_string('drafts/message_resurrect.txt', context)
elif type == 'revision':
context = {'rev':new_revision,'doc':new_filename,'doc_base':new_filename[:-4]}
data['to'] = get_revision_emails(draft)
data['cc'] = ''
data['subject'] = 'New Version Notification - %s' % (new_filename)
data['body'] = render_to_string('drafts/message_revision.txt', context)
elif type == 'update':
context = {'doc':input['filename'],'expire_date':expiration_date}
data['subject'] = 'Posting of %s' % (input['filename'])
data['body'] = render_to_string('drafts/message_update.txt', context)
elif type == 'withdraw':
context = {'doc':curr_filename,'by':input['type']}
data['subject'] = 'Withdrawl of %s' % (curr_filename)
data['body'] = render_to_string('drafts/message_withdraw.txt', context)
return data

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,56 @@
[
{
"pk": "rcross@amsl.com",
"model": "person.email",
"fields": {
"active": true,
"person": 111252,
"time": "1970-01-01 23:59:59"
}
},
{
"pk": "fluffy@cisco.com",
"model": "person.email",
"fields": {
"active": true,
"person": 105791,
"time": "1970-01-01 23:59:59"
}
},
{
"pk": "cabo@tzi.org",
"model": "person.email",
"fields": {
"active": true,
"person": 11843,
"time": "1970-01-01 23:59:59"
}
},
{
"pk": "br@brianrosen.net",
"model": "person.email",
"fields": {
"active": true,
"person": 106987,
"time": "1970-01-01 23:59:59"
}
},
{
"pk": "gaborbajko@yahoo.com",
"model": "person.email",
"fields": {
"active": true,
"person": 108123,
"time": "1970-01-01 23:59:59"
}
},
{
"pk": "wdec@cisco.com",
"model": "person.email",
"fields": {
"active": true,
"person": 106526,
"time": "1970-01-01 23:59:59"
}
}
]

View file

@ -0,0 +1,224 @@
[
{
"pk": 4,
"model": "group.group",
"fields": {
"charter": null,
"unused_states": [],
"ad": null,
"parent": null,
"list_email": "",
"acronym": "secretariat",
"comments": "",
"list_subscribe": "",
"state": "active",
"time": "2012-01-24 13:17:42",
"unused_tags": [],
"list_archive": "",
"type": "ietf",
"name": "IETF Secretariat"
}
},
{
"pk": 29,
"model": "group.group",
"fields": {
"charter": null,
"unused_states": [],
"ad": null,
"parent": null,
"list_email": "",
"acronym": "nomcom2011",
"comments": "",
"list_subscribe": "",
"state": "active",
"time": "2012-01-24 13:17:42",
"unused_tags": [],
"list_archive": "",
"type": "ietf",
"name": "IAB/IESG Nominating Committee 2011/2012"
}
},
{
"pk": 1008,
"model": "group.group",
"fields": {
"charter": null,
"unused_states": [],
"ad": null,
"parent": 2,
"list_email": "",
"acronym": "gen",
"comments": "",
"list_subscribe": "",
"state": "active",
"time": "2012-01-24 13:17:42",
"unused_tags": [],
"list_archive": "",
"type": "area",
"name": "General Area"
}
},
{
"pk": 1052,
"model": "group.group",
"fields": {
"charter": null,
"unused_states": [],
"ad": null,
"parent": 2,
"list_email": "",
"acronym": "int",
"comments": "",
"list_subscribe": "",
"state": "active",
"time": "2012-01-24 13:17:42",
"unused_tags": [],
"list_archive": "",
"type": "area",
"name": "Internet Area"
}
},
{
"pk": 934,
"model": "group.group",
"fields": {
"charter": null,
"unused_states": [],
"ad": null,
"parent": 2,
"list_email": "",
"acronym": "app",
"comments": "",
"list_subscribe": "",
"state": "active",
"time": "2012-01-24 13:17:42",
"unused_tags": [],
"list_archive": "",
"type": "area",
"name": "Applications Area"
}
},
{
"pk": 1789,
"model": "group.group",
"fields": {
"charter": "charter-ietf-core",
"unused_states": [],
"ad": 105907,
"parent": 934,
"list_email": "core@ietf.org",
"acronym": "core",
"comments": "",
"list_subscribe": "https://www.ietf.org/mailman/listinfo/core",
"state": "active",
"time": "2011-12-09 12:00:00",
"unused_tags": [],
"list_archive": "http://www.ietf.org/mail-archive/web/core/",
"type": "wg",
"name": "Constrained RESTful Environments"
}
},
{
"pk": 1819,
"model": "group.group",
"fields": {
"charter": "charter-ietf-paws",
"unused_states": [],
"ad": 105907,
"parent": 934,
"list_email": "paws@ietf.org",
"acronym": "paws",
"comments": "",
"list_subscribe": "https://www.ietf.org/mailman/listinfo/paws",
"state": "active",
"time": "2011-12-09 12:00:00",
"unused_tags": [],
"list_archive": "http://www.ietf.org/mail-archive/web/paws/",
"type": "wg",
"name": "Protocol to Access WS database"
}
},
{
"pk": 1693,
"model": "group.group",
"fields": {
"charter": "charter-ietf-ancp",
"unused_states": [],
"ad": 2348,
"parent": 1052,
"list_email": "ancp@ietf.org",
"acronym": "ancp",
"comments": "",
"list_subscribe": "ancp-request@ietf.org",
"state": "active",
"time": "2011-12-09 12:00:00",
"unused_tags": [],
"list_archive": "http://www.ietf.org/mail-archive/web/ancp/",
"type": "wg",
"name": "Access Node Control Protocol"
}
},
{
"pk": 1723,
"model": "group.group",
"fields": {
"charter": "charter-ietf-6man",
"unused_states": [],
"ad": 21072,
"parent": 1052,
"list_email": "ipv6@ietf.org",
"acronym": "6man",
"comments": "",
"list_subscribe": "https://www.ietf.org/mailman/listinfo/ipv6",
"state": "active",
"time": "2011-12-09 12:00:00",
"unused_tags": [],
"list_archive": "http://www.ietf.org/mail-archive/web/ipv6",
"type": "wg",
"name": "IPv6 Maintenance"
}
},
{
"pk": 1377,
"model": "group.group",
"fields": {
"charter": "charter-ietf-adsl",
"unused_states": [],
"ad": null,
"parent": 1052,
"list_email": "adsl@xlist.agcs.com",
"acronym": "adsl",
"comments": "",
"list_subscribe": "mgr@xlist.agcs.com",
"state": "conclude",
"time": "2011-12-09 12:00:00",
"unused_tags": [],
"list_archive": "",
"type": "wg",
"name": "Asymmetric Digital Subscriber Line"
}
},
{
"pk": 30,
"model": "group.group",
"fields": {
"charter": null,
"unused_states": [],
"ad": null,
"parent": 3,
"list_email": "",
"acronym": "asrg",
"comments": "",
"list_subscribe": "",
"state": "active",
"time": "2012-01-24 13:17:42",
"unused_tags": [],
"list_archive": "",
"type": "rg",
"name": "Anti-Spam Research Group"
}
}
]

View file

@ -0,0 +1,82 @@
[
{
"pk": 79,
"model": "meeting.meeting",
"fields": {
"city": "Beijing",
"venue_name": "",
"country": "CN",
"time_zone": "Asia/Shanghai",
"reg_area": "Valley Ballroom Foyer",
"number": "79",
"break_area": "Valley Ballroom Foyer",
"date": "2010-11-07",
"type": "ietf",
"venue_addr": ""
}
},
{
"pk": 80,
"model": "meeting.meeting",
"fields": {
"city": "Prague",
"venue_name": "",
"country": "CZ",
"time_zone": "Europe/Prague",
"reg_area": "Congress Hall Foyer",
"number": "80",
"break_area": "Congress Hall Foyer",
"date": "2011-03-27",
"type": "ietf",
"venue_addr": ""
}
},
{
"pk": 81,
"model": "meeting.meeting",
"fields": {
"city": "Quebec",
"venue_name": "",
"country": "CA",
"time_zone": "",
"reg_area": "2000 A",
"number": "81",
"break_area": "2000 BC",
"date": "2011-07-24",
"type": "ietf",
"venue_addr": ""
}
},
{
"pk": 82,
"model": "meeting.meeting",
"fields": {
"city": "Taipei",
"venue_name": "",
"country": "TW",
"time_zone": "",
"reg_area": "1F North Extended",
"number": "82",
"break_area": "Common Area",
"date": "2011-11-13",
"type": "ietf",
"venue_addr": ""
}
},
{
"pk": 83,
"model": "meeting.meeting",
"fields": {
"city": "Paris",
"venue_name": "",
"country": "FR",
"time_zone": "Europe/Paris",
"reg_area": "",
"number": "83",
"break_area": "",
"date": "2012-03-25",
"type": "ietf",
"venue_addr": ""
}
}
]

View file

@ -0,0 +1,132 @@
[
{
"pk": 111252,
"model": "person.person",
"fields": {
"name": "Ryan Cross",
"ascii_short": null,
"time": "2012-01-24 13:00:24",
"affiliation": "",
"user": 486,
"address": "",
"ascii": "Ryan Cross"
}
},
{
"pk": 105791,
"model": "person.person",
"fields": {
"name": "Cullen Jennings",
"ascii_short": null,
"time": "2012-01-24 13:00:23",
"affiliation": "Cisco Systems",
"user": 454,
"address": "",
"ascii": "Cullen Jennings"
}
},
{
"pk": 11843,
"model": "person.person",
"fields": {
"name": "Dr. Carsten Bormann",
"ascii_short": null,
"time": "2012-01-24 13:00:13",
"affiliation": "University Bremen TZI",
"user": 1128,
"address": "",
"ascii": "Dr. Carsten Bormann"
}
},
{
"pk": 106987,
"model": "person.person",
"fields": {
"name": "Brian Rosen",
"ascii_short": null,
"time": "2012-01-24 13:00:13",
"affiliation": "",
"user": 1016,
"address": "",
"ascii": "Brian Rosen"
}
},
{
"pk": 108123,
"model": "person.person",
"fields": {
"name": "Gabor Bajko",
"ascii_short": null,
"time": "2012-01-24 13:00:15",
"affiliation": "",
"user": 700,
"address": "",
"ascii": "Gabor Bajko"
}
},
{
"pk": 106526,
"model": "person.person",
"fields": {
"name": "Wojciech Dec",
"ascii_short": null,
"time": "2012-01-24 13:00:19",
"affiliation": "",
"user": 1395,
"address": "",
"ascii": "Wojciech Dec"
}
},
{
"pk": 105786,
"model": "person.person",
"fields": {
"name": "Matthew Bocci",
"ascii_short": null,
"time": "2012-01-24 13:00:16",
"affiliation": "",
"user": 483,
"address": "",
"ascii": "Matthew Bocci"
}
},
{
"pk": 2793,
"model": "person.person",
"fields": {
"name": "Robert M. Hinden",
"ascii_short": null,
"time": "2012-01-24 13:00:13",
"affiliation": "Nokia",
"user": 844,
"address": "",
"ascii": "Robert M. Hinden"
}
},
{
"pk": 106653,
"model": "person.person",
"fields": {
"name": "Brian Haberman",
"ascii_short": null,
"time": "2012-01-24 13:12:51",
"affiliation": "",
"user": null,
"address": "",
"ascii": "Brian Haberman"
}
},
{
"pk": 112453,
"model": "person.person",
"fields": {
"name": "Russel Housley",
"ascii_short": null,
"time": "2012-01-24 13:16:11",
"affiliation": "",
"user": null,
"address": "",
"ascii": "Russel Housley"
}
}
]

View file

@ -0,0 +1,62 @@
[
{
"pk": 1610,
"model": "group.role",
"fields": {
"person": 111252,
"group": 4,
"name": "secr",
"email": "rcross@amsl.com"
}
},
{
"pk": 1229,
"model": "group.role",
"fields": {
"person": 105791,
"group": 1358,
"name": "chair",
"email": "fluffy@cisco.com"
}
},
{
"pk": 1416,
"model": "group.role",
"fields": {
"person": 11843,
"group": 1774,
"name": "chair",
"email": "cabo@tzi.org"
}
},
{
"pk": 1515,
"model": "group.role",
"fields": {
"person": 106987,
"group": 1819,
"name": "chair",
"email": "br@brianrosen.net"
}
},
{
"pk": 1516,
"model": "group.role",
"fields": {
"person": 108123,
"group": 1819,
"name": "chair",
"email": "Gabor.Bajko@nokia.com"
}
},
{
"pk": 461,
"model": "group.role",
"fields": {
"person": 106526,
"group": 1693,
"name": "chair",
"email": "wdec@cisco.com"
}
}
]

View file

@ -0,0 +1,164 @@
[
{
"pk": 486,
"model": "auth.user",
"fields": {
"username": "rcross",
"first_name": "",
"last_name": "",
"is_active": true,
"is_superuser": true,
"is_staff": true,
"last_login": "2012-01-25 08:56:54",
"groups": [],
"user_permissions": [],
"password": "nopass",
"email": "",
"date_joined": "2010-07-27 01:32:02"
}
},
{
"pk": 454,
"model": "auth.user",
"fields": {
"username": "fluffy@cisco.com",
"first_name": "",
"last_name": "",
"is_active": true,
"is_superuser": false,
"is_staff": false,
"last_login": "2012-01-23 17:27:39",
"groups": [],
"user_permissions": [],
"password": "",
"email": "",
"date_joined": "2010-03-10 16:04:51"
}
},
{
"pk": 1128,
"model": "auth.user",
"fields": {
"username": "cabo@tzi.org",
"first_name": "",
"last_name": "",
"is_active": true,
"is_superuser": false,
"is_staff": false,
"last_login": "2012-01-10 05:07:13",
"groups": [],
"user_permissions": [],
"password": "",
"email": "",
"date_joined": "2011-12-20 03:37:01"
}
},
{
"pk": 1016,
"model": "auth.user",
"fields": {
"username": "br@brianrosen.net",
"first_name": "",
"last_name": "",
"is_active": true,
"is_superuser": false,
"is_staff": false,
"last_login": "2011-11-16 17:55:41",
"groups": [],
"user_permissions": [],
"password": "",
"email": "",
"date_joined": "2011-11-16 17:55:41"
}
},
{
"pk": 700,
"model": "auth.user",
"fields": {
"username": "gabor.bajko@nokia.com",
"first_name": "",
"last_name": "",
"is_active": true,
"is_superuser": false,
"is_staff": false,
"last_login": "2011-09-09 10:07:39",
"groups": [],
"user_permissions": [],
"password": "",
"email": "",
"date_joined": "2011-09-09 10:07:39"
}
},
{
"pk": 1395,
"model": "auth.user",
"fields": {
"username": "wdec@cisco.com",
"first_name": "",
"last_name": "",
"is_active": true,
"is_superuser": false,
"is_staff": false,
"last_login": "2012-01-24 13:00:19",
"groups": [],
"user_permissions": [],
"password": "",
"email": "",
"date_joined": "2012-01-24 13:00:19"
}
},
{
"pk": 483,
"model": "auth.user",
"fields": {
"username": "matthew.bocci@alcatel.co.uk",
"first_name": "",
"last_name": "",
"is_active": true,
"is_superuser": false,
"is_staff": false,
"last_login": "2012-01-13 09:12:04",
"groups": [],
"user_permissions": [],
"password": "",
"email": "",
"date_joined": "2010-07-19 07:16:42"
}
},
{
"pk": 986,
"model": "auth.user",
"fields": {
"username": "bob.hinden@nokia.com",
"first_name": "",
"last_name": "",
"is_active": true,
"is_superuser": false,
"is_staff": false,
"last_login": "2011-11-14 03:19:35",
"groups": [],
"user_permissions": [],
"password": "",
"email": "",
"date_joined": "2011-11-14 03:08:01"
}
},
{
"pk": 1066,
"model": "auth.user",
"fields": {
"username": "brian@innovationslab.net",
"first_name": "",
"last_name": "",
"is_active": true,
"is_superuser": false,
"is_staff": false,
"last_login": "2011-11-28 11:00:16",
"groups": [],
"user_permissions": [],
"password": "",
"email": "",
"date_joined": "2011-11-28 11:00:16"
}
}
]

File diff suppressed because it is too large Load diff

387
ietf/secr/drafts/forms.py Normal file
View file

@ -0,0 +1,387 @@
from django import forms
from django.forms.formsets import BaseFormSet
from ietf.doc.models import *
from ietf.name.models import IntendedStdLevelName
from ietf.group.models import Group
from ietf.secr.utils.ams_utils import get_base, get_revision
from ietf.secr.groups.forms import RoleForm, get_person
import datetime
import re
from os.path import splitext
# ---------------------------------------------
# Select Choices
# ---------------------------------------------
WITHDRAW_CHOICES = (('ietf','Withdraw by IETF'),('author','Withdraw by Author'))
# ---------------------------------------------
# Custom Fields
# ---------------------------------------------
class DocumentField(forms.FileField):
'''A validating document upload field'''
def __init__(self, unique=False, *args, **kwargs):
self.extension = kwargs.pop('extension')
self.filename = kwargs.pop('filename')
self.rev = kwargs.pop('rev')
super(DocumentField, self).__init__(*args, **kwargs)
def clean(self, data, initial=None):
file = super(DocumentField, self).clean(data,initial)
if file:
# validate general file format
m = re.search(r'.*-\d{2}\.(txt|pdf|ps|xml)', file.name)
if not m:
raise forms.ValidationError('File name must be in the form base-NN.[txt|pdf|ps|xml]')
# ensure file extension is correct
base,ext = os.path.splitext(file.name)
if ext != self.extension:
raise forms.ValidationError('Incorrect file extension: %s' % ext)
# if this isn't a brand new submission we need to do some extra validations
if self.filename:
# validate filename
if base[:-3] != self.filename:
raise forms.ValidationError, "Filename: %s doesn't match Draft filename." % base[:-3]
# validate revision
next_revision = str(int(self.rev)+1).zfill(2)
if base[-2:] != next_revision:
raise forms.ValidationError, "Expected revision # %s" % (next_revision)
return file
class GroupModelChoiceField(forms.ModelChoiceField):
'''
Custom ModelChoiceField sets queryset to include all active workgroups and the
individual submission group, none. Displays group acronyms as choices. Call it without the
queryset argument, for example:
group = GroupModelChoiceField(required=True)
'''
def __init__(self, *args, **kwargs):
kwargs['queryset'] = Group.objects.filter(type__in=('wg','individ'),state__in=('bof','proposed','active')).order_by('acronym')
super(GroupModelChoiceField, self).__init__(*args, **kwargs)
def label_from_instance(self, obj):
return obj.acronym
class AliasModelChoiceField(forms.ModelChoiceField):
'''
Custom ModelChoiceField, just uses Alias name in the select choices as opposed to the
more confusing alias -> doc format used by DocAlias.__unicode__
'''
def label_from_instance(self, obj):
return obj.name
# ---------------------------------------------
# Forms
# ---------------------------------------------
class AddModelForm(forms.ModelForm):
start_date = forms.DateField()
group = GroupModelChoiceField(required=True,help_text='Use group "none" for Individual Submissions')
class Meta:
model = Document
fields = ('title','group','stream','start_date','pages','abstract','internal_comments')
# use this method to set attrs which keeps other meta info from model.
def __init__(self, *args, **kwargs):
super(AddModelForm, self).__init__(*args, **kwargs)
self.fields['title'].label='Document Name'
self.fields['title'].widget=forms.Textarea()
self.fields['start_date'].initial=datetime.date.today
self.fields['pages'].label='Number of Pages'
self.fields['internal_comments'].label='Comments'
class AuthorForm(forms.Form):
'''
The generic javascript for populating the email list based on the name selected expects to
see an id_email field
'''
person = forms.CharField(max_length=50,widget=forms.TextInput(attrs={'class':'name-autocomplete'}),help_text="To see a list of people type the first name, or last name, or both.")
email = forms.CharField(widget=forms.Select(),help_text="Select an email")
# check for id within parenthesis to ensure name was selected from the list
def clean_person(self):
person = self.cleaned_data.get('person', '')
m = re.search(r'(\d+)', person)
if person and not m:
raise forms.ValidationError("You must select an entry from the list!")
# return person object
return get_person(person)
# check that email exists and return the Email object
def clean_email(self):
email = self.cleaned_data['email']
try:
obj = Email.objects.get(address=email)
except Email.ObjectDoesNoExist:
raise forms.ValidationError("Email address not found!")
# return email object
return obj
class EditModelForm(forms.ModelForm):
#expiration_date = forms.DateField(required=False)
state = forms.ModelChoiceField(queryset=State.objects.filter(type='draft'),empty_label=None)
iesg_state = forms.ModelChoiceField(queryset=State.objects.filter(type='draft-iesg'),required=False)
group = GroupModelChoiceField(required=True)
review_by_rfc_editor = forms.BooleanField(required=False)
shepherd = forms.CharField(max_length=100,widget=forms.TextInput(attrs={'class':'name-autocomplete'}),help_text="To see a list of people type the first name, or last name, or both.",required=False)
class Meta:
model = Document
fields = ('title','group','ad','shepherd','notify','stream','review_by_rfc_editor','name','rev','pages','intended_std_level','abstract','internal_comments')
# use this method to set attrs which keeps other meta info from model.
def __init__(self, *args, **kwargs):
super(EditModelForm, self).__init__(*args, **kwargs)
self.fields['ad'].queryset = Person.objects.filter(role__name='ad')
self.fields['title'].label='Document Name'
self.fields['title'].widget=forms.Textarea()
self.fields['rev'].widget.attrs['size'] = 2
self.fields['abstract'].widget.attrs['cols'] = 72
self.initial['state'] = self.instance.get_state()
self.initial['iesg_state'] = self.instance.get_state('draft-iesg')
if self.instance.shepherd:
self.initial['shepherd'] = "%s - (%s)" % (self.instance.shepherd.name, self.instance.shepherd.id)
# setup special fields
if self.instance:
# setup replaced
self.fields['review_by_rfc_editor'].initial = bool(self.instance.tags.filter(slug='rfc-rev'))
def save(self, force_insert=False, force_update=False, commit=True):
m = super(EditModelForm, self).save(commit=False)
state = self.cleaned_data['state']
iesg_state = self.cleaned_data['iesg_state']
if 'state' in self.changed_data:
m.set_state(state)
# note we're not sending notices here, is this desired
if 'iesg_state' in self.changed_data:
if iesg_state == None:
m.unset_state('draft-iesg')
else:
m.set_state(iesg_state)
if 'review_by_rfc_editor' in self.changed_data:
if self.cleaned_data.get('review_by_rfc_editor',''):
m.tags.add('rfc-rev')
else:
m.tags.remove('rfc-rev')
m.time = datetime.datetime.now()
# handle replaced by
if commit:
m.save()
return m
# field must contain filename of existing draft
def clean_replaced_by(self):
name = self.cleaned_data.get('replaced_by', '')
if name and not InternetDraft.objects.filter(filename=name):
raise forms.ValidationError("ERROR: Draft does not exist")
return name
# check for id within parenthesis to ensure name was selected from the list
def clean_shepherd(self):
person = self.cleaned_data.get('shepherd', '')
m = re.search(r'(\d+)', person)
if person and not m:
raise forms.ValidationError("You must select an entry from the list!")
# return person object
return get_person(person)
def clean(self):
super(EditModelForm, self).clean()
cleaned_data = self.cleaned_data
"""
expiration_date = cleaned_data.get('expiration_date','')
status = cleaned_data.get('status','')
replaced = cleaned_data.get('replaced',False)
replaced_by = cleaned_data.get('replaced_by','')
replaced_status_object = IDStatus.objects.get(status_id=5)
expired_status_object = IDStatus.objects.get(status_id=2)
# this condition seems to be valid
#if expiration_date and status != expired_status_object:
# raise forms.ValidationError('Expiration Date set but status is %s' % (status))
if status == expired_status_object and not expiration_date:
raise forms.ValidationError('Status is Expired but Expirated Date is not set')
if replaced and status != replaced_status_object:
raise forms.ValidationError('You have checked Replaced but status is %s' % (status))
if replaced and not replaced_by:
raise forms.ValidationError('You have checked Replaced but Replaced By field is empty')
"""
return cleaned_data
class EmailForm(forms.Form):
# max_lengths come from db limits, cc is not limited
to = forms.CharField(max_length=255)
cc = forms.CharField(required=False)
subject = forms.CharField(max_length=255)
body = forms.CharField(widget=forms.Textarea())
class ExtendForm(forms.Form):
expiration_date = forms.DateField()
class ReplaceForm(forms.Form):
replaced = AliasModelChoiceField(DocAlias.objects.none(),empty_label=None,help_text='This document may have more than one alias. Be sure to select the correct alias to replace.')
replaced_by = forms.CharField(max_length=100,help_text='Enter the filename of the Draft which replaces this one.')
def __init__(self, *args, **kwargs):
self.draft = kwargs.pop('draft')
super(ReplaceForm, self).__init__(*args, **kwargs)
self.fields['replaced'].queryset = DocAlias.objects.filter(document=self.draft)
# field must contain filename of existing draft
def clean_replaced_by(self):
name = self.cleaned_data.get('replaced_by', '')
try:
doc = Document.objects.get(name=name)
except Document.DoesNotExist:
raise forms.ValidationError("ERROR: Draft does not exist: %s" % name)
if name == self.draft.name:
raise forms.ValidationError("ERROR: A draft can't replace itself")
return doc
class BaseRevisionModelForm(forms.ModelForm):
class Meta:
model = Document
fields = ('title','pages','abstract')
class RevisionModelForm(forms.ModelForm):
class Meta:
model = Document
fields = ('title','pages','abstract')
# use this method to set attrs which keeps other meta info from model.
def __init__(self, *args, **kwargs):
super(RevisionModelForm, self).__init__(*args, **kwargs)
self.fields['title'].label='Document Name'
self.fields['title'].widget=forms.Textarea()
self.fields['pages'].label='Number of Pages'
class RfcModelForm(forms.ModelForm):
rfc_number = forms.IntegerField()
rfc_published_date = forms.DateField(initial=datetime.datetime.now)
group = GroupModelChoiceField(required=True)
class Meta:
model = Document
fields = ('title','group','pages','std_level','internal_comments')
# use this method to set attrs which keeps other meta info from model.
def __init__(self, *args, **kwargs):
super(RfcModelForm, self).__init__(*args, **kwargs)
self.fields['title'].widget = forms.Textarea()
self.fields['std_level'].required = True
def save(self, force_insert=False, force_update=False, commit=True):
obj = super(RfcModelForm, self).save(commit=False)
# create DocAlias
DocAlias.objects.create(document=self.instance,name="rfc%d" % self.cleaned_data['rfc_number'])
if commit:
obj.save()
return obj
def clean_rfc_number(self):
rfc_number = self.cleaned_data['rfc_number']
if DocAlias.objects.filter(name='rfc' + str(rfc_number)):
raise forms.ValidationError("RFC %d already exists" % rfc_number)
return rfc_number
class RfcObsoletesForm(forms.Form):
relation = forms.ModelChoiceField(queryset=DocRelationshipName.objects.filter(slug__in=('updates','obs')),required=False)
rfc = forms.IntegerField(required=False)
# ensure that RFC exists
def clean_rfc(self):
rfc = self.cleaned_data.get('rfc','')
if rfc:
if not Document.objects.filter(docalias__name="rfc%s" % rfc):
raise forms.ValidationError("RFC does not exist")
return rfc
def clean(self):
super(RfcObsoletesForm, self).clean()
cleaned_data = self.cleaned_data
relation = cleaned_data.get('relation','')
rfc = cleaned_data.get('rfc','')
if (relation and not rfc) or (rfc and not relation):
raise forms.ValidationError('You must select a relation and enter RFC #')
return cleaned_data
class SearchForm(forms.Form):
intended_std_level = forms.ModelChoiceField(queryset=IntendedStdLevelName.objects,label="Intended Status",required=False)
document_title = forms.CharField(max_length=80,label='Document Title',required=False)
group = forms.CharField(max_length=12,required=False)
filename = forms.CharField(max_length=80,required=False)
state = forms.ModelChoiceField(queryset=State.objects.filter(type='draft'),required=False)
revision_date_start = forms.DateField(label='Revision Date (start)',required=False)
revision_date_end = forms.DateField(label='Revision Date (end)',required=False)
class UploadForm(forms.Form):
txt = DocumentField(label=u'.txt format', required=True,extension='.txt',filename=None,rev=None)
xml = DocumentField(label=u'.xml format', required=False,extension='.xml',filename=None,rev=None)
pdf = DocumentField(label=u'.pdf format', required=False,extension='.pdf',filename=None,rev=None)
ps = DocumentField(label=u'.ps format', required=False,extension='.ps',filename=None,rev=None)
def __init__(self, *args, **kwargs):
if 'draft' in kwargs:
self.draft = kwargs.pop('draft')
else:
self.draft = None
super(UploadForm, self).__init__(*args, **kwargs)
if self.draft:
for field in self.fields.itervalues():
field.filename = self.draft.name
field.rev = self.draft.rev
def clean(self):
# Checks that all files have the same base
if any(self.errors):
# Don't bother validating unless each field is valid on its own
return
txt = self.cleaned_data['txt']
xml = self.cleaned_data['xml']
pdf = self.cleaned_data['pdf']
ps = self.cleaned_data['ps']
# we only need to do these validations for new drafts
if not self.draft:
names = []
for file in (txt,xml,pdf,ps):
if file:
base = splitext(file.name)[0]
if base not in names:
names.append(base)
if len(names) > 1:
raise forms.ValidationError, "All files must have the same base name"
# ensure that the basename is unique
base = splitext(txt.name)[0]
if Document.objects.filter(name=base[:-3]):
raise forms.ValidationError, "This doucment filename already exists: %s" % base[:-3]
# ensure that rev is 00
if base[-2:] != '00':
raise forms.ValidationError, "New Drafts must start with 00 revision number."
return self.cleaned_data
class WithdrawForm(forms.Form):
type = forms.CharField(widget=forms.Select(choices=WITHDRAW_CHOICES),help_text='Select which type of withdraw to perform')

View file

@ -0,0 +1 @@
from django.db import models

View file

@ -0,0 +1,3 @@
# see add_id5.cfm ~400 for email To addresses
# see generateNotification.cfm

View file

@ -0,0 +1,9 @@
from ietf import settings
from django.core import management
management.setup_environ(settings)
from ietf.secr.drafts.views import report_id_activity
import sys
print report_id_activity(sys.argv[1], sys.argv[2]),

View file

@ -0,0 +1,9 @@
from ietf import settings
from django.core import management
management.setup_environ(settings)
from ietf.secr.drafts.views import report_progress_report
import sys
print report_progress_report(sys.argv[1], sys.argv[2]),

27
ietf/secr/drafts/tests.py Normal file
View file

@ -0,0 +1,27 @@
from django.core.urlresolvers import reverse
from django.test import TestCase
from ietf.doc.models import Document
from ietf.utils.test_data import make_test_data
from pyquery import PyQuery
SECR_USER='secretary'
class MainTestCase(TestCase):
fixtures = ['names']
def test_main(self):
"Main Test"
draft = make_test_data()
url = reverse('drafts')
response = self.client.get(url, REMOTE_USER=SECR_USER)
self.assertEquals(response.status_code, 200)
def test_view(self):
"View Test"
draft = make_test_data()
drafts = Document.objects.filter(type='draft')
url = reverse('drafts_view', kwargs={'id':drafts[0].name})
response = self.client.get(url, REMOTE_USER=SECR_USER)
self.assertEquals(response.status_code, 200)

25
ietf/secr/drafts/urls.py Normal file
View file

@ -0,0 +1,25 @@
from django.conf.urls.defaults import *
urlpatterns = patterns('ietf.secr.drafts.views',
url(r'^$', 'search', name='drafts'),
url(r'^add/$', 'add', name='drafts_add'),
url(r'^approvals/$', 'approvals', name='drafts_approvals'),
url(r'^dates/$', 'dates', name='drafts_dates'),
url(r'^nudge-report/$', 'nudge_report', name='drafts_nudge_report'),
url(r'^(?P<id>[A-Za-z0-9._\-\+]+)/$', 'view', name='drafts_view'),
url(r'^(?P<id>[A-Za-z0-9._\-\+]+)/abstract/$', 'abstract', name='drafts_abstract'),
url(r'^(?P<id>[A-Za-z0-9._\-\+]+)/announce/$', 'announce', name='drafts_announce'),
url(r'^(?P<id>[A-Za-z0-9._\-\+]+)/authors/$', 'authors', name='drafts_authors'),
url(r'^(?P<id>[A-Za-z0-9._\-\+]+)/author_delete/(?P<oid>\d{1,6})$',
'author_delete', name='drafts_author_delete'),
url(r'^(?P<id>[A-Za-z0-9._\-\+]+)/confirm/$', 'confirm', name='drafts_confirm'),
url(r'^(?P<id>[A-Za-z0-9._\-\+]+)/edit/$', 'edit', name='drafts_edit'),
url(r'^(?P<id>[A-Za-z0-9._\-\+]+)/extend/$', 'extend', name='drafts_extend'),
url(r'^(?P<id>[A-Za-z0-9._\-\+]+)/email/$', 'email', name='drafts_email'),
url(r'^(?P<id>[A-Za-z0-9._\-\+]+)/makerfc/$', 'makerfc', name='drafts_makerfc'),
url(r'^(?P<id>[A-Za-z0-9._\-\+]+)/replace/$', 'replace', name='drafts_replace'),
url(r'^(?P<id>[A-Za-z0-9._\-\+]+)/resurrect/$', 'resurrect', name='drafts_resurrect'),
url(r'^(?P<id>[A-Za-z0-9._\-\+]+)/revision/$', 'revision', name='drafts_revision'),
url(r'^(?P<id>[A-Za-z0-9._\-\+]+)/update/$', 'update', name='drafts_update'),
url(r'^(?P<id>[A-Za-z0-9._\-\+]+)/withdraw/$', 'withdraw', name='drafts_withdraw'),
)

1284
ietf/secr/drafts/views.py Normal file

File diff suppressed because it is too large Load diff

View file

174
ietf/secr/groups/forms.py Normal file
View file

@ -0,0 +1,174 @@
from django import forms
from django.db.models import Q
from ietf.group.models import Group, GroupMilestone, Role
from ietf.name.models import GroupStateName, GroupTypeName, RoleName
from ietf.person.models import Person, Email
from ietf.secr.areas.forms import AWPForm
import re
# ---------------------------------------------
# Select Choices
# ---------------------------------------------
SEARCH_MEETING_CHOICES = (('',''),('NO','NO'),('YES','YES'))
# ---------------------------------------------
# Functions
# ---------------------------------------------
def get_person(name):
'''
This function takes a string which is in the name autocomplete format "name - (id)"
and returns a person object
'''
match = re.search(r'\((\d+)\)', name)
if not match:
return None
id = match.group(1)
try:
person = Person.objects.get(id=id)
except (Person.ObjectDoesNoExist, Person.MultipleObjectsReturned):
return None
return person
# ---------------------------------------------
# Forms
# ---------------------------------------------
class DescriptionForm (forms.Form):
description = forms.CharField(widget=forms.Textarea(attrs={'rows':'20'}),required=True)
class GroupMilestoneForm(forms.ModelForm):
class Meta:
model = GroupMilestone
exclude = ('done')
# use this method to set attrs which keeps other meta info from model.
def __init__(self, *args, **kwargs):
super(GroupMilestoneForm, self).__init__(*args, **kwargs)
self.fields['desc'].widget=forms.TextInput(attrs={'size':'60'})
self.fields['expected_due_date'].widget.attrs['size'] = 10
self.fields['done_date'].widget.attrs['size'] = 10
# override save. set done=True if done_date set
def save(self, force_insert=False, force_update=False, commit=True):
m = super(GroupMilestoneForm, self).save(commit=False)
if 'done_date' in self.changed_data:
if self.cleaned_data.get('done_date',''):
m.done = True
else:
m.done = False
if commit:
m.save()
return m
class GroupModelForm(forms.ModelForm):
type = forms.ModelChoiceField(queryset=GroupTypeName.objects.all(),empty_label=None)
parent = forms.ModelChoiceField(queryset=Group.objects.filter(Q(type='area',state='active')|Q(acronym='irtf')),required=False)
ad = forms.ModelChoiceField(queryset=Person.objects.filter(role__name='ad',role__group__state='active'),required=False)
state = forms.ModelChoiceField(queryset=GroupStateName.objects.exclude(slug__in=('dormant','unknown')),empty_label=None)
class Meta:
model = Group
fields = ('acronym','name','type','state','parent','ad','list_email','list_subscribe','list_archive','comments')
def __init__(self, *args, **kwargs):
super(GroupModelForm, self).__init__(*args, **kwargs)
self.fields['list_email'].label = 'List Email'
self.fields['list_subscribe'].label = 'List Subscribe'
self.fields['list_archive'].label = 'List Archive'
self.fields['ad'].label = 'Area Director'
self.fields['comments'].widget.attrs['rows'] = 3
self.fields['parent'].label = 'Area'
def clean_parent(self):
parent = self.cleaned_data['parent']
type = self.cleaned_data['type']
if type.slug in ('ag','wg','rg') and not parent:
raise forms.ValidationError("This field is required.")
return parent
def clean(self):
if any(self.errors):
return self.cleaned_data
super(GroupModelForm, self).clean()
type = self.cleaned_data['type']
parent = self.cleaned_data['parent']
state = self.cleaned_data['state']
irtf_area = Group.objects.get(acronym='irtf')
# ensure proper parent for group type
if type.slug == 'rg' and parent != irtf_area:
raise forms.ValidationError('The Area for a research group must be %s' % irtf_area)
# an RG can't be proposed
if type.slug == 'rg' and state.slug not in ('active','conclude'):
raise forms.ValidationError('You must choose "active" or "concluded" for research group state')
return self.cleaned_data
class RoleForm(forms.Form):
name = forms.ModelChoiceField(RoleName.objects.filter(slug__in=('chair','editor','secr','techadv')),empty_label=None)
person = forms.CharField(max_length=50,widget=forms.TextInput(attrs={'class':'name-autocomplete'}),help_text="To see a list of people type the first name, or last name, or both.")
email = forms.CharField(widget=forms.Select(),help_text="Select an email")
group_acronym = forms.CharField(widget=forms.HiddenInput(),required=False)
def __init__(self, *args, **kwargs):
self.group = kwargs.pop('group')
super(RoleForm, self).__init__(*args,**kwargs)
# this form is re-used in roles app, use different roles in select
if self.group.type.slug not in ('wg','rg'):
self.fields['name'].queryset = RoleName.objects.all()
# check for id within parenthesis to ensure name was selected from the list
def clean_person(self):
person = self.cleaned_data.get('person', '')
m = re.search(r'(\d+)', person)
if person and not m:
raise forms.ValidationError("You must select an entry from the list!")
# return person object
return get_person(person)
# check that email exists and return the Email object
def clean_email(self):
email = self.cleaned_data['email']
try:
obj = Email.objects.get(address=email)
except Email.ObjectDoesNoExist:
raise forms.ValidationError("Email address not found!")
# return email object
return obj
def clean(self):
# here we abort if there are any errors with individual fields
# One predictable problem is that the user types a name rather then
# selecting one from the list, as instructed to do. We need to abort
# so the error is displayed before trying to call get_person()
if any(self.errors):
# Don't bother validating the formset unless each form is valid on its own
return
super(RoleForm, self).clean()
cleaned_data = self.cleaned_data
person = cleaned_data['person']
email = cleaned_data['email']
name = cleaned_data['name']
if Role.objects.filter(name=name,group=self.group,person=person,email=email):
raise forms.ValidationError('ERROR: This is a duplicate entry')
return cleaned_data
class SearchForm(forms.Form):
group_acronym = forms.CharField(max_length=12,required=False)
group_name = forms.CharField(max_length=80,required=False)
primary_area = forms.ModelChoiceField(queryset=Group.objects.filter(type='area',state='active'),required=False)
type = forms.ModelChoiceField(queryset=GroupTypeName.objects.all(),required=False)
meeting_scheduled = forms.CharField(widget=forms.Select(choices=SEARCH_MEETING_CHOICES),required=False)
state = forms.ModelChoiceField(queryset=GroupStateName.objects.exclude(slug__in=('dormant','unknown')),required=False)

View file

@ -0,0 +1 @@
from django.db import models

138
ietf/secr/groups/tests.py Normal file
View file

@ -0,0 +1,138 @@
from django.core.urlresolvers import reverse
from django.test import TestCase
from ietf.group.models import Group
from ietf.person.models import Person
from ietf.utils.test_data import make_test_data
SECR_USER='secretary'
class GroupsTest(TestCase):
fixtures = ['names']
"""
fixtures = [ 'acronym.json',
'area.json',
'areadirector',
'areagroup.json',
'goalmilestone',
'iesglogin.json',
'ietfwg',
'personororginfo.json',
'wgchair.json',
'wgstatus.json',
'wgtype.json' ]
"""
# ------- Test Search -------- #
def test_search(self):
"Test Search"
draft = make_test_data()
group = Group.objects.all()[0]
url = reverse('groups_search')
post_data = {'group_acronym':group.acronym,'submit':'Search'}
response = self.client.post(url,post_data,follow=True, REMOTE_USER=SECR_USER)
#assert False, response.content
self.assertEquals(response.status_code, 200)
self.failUnless(group.acronym in response.content)
# ------- Test Add -------- #
def test_add_button(self):
url = reverse('groups_search')
target = reverse('groups_add')
post_data = {'submit':'Add'}
response = self.client.post(url,post_data,follow=True, REMOTE_USER=SECR_USER)
self.assertRedirects(response, target)
def test_add_group_invalid(self):
url = reverse('groups_add')
post_data = {'acronym':'test',
'type':'wg',
'awp-TOTAL_FORMS':'2',
'awp-INITIAL_FORMS':'0',
'submit':'Save'}
response = self.client.post(url,post_data, REMOTE_USER=SECR_USER)
self.assertEquals(response.status_code, 200)
self.failUnless('This field is required' in response.content)
def test_add_group_dupe(self):
draft = make_test_data()
group = Group.objects.all()[0]
area = Group.objects.filter(type='area')[0]
url = reverse('groups_add')
post_data = {'acronym':group.acronym,
'name':'Test Group',
'state':'active',
'type':'wg',
'parent':area.id,
'awp-TOTAL_FORMS':'2',
'awp-INITIAL_FORMS':'0',
'submit':'Save'}
response = self.client.post(url,post_data, REMOTE_USER=SECR_USER)
#print response.content
self.assertEquals(response.status_code, 200)
self.failUnless('Group with this Acronym already exists' in response.content)
def test_add_group_success(self):
draft = make_test_data()
area = Group.objects.filter(type='area')[0]
url = reverse('groups_add')
post_data = {'acronym':'test',
'name':'Test Group',
'type':'wg',
'status':'active',
'parent':area.id,
'awp-TOTAL_FORMS':'2',
'awp-INITIAL_FORMS':'0',
'submit':'Save'}
response = self.client.post(url,post_data, REMOTE_USER=SECR_USER)
self.assertEquals(response.status_code, 200)
# ------- Test View -------- #
def test_view(self):
draft = make_test_data()
group = Group.objects.all()[0]
url = reverse('groups_view', kwargs={'acronym':group.acronym})
response = self.client.get(url)
self.assertEquals(response.status_code, 200)
# ------- Test Edit -------- #
def test_edit_valid(self):
draft = make_test_data()
group = Group.objects.filter(type='wg')[0]
area = Group.objects.filter(type='area')[0]
url = reverse('groups_edit', kwargs={'acronym':group.acronym})
target = reverse('groups_view', kwargs={'acronym':group.acronym})
post_data = {'acronym':group.acronym,
'name':group.name,
'type':'wg',
'state':group.state_id,
'parent':area.id,
'ad':3,
'groupurl_set-TOTAL_FORMS':'2',
'groupurl_set-INITIAL_FORMS':'0',
'submit':'Save'}
response = self.client.post(url,post_data,follow=True, REMOTE_USER=SECR_USER)
self.assertRedirects(response, target)
self.failUnless('changed successfully' in response.content)
# ------- Test People -------- #
def test_people_delete(self):
draft = make_test_data()
group = Group.objects.filter(type='wg')[0]
role = group.role_set.all()[0]
url = reverse('groups_delete_role', kwargs={'acronym':group.acronym,'id':role.id})
target = reverse('groups_people', kwargs={'acronym':group.acronym})
response = self.client.get(url,follow=True, REMOTE_USER=SECR_USER)
self.assertRedirects(response, target)
self.failUnless('deleted successfully' in response.content)
def test_people_add(self):
draft = make_test_data()
person = Person.objects.get(name='Aread Irector')
group = Group.objects.filter(type='wg')[0]
url = reverse('groups_people', kwargs={'acronym':group.acronym})
post_data = {'name':'chair',
'person':'Joe Smith - (%s)' % person.id,
'email':person.email_set.all()[0].address,
'submit':'Add'}
response = self.client.post(url,post_data,follow=True, REMOTE_USER=SECR_USER)
self.assertRedirects(response, url)
self.failUnless('added successfully' in response.content)

View file

@ -0,0 +1,15 @@
from ietf import settings
from django.core import management
management.setup_environ(settings)
from ietf.group.models import *
import sys
def output_charter(group):
report = render_to_string('groups/text_charter.txt', context)
return report
group = Group.objects.get(acronym='alto')
output_charter(group)

16
ietf/secr/groups/urls.py Normal file
View file

@ -0,0 +1,16 @@
from django.conf.urls.defaults import *
urlpatterns = patterns('ietf.secr.groups.views',
url(r'^$', 'search', name='groups'),
url(r'^add/$', 'add', name='groups_add'),
url(r'^blue-dot-report/$', 'blue_dot', name='groups_blue_dot'),
url(r'^search/$', 'search', name='groups_search'),
#(r'^ajax/get_ads/$', 'get_ads'),
url(r'^(?P<acronym>[A-Za-z0-9_\-\+\.]+)/$', 'view', name='groups_view'),
url(r'^(?P<acronym>[A-Za-z0-9_\-\+\.]+)/delete/(?P<id>\d{1,6})/$', 'delete_role', name='groups_delete_role'),
url(r'^(?P<acronym>[A-Za-z0-9_\-\+\.]+)/charter/$', 'charter', name='groups_charter'),
url(r'^(?P<acronym>[A-Za-z0-9_\-\+\.]+)/edit/$', 'edit', name='groups_edit'),
url(r'^(?P<acronym>[A-Za-z0-9_\-\+\.]+)/gm/$', 'view_gm', name='groups_view_gm'),
url(r'^(?P<acronym>[A-Za-z0-9_\-\+\.]+)/gm/edit/$', 'edit_gm', name='groups_edit_gm'),
url(r'^(?P<acronym>[A-Za-z0-9_\-\+\.]+)/people/$', 'people', name='groups_people'),
)

492
ietf/secr/groups/views.py Normal file
View file

@ -0,0 +1,492 @@
from django.conf import settings
from django.contrib import messages
from django.core.urlresolvers import reverse
from django.db.models import get_model
from django.core.exceptions import ObjectDoesNotExist
from django.forms.formsets import formset_factory
from django.forms.models import inlineformset_factory
from django.http import HttpResponseRedirect, HttpResponse
from django.shortcuts import render_to_response, get_object_or_404
from django.template import RequestContext
from django.template.loader import render_to_string
from django.utils import simplejson
#from sec.utils.group import get_charter_text
from ietf.secr.utils.meeting import get_current_meeting
from ietf.group.models import ChangeStateGroupEvent, GroupEvent, GroupURL, Role
from ietf.group.utils import save_group_in_history, get_charter_text
from ietf.person.name import name_parts
from ietf.wginfo.views import fill_in_charter_info
from forms import *
import os
import datetime
# -------------------------------------------------
# Helper Functions
# -------------------------------------------------
def add_legacy_fields(group):
'''
This function takes a Group object as input and adds legacy attributes:
start_date,proposed_date,concluded_date,meeting_scheduled
'''
# it's possible there could be multiple records of a certain type in which case
# we just return the latest record
query = GroupEvent.objects.filter(group=group, type="changed_state").order_by('time')
proposed = query.filter(changestategroupevent__state="proposed")
meeting = get_current_meeting()
if proposed:
group.proposed_date = proposed[0].time
active = query.filter(changestategroupevent__state="active")
if active:
group.start_date = active[0].time
concluded = query.filter(changestategroupevent__state="conclude")
if concluded:
group.concluded_date = concluded[0].time
if group.session_set.filter(meeting__number=meeting.number):
group.meeting_scheduled = 'YES'
else:
group.meeting_scheduled = 'NO'
group.chairs = group.role_set.filter(name="chair")
group.techadvisors = group.role_set.filter(name="techadv")
group.editors = group.role_set.filter(name="editor")
group.secretaries = group.role_set.filter(name="secretaries")
#fill_in_charter_info(group)
#--------------------------------------------------
# AJAX Functions
# -------------------------------------------------
'''
def get_ads(request):
""" AJAX function which takes a URL parameter, "area" and returns the area directors
in the form of a list of dictionaries with "id" and "value" keys(in json format).
Used to populate select options.
"""
results=[]
area = request.GET.get('area','')
qs = AreaDirector.objects.filter(area=area)
for item in qs:
d = {'id': item.id, 'value': item.person.first_name + ' ' + item.person.last_name}
results.append(d)
return HttpResponse(simplejson.dumps(results), mimetype='application/javascript')
'''
# -------------------------------------------------
# Standard View Functions
# -------------------------------------------------
def add(request):
'''
Add a new IETF or IRTF Group
**Templates:**
* ``groups/add.html``
**Template Variables:**
* form, awp_formset
'''
AWPFormSet = inlineformset_factory(Group, GroupURL, form=AWPForm, max_num=2, can_delete=False)
if request.method == 'POST':
button_text = request.POST.get('submit', '')
if button_text == 'Cancel':
url = reverse('groups')
return HttpResponseRedirect(url)
form = GroupModelForm(request.POST)
awp_formset = AWPFormSet(request.POST, prefix='awp')
if form.is_valid() and awp_formset.is_valid():
group = form.save()
for form in awp_formset.forms:
if form.has_changed():
awp = form.save(commit=False)
awp.group = group
awp.save()
# create GroupEvent(s)
# always create started event
ChangeStateGroupEvent.objects.create(group=group,
type='changed_state',
by=request.user.get_profile(),
state=group.state,
desc='Started group')
messages.success(request, 'The Group was created successfully!')
url = reverse('groups_view', kwargs={'acronym':group.acronym})
return HttpResponseRedirect(url)
else:
form = GroupModelForm(initial={'state':'active','type':'wg'})
awp_formset = AWPFormSet(prefix='awp')
return render_to_response('groups/add.html', {
'form': form,
'awp_formset': awp_formset},
RequestContext(request, {}),
)
def blue_dot(request):
'''
This is a report view. It returns a text/plain listing of chairs for active and bof groups.
'''
people = Person.objects.filter(role__name__slug='chair',
role__group__type='wg',
role__group__state__slug__in=('active','bof')).distinct()
chairs = []
for person in people:
parts = person.name_parts()
groups = [ r.group.acronym for r in person.role_set.filter(name__slug='chair',
group__type='wg',
group__state__slug__in=('active','bof')) ]
entry = {'name':'%s, %s' % (parts[3], parts[1]),
'groups': ', '.join(groups)}
chairs.append(entry)
# sort the list
sorted_chairs = sorted(chairs, key = lambda a: a['name'])
return render_to_response('groups/blue_dot_report.txt', {
'chairs':sorted_chairs},
RequestContext(request, {}), mimetype="text/plain",
)
def charter(request, acronym):
"""
View Group Charter
**Templates:**
* ``groups/charter.html``
**Template Variables:**
* group, charter_text
"""
group = get_object_or_404(Group, acronym=acronym)
# TODO: get_charter_text() should be updated to return None
if group.charter:
charter_text = get_charter_text(group)
else:
charter_text = ''
return render_to_response('groups/charter.html', {
'group': group,
'charter_text': charter_text},
RequestContext(request, {}),
)
def delete_role(request, acronym, id):
"""
Handle deleting roles for groups (chair, editor, advisor, secretary)
**Templates:**
* none
Redirects to people page on success.
"""
group = get_object_or_404(Group, acronym=acronym)
role = get_object_or_404(Role, id=id)
# save group
save_group_in_history(group)
role.delete()
messages.success(request, 'The entry was deleted successfully')
url = reverse('groups_people', kwargs={'acronym':acronym})
return HttpResponseRedirect(url)
def edit(request, acronym):
"""
Edit Group details
**Templates:**
* ``groups/edit.html``
**Template Variables:**
* group, form, awp_formset
"""
group = get_object_or_404(Group, acronym=acronym)
AWPFormSet = inlineformset_factory(Group, GroupURL, form=AWPForm, max_num=2)
if request.method == 'POST':
button_text = request.POST.get('submit', '')
if button_text == 'Cancel':
url = reverse('groups_view', kwargs={'acronym':acronym})
return HttpResponseRedirect(url)
form = GroupModelForm(request.POST, instance=group)
awp_formset = AWPFormSet(request.POST, instance=group)
if form.is_valid() and awp_formset.is_valid():
awp_formset.save()
if form.changed_data:
state = form.cleaned_data['state']
# save group
save_group_in_history(group)
form.save()
# create appropriate GroupEvent
if 'state' in form.changed_data:
if state.name == 'Active':
desc = 'Started group'
else:
desc = state.name + ' group'
ChangeStateGroupEvent.objects.create(group=group,
type='changed_state',
by=request.user.get_profile(),
state=state,
desc=desc)
form.changed_data.remove('state')
# if anything else was changed
if form.changed_data:
GroupEvent.objects.create(group=group,
type='info_changed',
by=request.user.get_profile(),
desc='Info Changed')
# if the acronym was changed we'll want to redirect using the new acronym below
if 'acronym' in form.changed_data:
acronym = form.cleaned_data['acronym']
messages.success(request, 'The Group was changed successfully')
url = reverse('groups_view', kwargs={'acronym':acronym})
return HttpResponseRedirect(url)
else:
form = GroupModelForm(instance=group)
awp_formset = AWPFormSet(instance=group)
return render_to_response('groups/edit.html', {
'group': group,
'awp_formset': awp_formset,
'form': form},
RequestContext(request, {}),
)
def edit_gm(request, acronym):
"""
Edit IETF Group Goal and Milestone details
**Templates:**
* ``groups/edit_gm.html``
**Template Variables:**
* group, formset
"""
group = get_object_or_404(Group, acronym=acronym)
GMFormset = inlineformset_factory(Group, GroupMilestone, form=GroupMilestoneForm, can_delete=True, extra=5)
if request.method == 'POST':
button_text = request.POST.get('submit', '')
if button_text == 'Cancel':
url = reverse('groups_view', kwargs={'acronym':acronym})
return HttpResponseRedirect(url)
formset = GMFormset(request.POST, instance=group, prefix='goalmilestone')
if formset.is_valid():
formset.save()
messages.success(request, 'The Goals Milestones were changed successfully')
url = reverse('groups_view', kwargs={'acronym':acronym})
return HttpResponseRedirect(url)
else:
formset = GMFormset(instance=group, prefix='goalmilestone')
return render_to_response('groups/edit_gm.html', {
'group': group,
'formset': formset},
RequestContext(request, {}),
)
def people(request, acronym):
"""
Edit Group Roles (Chairs, Secretary, etc)
**Templates:**
* ``groups/people.html``
**Template Variables:**
* form, group
"""
group = get_object_or_404(Group, acronym=acronym)
if request.method == 'POST':
# we need to pass group for form validation
form = RoleForm(request.POST,group=group)
if form.is_valid():
name = form.cleaned_data['name']
person = form.cleaned_data['person']
email = form.cleaned_data['email']
# save group
save_group_in_history(group)
Role.objects.create(name=name,
person=person,
email=email,
group=group)
messages.success(request, 'New %s added successfully!' % name)
url = reverse('groups_people', kwargs={'acronym':group.acronym})
return HttpResponseRedirect(url)
else:
form = RoleForm(initial={'name':'chair'},group=group)
return render_to_response('groups/people.html', {
'form':form,
'group':group},
RequestContext(request, {}),
)
def search(request):
"""
Search IETF Groups
**Templates:**
* ``groups/search.html``
**Template Variables:**
* form, results
"""
results = []
if request.method == 'POST':
form = SearchForm(request.POST)
if request.POST['submit'] == 'Add':
url = reverse('groups_add')
return HttpResponseRedirect(url)
if form.is_valid():
kwargs = {}
group_acronym = form.cleaned_data['group_acronym']
group_name = form.cleaned_data['group_name']
primary_area = form.cleaned_data['primary_area']
meeting_scheduled = form.cleaned_data['meeting_scheduled']
state = form.cleaned_data['state']
type = form.cleaned_data['type']
meeting = get_current_meeting()
# construct seach query
if group_acronym:
kwargs['acronym__istartswith'] = group_acronym
if group_name:
kwargs['name__istartswith'] = group_name
if primary_area:
kwargs['parent'] = primary_area
if state:
kwargs['state'] = state
if type:
kwargs['type'] = type
#else:
# kwargs['type__in'] = ('wg','rg','ietf','ag','sdo','team')
if meeting_scheduled == 'YES':
kwargs['session__meeting__number'] = meeting.number
# perform query
if kwargs:
if meeting_scheduled == 'NO':
qs = Group.objects.filter(**kwargs).exclude(session__meeting__number=meeting.number).distinct()
else:
qs = Group.objects.filter(**kwargs).distinct()
else:
qs = Group.objects.all()
results = qs.order_by('acronym')
# if there's just one result go straight to view
if len(results) == 1:
url = reverse('groups_view', kwargs={'acronym':results[0].acronym})
return HttpResponseRedirect(url)
# process GET argument to support link from area app
elif 'primary_area' in request.GET:
area = request.GET.get('primary_area','')
results = Group.objects.filter(parent__id=area,type='wg',state__in=('bof','active','proposed')).order_by('name')
form = SearchForm({'primary_area':area,'state':'','type':'wg'})
else:
form = SearchForm(initial={'state':'active','type':'wg'})
# loop through results and tack on meeting_scheduled because it is no longer an
# attribute of the meeting model
for result in results:
add_legacy_fields(result)
return render_to_response('groups/search.html', {
'results': results,
'form': form},
RequestContext(request, {}),
)
def view(request, acronym):
"""
View IETF Group details
**Templates:**
* ``groups/view.html``
**Template Variables:**
* group
"""
group = get_object_or_404(Group, acronym=acronym)
add_legacy_fields(group)
return render_to_response('groups/view.html', {
'group': group},
RequestContext(request, {}),
)
def view_gm(request, acronym):
"""
View IETF Group Goals and Milestones details
**Templates:**
* ``groups/view_gm.html``
**Template Variables:**
* group
"""
group = get_object_or_404(Group, acronym=acronym)
return render_to_response('groups/view_gm.html', {
'group': group},
RequestContext(request, {}),
)

View file

View file

334
ietf/secr/ipradmin/forms.py Normal file
View file

@ -0,0 +1,334 @@
from copy import deepcopy
from form_utils.forms import BetterModelForm
from django import forms
from django.utils.safestring import mark_safe
from django.forms.formsets import formset_factory
from django.utils import simplejson
from ietf.ipr.models import IprDetail, IprContact, LICENSE_CHOICES, IprRfc, IprDraft, IprUpdate, SELECT_CHOICES, IprDocAlias
from ietf.doc.models import DocAlias
from ietf.secr.utils.document import get_rfc_num
def mytest(val):
if val == '1':
return True
class IprContactForm(forms.ModelForm):
contact_type = forms.ChoiceField(widget=forms.HiddenInput, choices=IprContact.TYPE_CHOICES)
name = forms.CharField(required=False, max_length=255)
telephone = forms.CharField(required=False, max_length=25)
email = forms.CharField(required=False, max_length=255)
class Meta:
model = IprContact
fields = [ 'name', 'title', 'department', 'address1', 'address2', 'telephone', 'fax', 'email' ,
'contact_type',]
def clean_contact_type(self):
return str(self.cleaned_data['contact_type'])
@property
def _empty(self):
fields = deepcopy(self._meta.fields)
fields.remove("contact_type")
for field in fields:
if self.cleaned_data[field].strip():
return False
return True
def save(self, ipr_detail, *args, **kwargs):
#import ipdb; ipdb.set_trace()
if(self.cleaned_data['contact_type'] != 1 and self._empty):
return None
contact = super(IprContactForm, self).save(*args, commit=False, **kwargs)
contact.ipr = ipr_detail
contact.save()
return contact
IPRContactFormset = formset_factory(IprContactForm, extra=0)
class IprDetailForm(BetterModelForm):
IS_PENDING_CHOICES = DOCUMENT_SECTIONS_CHOICES = (
("0", "no"),
("1", "yes"))
title = forms.CharField(required=True)
updated = forms.IntegerField(
required=False, label='IPR ID that is updated by this IPR')
remove_old_ipr = forms.BooleanField(required=False, label='Remove old IPR')
rfc_num = forms.CharField(required=False, label='RFC Number', widget=forms.HiddenInput)
id_filename = forms.CharField(
max_length=512, required=False,
label='I-D Filename (draft-ietf...)',
widget=forms.HiddenInput)
#is_pending = forms.ChoiceField(choices=IS_PENDING_CHOICES,required=False, label="B. Does your disclosure relate to an unpublished pending patent application?", widget=forms.RadioSelect)
#is_pending = forms.BooleanField(required=False, label="B. Does your disclosure relate to an unpublished pending patent application?")
licensing_option = forms.ChoiceField(
widget=forms.RadioSelect, choices=LICENSE_CHOICES, required=False)
patents = forms.CharField(widget=forms.Textarea, required=False)
date_applied = forms.CharField(required=False)
country = forms.CharField(required=False)
submitted_date = forms.DateField(required=True)
#lic_opt_c_sub = forms.BooleanField(required=False,widget=forms.CheckboxInput)
'''
FIXME: (stalled - comply is bool in model)
comply = forms.MultipleChoiceField(
choices = (('YES', 'YES'), ('NO', 'NO'), ('N/A', 'N/A')),
widget = forms.RadioSelect()
)
'''
def clean_lic_opt_sub(self, val):
return int(val)
def clean(self):
#print self.data, "\n\n", self.cleaned_data
#import ipdb;ipdb.set_trace()
lic_opt = self.cleaned_data['licensing_option']
for num, ltr in (('1', 'a'), ('2', 'b'), ('3', 'c')):
opt_sub = 'lic_opt_'+ltr+'_sub'
self._meta.fields.append(opt_sub)
if lic_opt == num and opt_sub in self.data:
self.cleaned_data[opt_sub] = 1
else:
self.cleaned_data[opt_sub] = 0
#self.cleaned_data['lic_opt_a_sub'] = self.clean_lic_opt_sub(self.data['lic_opt_a_sub'])
return self.cleaned_data
def __init__(self, *args, **kwargs):
formtype = kwargs.get('formtype')
if formtype:
del kwargs['formtype']
super(IprDetailForm, self).__init__(*args, **kwargs)
self.fields['legacy_url_0'].label='Old IPR Url'
self.fields['title'].label='IPR Title'
self.fields['legacy_title_1'].label='Text for Update Link'
self.fields['legacy_url_1'].label='URL for Update Link'
self.fields['legacy_title_2'].label='Additional Old Title 2'
self.fields['legacy_url_2'].label='Additional Old URL 2'
self.fields['document_sections'].label='C. If an Internet-Draft or RFC includes multiple parts and it is not reasonably apparent which part of such Internet-Draft or RFC is alleged to be covered by the patent information disclosed in Section V(A) or V(B), it is helpful if the discloser identifies here the sections of the Internet-Draft or RFC that are alleged to be so covered.'
self.fields['patents'].label='Patent, Serial, Publication, Registration, or Application/File number(s)'
self.fields['date_applied'].label='Date(s) granted or applied for (YYYY-MM-DD)'
self.fields['comments'].label='Licensing information, comments, notes or URL for further information'
self.fields['lic_checkbox'].label='The individual submitting this template represents and warrants that all terms and conditions that must be satisfied for implementers of any covered IETF specification to obtain a license have been disclosed in this IPR disclosure statement.'
self.fields['third_party'].label='Third Party Notification?'
self.fields['generic'].label='Generic IPR?'
self.fields['comply'].label='Complies with RFC 3979?'
self.fields['is_pending'].label="B. Does your disclosure relate to an unpublished pending patent application?"
# textarea sizes
self.fields['patents'].widget.attrs['rows'] = 2
self.fields['patents'].widget.attrs['cols'] = 70
self.fields['notes'].widget.attrs['rows'] = 3
self.fields['notes'].widget.attrs['cols'] = 70
self.fields['document_sections'].widget.attrs['rows'] = 3
self.fields['document_sections'].widget.attrs['cols'] = 70
self.fields['comments'].widget.attrs['rows'] = 3
self.fields['comments'].widget.attrs['cols'] = 70
self.fields['other_notes'].widget.attrs['rows'] = 5
self.fields['other_notes'].widget.attrs['cols'] = 70
#self.fields['is_pending'].widget.check_test = mytest
self.fields['is_pending'].widget = forms.Select(choices=self.IS_PENDING_CHOICES)
if formtype == 'update':
if self.instance.generic:
self.fields['document_sections'] = forms.ChoiceField(
widget=forms.RadioSelect,
choices=self.DOCUMENT_SECTIONS_CHOICES,
required=False,
label='C. Does this disclosure apply to all IPR owned by the submitter?')
legacy_url = self.instance.legacy_url_0
self.fields['legacy_url_0'].label = mark_safe(
'<a href="%s">Old IPR Url</a>' % legacy_url
)
updates = self.instance.updates.all()
if updates:
self.fields['updated'].initial = updates[0].updated.ipr_id
rfcs = {}
for rfc in self.instance.documents.filter(doc_alias__name__startswith='rfc'):
rfcs[rfc.doc_alias.id] = get_rfc_num(rfc.doc_alias.document)+" "+rfc.doc_alias.document.title
drafts = {}
for draft in self.instance.documents.exclude(doc_alias__name__startswith='rfc'):
drafts[draft.doc_alias.id] = draft.doc_alias.document.name
self.initial['rfc_num'] = simplejson.dumps(rfcs)
self.initial['id_filename'] = simplejson.dumps(drafts)
else:
# if this is a new IPR hide status field
self.fields['status'].widget = forms.HiddenInput()
def _fields(self, lst):
''' pass in list of titles, get back a list of form fields '''
return [self.fields[k] for k in lst]
def _fetch_objects(self, data, model):
if data:
ids = [int(x) for x in simplejson.loads(data)]
else:
return []
objects = []
for id in ids:
try:
objects.append(model.objects.get(pk=id))
except model.DoesNotExist, e:
raise forms.ValidationError("%s not found for id %d" %(model._meta.verbose_name, id))
return objects
#def clean_document_sections(self):
# import ipdb; ipdb.set_trace()
# if self.data['document_sections'] not in self.fields['document_sections'].choices:
# return ''
# else:
# return self.data['document_sections']
def clean_rfc_num(self):
return self._fetch_objects(
self.cleaned_data['rfc_num'].strip(), DocAlias)
def clean_licensing_option(self):
data = self.cleaned_data['licensing_option']
if data == "":
return 0
return data
def clean_id_filename(self):
return self._fetch_objects(
self.cleaned_data['id_filename'].strip(), DocAlias)
def clean_is_pending(self):
data = self.cleaned_data['is_pending']
if data == "":
return 0
return data
def clean_updated(self):
id = self.cleaned_data['updated']
if id == None:
return None
try:
old_ipr = IprDetail.objects.get(pk=id)
except IprDetail.DoesNotExist:
raise forms.ValidationError("old IPR not found for id %d" % id)
return old_ipr
def clean_status(self):
status = self.cleaned_data['status']
return 0 if status == None else status
def save(self, *args, **kwargs):
#import ipdb; ipdb.set_trace()
ipr_detail = super(IprDetailForm, self).save(*args, **kwargs)
ipr_detail.rfc_document_tag = ipr_detail.rfc_number = None
# Force saving lic_opt_sub to override model editable=False
lic_opt = self.cleaned_data['licensing_option']
for num, ltr in (('1', 'a'), ('2', 'b'), ('3', 'c')):
opt_sub = 'lic_opt_'+ltr+'_sub'
self._meta.fields.append(opt_sub)
if lic_opt == num and opt_sub in self.data:
exec('ipr_detail.'+opt_sub+' = 1')
else:
exec('ipr_detail.'+opt_sub+' = 0')
ipr_detail.save()
old_ipr = self.cleaned_data['updated']
if old_ipr:
if self.cleaned_data['remove_old_ipr']:
old_ipr.status = 3
old_ipr.save()
obj,created = IprUpdate.objects.get_or_create(ipr=ipr_detail,updated=old_ipr)
if created:
obj.status_to_be = old_ipr.status
obj.processed = 0
obj.save()
'''
IprRfc.objects.filter(ipr=ipr_detail).delete()
IprDraft.objects.filter(ipr=ipr_detail).delete()
for rfc in self.cleaned_data['rfc_num']:
IprRfc.objects.create(ipr=ipr_detail, document=rfc)
for draft in self.cleaned_data['id_filename']:
IprDraft.objects.create(ipr=ipr_detail, document=draft)
'''
IprDocAlias.objects.filter(ipr=ipr_detail).delete()
for doc in self.cleaned_data['rfc_num']:
IprDocAlias.objects.create(ipr=ipr_detail,doc_alias=doc)
for doc in self.cleaned_data['id_filename']:
#doc_alias = DocAlias.objects.get(id=doc)
IprDocAlias.objects.create(ipr=ipr_detail,doc_alias=doc)
return ipr_detail
class Meta:
model = IprDetail
fieldsets = [
('basic', {
'fields': [
'title',
'legacy_url_0',
'legacy_title_1',
'legacy_url_1',
'legacy_title_2',
'legacy_url_2',
'submitted_date',
],
}),
('booleans', {
'fields': [
'third_party',
'generic',
'comply',
],
}),
('old_ipr', {
'fields': [
'status',
'updated',
'remove_old_ipr',
],
}),
('legal_name', {
'legend': 'I. Patent Holder/Applicant ("Patent Holder")',
'fields': [
'legal_name',
],
}),
('rfc', {
'legend': 'IV. IETF Document or Working Group Contribution to Which Patent Disclosure Relates',
'fields': [
'rfc_num',
'id_filename',
'other_designations',
],
}),
('disclosure', {
'legend': 'V. Disclosure of Patent Information (i.e., patents or patent applications required to be disclosed by Section 6 of RFC 3979)',
'description': 'A. For granted patents or published pending patent applications, please provide the following information',
'fields': [
'patents',
'date_applied',
'country',
'notes',
'is_pending',
'document_sections',
],
}),
('other_notes', {
'legend': 'VIII. Other Notes',
'fields': [
'other_notes',
],
}),
('licensing_declaration', {
'fields': [
'lic_checkbox',
'licensing_option',
'comments',
],
}),
]

View file

@ -0,0 +1,28 @@
from ietf.ipr.models import IprDetail
class _IprDetailManager(object):
def queue_ipr(self):
# qq{select document_title, ipr_id, submitted_date,status from ipr_detail where status = 0 order by submitted_date desc};
return IprDetail.objects.filter(status=0).order_by('-submitted_date')
def generic_ipr(self):
#qq{select document_title, ipr_id, submitted_date,status,additional_old_title1,additional_old_url1,additional_old_title2,additional_old_url2 from ipr_detail where status = 1 and generic=1 and third_party=0 order by submitted_date desc};
return IprDetail.objects.filter(status=1, generic=True, third_party=False).order_by('-submitted_date')
def third_party_notifications(self):
#qq{select document_title, ipr_id, submitted_date,status,additional_old_title1,additional_old_url1,additional_old_title2,additional_old_url2 from ipr_detail where status = 1 and third_party=1 order by submitted_date desc};
return IprDetail.objects.filter(status=1, third_party=True).order_by('-submitted_date')
def specific_ipr(self):
# qq{select document_title, ipr_id, submitted_date,status,additional_old_title1,additional_old_url1,additional_old_title2,additional_old_url2 from ipr_detail where status = 1 and generic=0 and third_party=0 order by submitted_date desc};
return IprDetail.objects.filter(status=1, generic=False, third_party=False).order_by('-submitted_date')
def admin_removed_ipr(self):
#qq{select document_title, ipr_id, submitted_date,status,additional_old_title1,additional_old_url1,additional_old_title2,additional_old_url2 from ipr_detail where status = 2 order by submitted_date desc};
return IprDetail.objects.filter(status=2).order_by('-submitted_date')
def request_removed_ipr(self):
#qq{select document_title, ipr_id, submitted_date,status,additional_old_title1,additional_old_url1,additional_old_title2,additional_old_url2 from ipr_detail where status = 3 order by submitted_date desc};
return IprDetail.objects.filter(status=3).order_by('-submitted_date')
IprDetailManager = _IprDetailManager()

View file

@ -0,0 +1,193 @@
# Copyright The IETF Trust 2007, All Rights Reserved
from django.db import models
#from django import newforms as forms
#from sec.drafts.models import InternetDraft
#from sec.drafts.models import Rfc
from ietf.ipr.models import *
# ------------------------------------------------------------------------
# Models
'''
LICENSE_CHOICES = (
(1, 'a) No License Required for Implementers.'),
(2, 'b) Royalty-Free, Reasonable and Non-Discriminatory License to All Implementers.'),
(3, 'c) Reasonable and Non-Discriminatory License to All Implementers with Possible Royalty/Fee.'),
(4, 'd) Licensing Declaration to be Provided Later (implies a willingness'
' to commit to the provisions of a), b), or c) above to all implementers;'
' otherwise, the next option "Unwilling to Commit to the Provisions of'
' a), b), or c) Above". - must be selected).'),
(5, 'e) Unwilling to Commit to the Provisions of a), b), or c) Above.'),
(6, 'f) See Text Below for Licensing Declaration.'),
)
STDONLY_CHOICES = (
(0, ""),
(1, "The licensing declaration is limited solely to standards-track IETF documents."),
)
SELECT_CHOICES = (
(0, 'NO'),
(1, 'YES'),
(2, 'NO'),
)
STATUS_CHOICES = (
( 0, "Waiting for approval" ),
( 1, "Approved and Posted" ),
( 2, "Rejected by Administrator" ),
( 3, "Removed by Request" ),
)
# not clear why this has both an ID and selecttype
# Also not clear why a table for "YES" and "NO".
class IprSelecttype(models.Model):
type_id = models.AutoField(primary_key=True)
is_pending = models.IntegerField(unique=True, db_column="selecttype")
type_display = models.CharField(blank=True, max_length=15)
def __str__(self):
return self.type_display
class Meta:
db_table = 'ipr_selecttype'
class IprLicensing(models.Model):
licensing_option = models.AutoField(primary_key=True)
value = models.CharField(max_length=255, db_column='licensing_option_value')
def __str__(self):
return self.value;
class Meta:
db_table = 'ipr_licensing'
class IprDetail(models.Model):
ipr_id = models.AutoField(primary_key=True)
title = models.CharField(blank=True, db_column="document_title", max_length=255)
# Legacy information fieldset
legacy_url_0 = models.CharField(blank=True, null=True, db_column="old_ipr_url", max_length=255)
legacy_url_1 = models.CharField(blank=True, null=True, db_column="additional_old_url1", max_length=255)
legacy_title_1 = models.CharField(blank=True, null=True, db_column="additional_old_title1", max_length=255)
legacy_url_2 = models.CharField(blank=True, null=True, db_column="additional_old_url2", max_length=255)
legacy_title_2 = models.CharField(blank=True, null=True, db_column="additional_old_title2", max_length=255)
# Patent holder fieldset
legal_name = models.CharField("Legal Name", db_column="p_h_legal_name", max_length=255)
# Patent Holder Contact fieldset
# self.contact.filter(contact_type=1)
# IETF Contact fieldset
# self.contact.filter(contact_type=3)
# Related IETF Documents fieldset
rfc_number = models.IntegerField(null=True, editable=False, blank=True) # always NULL
id_document_tag = models.IntegerField(null=True, editable=False, blank=True) # always NULL
other_designations = models.CharField(blank=True, max_length=255)
document_sections = models.TextField("Specific document sections covered", blank=True, max_length=255, db_column='disclouser_identify')
# Patent Information fieldset
patents = models.TextField("Patent Applications", db_column="p_applications", max_length=255)
date_applied = models.CharField(max_length=255)
country = models.CharField(max_length=100)
notes = models.TextField("Additional notes", db_column="p_notes", blank=True)
# AMS Change
#is_pending = models.IntegerField("Unpublished Pending Patent Application", blank=True, choices=SELECT_CHOICES, db_column="selecttype")
#is_pending = models.BooleanField(db_column="selecttype")
is_pending = models.CharField(max_length=3,db_column="selecttype")
applies_to_all = models.IntegerField("Applies to all IPR owned by Submitter", blank=True, choices=SELECT_CHOICES, db_column="selectowned")
# Licensing Declaration fieldset
#licensing_option = models.ForeignKey(IprLicensing, db_column='licensing_option')
licensing_option = models.IntegerField(null=True, blank=True, choices=LICENSE_CHOICES)
lic_opt_a_sub = models.IntegerField(editable=False, choices=STDONLY_CHOICES)
lic_opt_b_sub = models.IntegerField(editable=False, choices=STDONLY_CHOICES)
lic_opt_c_sub = models.IntegerField(editable=False, choices=STDONLY_CHOICES)
comments = models.TextField("Licensing Comments", blank=True)
lic_checkbox = models.BooleanField("All terms and conditions has been disclosed")
# Other notes fieldset
other_notes = models.TextField(blank=True)
# Generated fields, not part of the submission form
# Hidden fields
third_party = models.BooleanField()
generic = models.BooleanField()
comply = models.BooleanField()
status = models.IntegerField(null=True, blank=True, choices=STATUS_CHOICES)
submitted_date = models.DateField(blank=True)
update_notified_date = models.DateField(null=True, blank=True)
def __str__(self):
return self.title
def docs(self):
return list(self.drafts.all()) + list(self.rfcs.all())
def get_absolute_url(self):
return "/ipr/%d/" % self.ipr_id
def get_submitter(self):
try:
return self.contact.get(contact_type=3)
except IprContact.DoesNotExist:
return None
class Meta:
db_table = 'ipr_detail'
class IprContact(models.Model):
TYPE_CHOICES = (
(1, 'Patent Holder Contact'),
(2, 'IETF Participant Contact'),
(3, 'Submitter Contact'),
)
contact_id = models.AutoField(primary_key=True)
ipr = models.ForeignKey(IprDetail, related_name="contact")
contact_type = models.IntegerField(choices=TYPE_CHOICES)
name = models.CharField(max_length=255)
title = models.CharField(blank=True, max_length=255)
department = models.CharField(blank=True, max_length=255)
address1 = models.CharField(blank=True, max_length=255)
address2 = models.CharField(blank=True, max_length=255)
telephone = models.CharField(max_length=25)
fax = models.CharField(blank=True, max_length=25)
email = models.EmailField(max_length=255)
def __str__(self):
return self.name or '<no name>'
class Meta:
db_table = 'ipr_contacts'
class IprDraft(models.Model):
ipr = models.ForeignKey(IprDetail, related_name='drafts')
document = models.ForeignKey(InternetDraft, db_column='id_document_tag', related_name="ipr")
revision = models.CharField(max_length=2)
def __str__(self):
return "%s which applies to %s-%s" % ( self.ipr, self.document, self.revision )
class Meta:
db_table = 'ipr_ids'
class IprNotification(models.Model):
ipr = models.ForeignKey(IprDetail)
notification = models.TextField(blank=True)
date_sent = models.DateField(null=True, blank=True)
time_sent = models.CharField(blank=True, max_length=25)
def __str__(self):
return "IPR notification for %s sent %s %s" % (self.ipr, self.date_sent, self.time_sent)
class Meta:
db_table = 'ipr_notifications'
class IprRfc(models.Model):
ipr = models.ForeignKey(IprDetail, related_name='rfcs')
document = models.ForeignKey(Rfc, db_column='rfc_number', related_name="ipr")
def __str__(self):
return "%s applies to RFC%04d" % ( self.ipr, self.document_id )
class Meta:
db_table = 'ipr_rfcs'
class IprUpdate(models.Model):
id = models.IntegerField(primary_key=True)
ipr = models.ForeignKey(IprDetail, related_name='updates')
updated = models.ForeignKey(IprDetail, db_column='updated', related_name='updated_by')
status_to_be = models.IntegerField(null=True, blank=True)
processed = models.IntegerField(null=True, blank=True)
class Meta:
db_table = 'ipr_updates'
'''

View file

@ -0,0 +1,28 @@
from django.core.urlresolvers import reverse
from django.test import TestCase
from ietf.doc.models import Document
from ietf.utils.test_data import make_test_data
from pyquery import PyQuery
SECR_USER='secretary'
class MainTestCase(TestCase):
fixtures = ['names']
def test_main(self):
"Main Test"
draft = make_test_data()
url = reverse('ipradmin')
response = self.client.get(url, REMOTE_USER=SECR_USER)
self.assertEquals(response.status_code, 301)
"""
def test_view(self):
"View Test"
draft = make_test_data()
drafts = Document.objects.filter(type='draft')
url = reverse('drafts_view', kwargs={'id':drafts[0].name})
response = self.client.get(url, REMOTE_USER=SECR_USER)
self.assertEquals(response.status_code, 200)
"""

View file

@ -0,0 +1,25 @@
from django.conf.urls.defaults import *
from django.views.generic.simple import redirect_to
urlpatterns = patterns('ietf.secr.ipradmin.views',
url(r'^$', redirect_to, {'url': 'admin/'}, name="ipradmin"),
url(r'^admin/?$', 'admin_list', name="ipradmin_admin_list"),
url(r'^admin/detail/(?P<ipr_id>\d+)/?$', 'admin_detail', name="ipradmin_admin_detail"),
url(r'^admin/create/?$', 'admin_create', name="ipradmin_admin_create"),
url(r'^admin/update/(?P<ipr_id>\d+)/?$', 'admin_update', name="ipradmin_admin_update"),
url(r'^admin/notify/(?P<ipr_id>\d+)/?$', 'admin_notify', name="ipradmin_admin_notify"),
url(r'^admin/old_submitter_notify/(?P<ipr_id>\d+)/?$', 'old_submitter_notify', name="ipradmin_old_submitter_notify"),
url(r'^admin/post/(?P<ipr_id>\d+)/?$', 'admin_post', name="ipradmin_admin_post"),
url(r'^admin/delete/(?P<ipr_id>\d+)/?$', 'admin_delete', name="ipradmin_admin_delete"),
url(r'^ajax/rfc_num/?$', 'ajax_rfc_num', name="ipradmin_ajax_rfc_num"),
url(r'^ajax/internet_draft/?$', 'ajax_internet_draft', name="ipradmin_ajax_internet_draft"),
)

828
ietf/secr/ipradmin/views.py Normal file
View file

@ -0,0 +1,828 @@
import re
import itertools
from datetime import datetime
from textwrap import TextWrapper
from smtplib import SMTPException
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
from django.utils.safestring import mark_safe
from ietf.secr.lib import template, jsonapi
from ietf.secr.ipradmin.managers import IprDetailManager
from ietf.secr.ipradmin.forms import IprDetailForm, IPRContactFormset
from ietf.secr.utils.document import get_rfc_num, is_draft
import ietf.settings as settings
from ietf.ipr.models import IprDetail, IprUpdate, IprRfc, IprDraft, IprContact, LICENSE_CHOICES, STDONLY_CHOICES, IprNotification
from ietf.utils.mail import send_mail_text
from ietf.doc.models import DocAlias
from ietf.group.models import Role
@template('ipradmin/list.html')
def admin_list(request):
queue_ipr = IprDetailManager.queue_ipr()
generic_ipr = IprDetailManager.generic_ipr()
specific_ipr = IprDetailManager.specific_ipr()
admin_removed_ipr = IprDetailManager.admin_removed_ipr()
request_removed_ipr = IprDetailManager.request_removed_ipr()
third_party_notifications = IprDetailManager.third_party_notifications()
return dict ( queue_ipr = queue_ipr,
generic_ipr = generic_ipr,
specific_ipr = specific_ipr,
admin_removed_ipr = admin_removed_ipr,
request_removed_ipr = request_removed_ipr,
third_party_notifications = third_party_notifications)
def admin_post(request, ipr_id, from_page, command):
updated_ipr_id = 0
ipr_dtl = IprDetail.objects.get(ipr_id=ipr_id)
ipr_dtl.status = 1
#assert False, (ipr_dtl.ipr_id, ipr_dtl.is_pending)
ipr_dtl.save()
updates = ipr_dtl.updates.filter(processed=0)
for update in updates:
updated_ipr_id = update.updated.ipr_id
old_ipr = IprDetail.objects.get(ipr_id=update.updated.ipr_id)
updated_title = re.sub(r' \(\d\)$', '', old_ipr.title)
lc_updated_title = updated_title.lower()
lc_this_title = ipr_dtl.title.lower()
if lc_updated_title == lc_this_title:
doc_number = IprDetail.objects.filter(title__istartswith=lc_this_title+' (', title__iendswith=')').count()
if doc_number == 0: # No same ipr title before - number the orginal ipr
old_ipr.title = "%s (1)" % ipr_dtl.title
ipr_dtl.title = "%s (2)" % ipr_dtl.title
else: # Second or later update, increment
ipr_dtl.title = "%s (%d)" % (ipr_dtl.title, doc_number+1)
old_ipr.status = update.status_to_be
update.processed = 1
old_ipr.save()
update.save()
ipr_dtl.save()
#assert False, (ipr_dtl.ipr_id, ipr_dtl.is_pending)
redirect_url = '/ipradmin/admin/notify/%s?from=%s' % (ipr_id, from_page)
return HttpResponseRedirect(redirect_url)
# end admin_post
def send_notifications(post_data, ipr_id, update=False):
for field in post_data:
if 'notify' in field:
str_msg = re.sub(r'\r', '', post_data[field])
msg_lines = str_msg.split('\n')
body = '\n'.join(msg_lines[4:])
headers = msg_lines[:4]
to = re.sub('To:', '', headers[0]).strip()
frm = re.sub('From:', '', headers[1]).strip()
subject = re.sub('Subject:', '', headers[2]).strip()
cc = re.sub('Cc:', '', headers[3]).strip()
'''
not required, send_mail_text handles this
try:
if settings.DEVELOPMENT:
to = cc = settings.TEST_EMAIL
frm = 'test@amsl.com'
except AttributeError:
pass
'''
try:
send_mail_text(None, to, frm, subject, body, cc)
except (ImproperlyConfigured, SMTPException) as e:
return e
now = datetime.now()
IprNotification(
ipr_id=ipr_id,
notification=str_msg,
date_sent=now.date(),
time_sent=now.time()
)
if update:
ipr_dtl = IprDetail.objects.get(ipr_id=ipr_id)
today = datetime.today().date()
ipr_dtl.update_notified_date = today
ipr_dtl.save()
return None
@template('ipradmin/notify.html')
def admin_notify(request, ipr_id):
if request.POST and 'command' in request.POST and 'do_send_notifications' == request.POST['command']:
send_result = send_notifications(request.POST, ipr_id)
if send_result:
request.session['send_result'] = 'Some messages failed to send'
else:
request.session['send_result'] = 'Messages sent successfully'
return HttpResponseRedirect(reverse(
'ipr_old_submitter_notify',
args=[ipr_id]
))
if 'send_result' in request.session:
result = request.session['send_result']
del request.session['send_result']
return dict(
page_id = 'send_result',
result = result
)
if request.GET and 'from' in request.GET:
from_page = request.GET['from']
page_id = from_page + '_post'
ipr_dtl = IprDetail.objects.get(ipr_id=ipr_id)
generic_ad = ''
if ipr_dtl.generic:
generic_ad = get_generic_ad_text(ipr_id)
updated_ipr = ipr_dtl.updates.filter(processed=0)
updated_ipr_id = updated_ipr[0].ipr_id if updated_ipr else 0
submitter_text = get_submitter_text(ipr_id, updated_ipr_id, from_page)
document_relatives = ''
#drafts = IprDraft.objects.filter(ipr__ipr_id=ipr_id)
#for draft in drafts:
# document_relatives += get_document_relatives(ipr_id, draft, is_draft=1)
#rfcs = IprRfc.objects.filter(ipr__ipr_id=ipr_id)
#for rfc in rfcs:
# document_relatives += get_document_relatives(ipr_id, rfc, is_draft=0)
# REDESIGN
for iprdocalias in ipr_dtl.documents.all():
document_relatives += get_document_relatives(ipr_dtl, iprdocalias.doc_alias)
return dict(
page_id = page_id,
ipr_id = ipr_id,
submitter_text = submitter_text,
document_relatives = document_relatives,
generic_ad = generic_ad
)
def get_generic_ad_text(id):
'''
This function builds an email to the General Area, Area Director
'''
text = ''
role = Role.objects.filter(group__acronym='gen',name='ad')[0]
gen_ad_name = role.person.name
gen_ad_email = role.email.address
ipr_dtl = IprDetail.objects.get(ipr_id=id)
submitted_date, ipr_title = ipr_dtl.submitted_date, ipr_dtl.title
email_body = TextWrapper(width=80, break_long_words=False).fill(
'A generic IPR disclosure was submitted to the IETF Secretariat on %s and has been posted on the "IETF Page of Intellectual Property Rights Disclosures" (https://datatracker.ietf.org/public/ipr_list.cgi). The title of the IPR disclosure is "%s."' % (submitted_date, ipr_title)
)
text = '''
<h4>Generic IPR notification to GEN AD, %s</h4>
<textarea name="notify_gen_ad" rows=25 cols=80>
To: %s
From: IETF Secretariat <ietf-ipr@ietf.org>
Subject: Posting of IPR Disclosure
Cc:
Dear %s:
%s
The IETF Secretariat
</textarea>
<br><br>
<br>
''' % (gen_ad_name, gen_ad_email, gen_ad_name, email_body)
return text
def get_submitter_text(ipr_id, updated_ipr_id, from_page):
text = ''
ipr_dtl = IprDetail.objects.get(ipr_id=ipr_id)
c3, c2, c1 = [ipr_dtl.contact.filter(contact_type=x) for x in [3,2,1]]
if c3:
to_email, to_name = c3[0].email, c3[0].name
elif c2:
to_email, to_name = c2[0].email, c2[0].name
elif c1:
to_email, to_name = c1[0].email, c1[0].name
else:
to_email = "UNKNOWN EMAIL - NEED ASSISTANCE HERE"
to_name = "UNKNOWN NAME - NEED ASSISTANCE HERE"
ipr_title = IprDetail.objects.get(ipr_id=ipr_id).title
wrapper = TextWrapper(width=80, break_long_words=False)
if from_page == 'detail':
email_body = 'Your IPR disclosure entitled "%s" has been posted on the "IETF Page of Intellectual Property Rights Disclosures" (https://datatracker.ietf.org/public/ipr_list.cgi).' % (ipr_title)
subject = "Posting of IPR Disclosure";
elif from_page == 'update':
email_body = 'On DATE, UDPATE NAME submitted an update to your 3rd party disclosure -- entitled "%s". The update has been posted on the "IETF Page of Intellectual Property Rights Disclosures" (https://datatracker.ietf.org/ipr/%s/)' % (ipr_title, ipr_id)
subject = "IPR disclosure Update Notification"
email_body = wrapper.fill(email_body)
cc_list = [];
if updated_ipr_id > 0:
subject = "Posting of Updated IPR Disclosure"
updated_ipr_dtl = IprDetail.objects.get(ipr_id=updated_ipr_id)
old_submitted_date, old_title = updated_ipr_dtl.submitted_date, updated_ipr_dtl.old_title
email_body = 'Your IPR disclosure entitled "%s" has been posted on the "IETF Page of Intellectual Property Rights Disclosures" (https://datatracker.ietf.org/public/ipr_list.cgi). Your IPR disclosure updates IPR disclosure ID #$updated_ipr_id, "%s," which was posted on $old_submitted_date' % (ipr_title, updated_ipr_id, old_title, old_submitted_date)
updated_contacts = updated_ipr_dtl.contact.all()
c3, c2, c1 = [updated_contacts.filter(contact_type=x) for x in [3,2,1]]
if c3:
cc_list.append(c3[0].email)
elif c2:
cc_list.append(c2[0].email)
for idx in range(10):
cc_list.append(c1[0].email)
updated_ipr = IprUpdate.objects.filter(ipr_id=updated_ipr_id)
if updated_ipr:
c1 = IprContact.objects.filter(ipr_id=updated_ipr[0].updated, contact_type=1)
if not updated_ipr or not c1:
break
cc_list.append(ipr_dtl.contacts.filter(contact_type=1)[0].email)
cc_list = ','.join(list(set(cc_list)))
text = '''To: %s
From: IETF Secretariat <ietf-ipr@ietf.org>
Subject: %s
Cc: %s
Dear %s:
%s
The IETF Secretariat
''' % (to_email, subject, cc_list, to_name, email_body)
return text
# end get_submitter_text
def get_document_relatives(ipr_dtl, docalias):
'''
This function takes a IprDetail object and a DocAlias object and returns an email.
'''
text = ''
doc = docalias.document
doc_info, author_names, author_emails, cc_list = '', '', '', ''
authors = doc.authors.all()
if is_draft(doc):
doc_info = 'Internet-Draft entitled "%s" (%s)' \
% (doc.title, doc.name)
updated_id = doc.pk
else: # not i-draft, therefore rfc
rfc_num = get_rfc_num(doc)
doc_info = 'RFC entitled "%s" (RFC%s)' \
% (doc.title, rfc_num)
updated_id = rfc_num
# if the document is not associated with a group copy job owner or Gernal Area Director
if doc.group.acronym == 'none':
if doc.ad and is_draft(doc):
cc_list = doc.ad.role_email('ad').address
else:
role = Role.objects.filter(group__acronym='gen',name='ad')[0]
cc_list = role.email.address
else:
cc_list = get_wg_email_list(doc.group)
author_emails = ','.join([a.address for a in authors])
author_names = ', '.join([a.person.name for a in authors])
cc_list += ", ipr-announce@ietf.org"
submitted_date = ipr_dtl.submitted_date
ipr_title = ipr_dtl.title
email_body = '''
An IPR disclosure that pertains to your %s was submitted to the IETF Secretariat on %s and has been posted on the "IETF Page of Intellectual Property Rights Disclosures" (https://datatracker.ietf.org/ipr/%s/). The title of the IPR disclosure is "%s."");
''' % (doc_info, submitted_date, ipr_dtl.ipr_id, ipr_title)
wrapper = TextWrapper(width=80, break_long_words=False)
email_body = wrapper.fill(email_body)
text = '''
<h4>Notification for %s</h4>
<textarea name="notify_%s" rows=25 cols=80>
To: %s
From: IETF Secretariat <ietf-ipr@ietf.org>
Subject: IPR Disclosure: %s
Cc: %s
Dear %s:
%s
The IETF Secretariat
</textarea>
<br><br>
''' % (doc_info, updated_id, author_emails, ipr_title, cc_list, author_names, email_body)
# FIXME: why isn't this working - done in template now, also
return mark_safe(text)
# end get_document_relatives
def get_wg_email_list(group):
'''This function takes a Working Group object and returns a string of comman separated email
addresses for the Area Directors and WG Chairs
'''
result = []
roles = itertools.chain(Role.objects.filter(group=group.parent,name='ad'),
Role.objects.filter(group=group,name='chair'))
for role in roles:
result.append(role.email.address)
if group.list_email:
result.append(group.list_email)
return ', '.join(result)
@template('ipradmin/delete.html')
def admin_delete(request, ipr_id):
ipr_dtl = IprDetail.objects.get(ipr_id=ipr_id)
ipr_dtl.status = 2
ipr_dtl.save()
return HttpResponseRedirect(reverse('ipr_admin_list'))
@template('ipradmin/notify.html')
def old_submitter_notify(request, ipr_id):
if request.POST and 'command' in request.POST \
and 'do_send_update_notification' == request.POST['command']:
send_result = send_notifications(request.POST, ipr_id, update=True)
if send_result:
#assert False, send_result
request.session['send_result'] = 'Some messages failed to send'
else:
request.session['send_result'] = 'Messages sent successfully'
return HttpResponseRedirect(reverse(
'ipr_old_submitter_notify',
args=[ipr_id]
))
if 'send_result' in request.session:
result = request.session['send_result']
del request.session['send_result']
return dict(
page_id = 'send_result',
result = result
)
contact_three = IprContact.objects.filter(ipr__ipr_id=ipr_id, contact_type=3)
if contact_three:
submitter_email, submitter_name = contact_three[0].email, contact_three[0].name
else:
contact_two = IprContact.objects.filter(ipr__ipr_id=ipr_id, contact_type=2)
if contact_two:
submitter_email, submitter_name = contact_two[0].email, contact_two[0].name
else:
submitter_email = submitter_name = ''
try:
ipr_update = IprUpdate.objects.get(ipr__ipr_id=ipr_id, processed=0)
except IprUpdate.DoesNotExist:
# import ipdb; ipdb.set_trace()
pass
old_ipr_id = ipr_update.updated.ipr_id
old_ipr = IprDetail.objects.get(ipr_id=old_ipr_id)
old_contact_three = IprContact.objects.filter(ipr__ipr_id=old_ipr_id, contact_type=3)
if old_contact_three:
to_email, to_name = old_contact_three[0].email, old_contact_three[0].name
else:
old_contact_two = IprContact.objects.filter(ipr__ipr_id=old_ipr_id, contact_type=2)
if old_contact_two:
to_email, to_name = old_contact_two[0].email, old_contact_two[0].name
else:
to_email = to_name = ''
updated_document_title, orig_submitted_date = old_ipr.title, old_ipr.submitted_date
return dict(
page_id = 'detail_notify',
ipr_id = ipr_id,
updated_ipr_id = old_ipr_id,
submitter_email = submitter_email,
submitter_name = submitter_name,
to_email = to_email,
to_name = to_name,
updated_document_title = updated_document_title,
orig_submitted_date = orig_submitted_date,
)
# end old_submitter_notify
@template('ipradmin/detail.html')
def admin_detail(request, ipr_id):
if request.POST and request.POST['command']:
command = request.POST['command']
if command == 'post':
return admin_post(request, ipr_id, 'detail', 'post')
elif command == 'notify':
return HttpResponseRedirect(reverse('ipr_old_submitter_notify', args=[ipr_id]))
elif command == 'delete':
return HttpResponseRedirect(reverse('ipr_admin_delete', args=[ipr_id]))
header_text = possible = temp_name = footer_text = ''
contact_one_data, contact_two_data, document_data, licensing_data,\
disclosure_data, designations_data, contact_three_data,\
notes_data, controls = [], [], [], [], [], [], [], [], []
ipr_dtl = IprDetail.objects.get(ipr_id=ipr_id)
ipr_updates = IprUpdate.objects.filter(processed=0, ipr__ipr_id=ipr_id)
contact_one, contact_two, contact_three = [
ipr_dtl.contact.filter(contact_type=x) for x in (1,2,3)
]
if ipr_updates:
if ipr_dtl.update_notified_date:
footer_text = mark_safe('<span class="alert">This update was notifed to the submitter of the IPR that is being updated on %s.</span>' % ipr_dtl.update_notified_date)
else:
controls = ['notify']
if not ipr_updates or ipr_dtl.update_notified_date:
controls = ['post']
controls.append('delete')
if ipr_dtl.third_party:
temp_name = 'Notification'
possible = 'Possible '
displaying_section = "I, II, and III"
header_text = '''This form is used to let the IETF know about patent information regarding an IETF document or contribution when the person letting the IETF know about the patent has no relationship with the patent owners.<br>
Click <a href="./ipr.cgi"> here</a> if you want to disclose information about patents or patent applications where you do have a relationship to the patent owners or patent applicants.'''
elif ipr_dtl.generic:
temp_name = "Generic IPR Disclosures"
displaying_section = "I and II"
header_text = '''This document is an IETF IPR Patent Disclosure and Licensing Declaration
Template and is submitted to inform the IETF of a) patent or patent application information that is not related to a specific IETF document or contribution, and b) an IPR Holder's intention with respect to the licensing of its necessary patent claims.
No actual license is implied by submission of this template.'''
else:
temp_name = "Specific IPR Disclosures"
displaying_section = "I, II, and IV"
header_text = '''This document is an IETF IPR Disclosure and Licensing Declaration
Template and is submitted to inform the IETF of a) patent or patent application information regarding
the IETF document or contribution listed in Section IV, and b) an IPR Holder\'s intention with respect to the licensing of its necessary patent claims.
No actual license is implied by submission of this template.
Please complete and submit a separate template for each IETF document or contribution to which the
disclosed patent information relates.'''
legacy_links = (
(ipr_dtl.legacy_url_1 or '', ipr_dtl.legacy_title_1 or ''),
(ipr_dtl.legacy_url_2 or '', ipr_dtl.legacy_title_2 or ''),
)
comply_statement = '' if ipr_dtl.comply else mark_safe('<div class="alert">This IPR disclosure does not comply with the formal requirements of Section 6, "IPR Disclosures," of RFC 3979, "Intellectual Property Rights in IETF Technology."</div>')
# FIXME: header_text is assembled in perl code but never printed
if ipr_dtl.legacy_url_0:
#header_text = header_text + '''
header_text = '''
This IPR disclosure was submitted by e-mail.<br>
%s
Sections %s of "The Patent Disclosure and Licensing Declaration Template for %s" have been completed for this IPR disclosure. Additional information may be available in the original submission.<br>
Click <a href="%s">here</a> to view the content of the original IPR disclosure.''' % (comply_statement, displaying_section, temp_name, ipr_dtl.legacy_url_0)
else:
#header_text = header_text + '''
header_text = '''
Only those sections of the "Patent Disclosure and Licensing Declaration Template for %s" where the submitter provided information are displayed.''' % temp_name
if not ipr_dtl.generic or not (not ipr_dtl.legacy_url_0 and (ipr_dtl.notes or ipr_dtl.patents)):
# FIXME: behavior as perl, but is quite confusing and seems wrong
if contact_one and contact_one[0].name:
contact_one = contact_one[0]
contact_one_data = [
('II. Patent Holder\'s Contact for License Application:'),
('Name:', contact_one.name),
('Title:', contact_one.title),
('Department:', contact_one.department),
('Address1:', contact_one.address1),
('Address2:', contact_one.address2),
('Telephone:', contact_one.telephone),
('Fax:', contact_one.fax),
('Email:', contact_one.email)
]
if not ipr_dtl.generic:
if contact_two and contact_two[0].name:
contact_two = contact_two[0]
contact_two_data = [
('III. Contact Information for the IETF Participant Whose Personal Belief Triggered this Disclosure:'),
('Name:', contact_two.name),
('Title:', contact_two.title),
('Department:', contact_two.department),
('Address1:', contact_two.address1),
('Address2:', contact_two.address2),
('Telephone:', contact_two.telephone),
('Fax:', contact_two.fax),
('Email:', contact_two.email)
]
# conversion
#rfcs = ipr_dtl.rfcs.all()
#drafts = ipr_dtl.drafts.all()
rfcs = ipr_dtl.documents.filter(doc_alias__name__startswith='rfc')
drafts = ipr_dtl.documents.exclude(doc_alias__name__startswith='rfc')
titles_data, rfcs_data, drafts_data, designations_data = (), (), (), ()
rfc_titles, draft_titles = [], []
if rfcs:
rfc_titles = [
rfc.doc_alias.document.title for rfc in rfcs
]
rfcs_data = tuple([
'RFC Number:',
[get_rfc_num(rfc.doc_alias.document) for rfc in rfcs]
])
if drafts:
draft_titles = [
draft.doc_alias.document.title for draft in drafts
]
drafts_data = tuple([
'ID Filename:',
[draft.doc_alias.document.name+'.txt' for draft in drafts]
])
if ipr_dtl.other_designations:
designations_data = tuple([
'Designations for Other Contributions:',
ipr_dtl.other_designations
])
if drafts or rfcs:
titles_data = tuple([
'Title:',
rfc_titles + draft_titles
])
if rfcs_data or drafts_data or designations_data:
document_data = [
('IV. IETF Document or Other Contribution to Which this IPR Disclosure Relates'),
titles_data,
rfcs_data,
drafts_data,
designations_data,
]
if not ipr_dtl.legacy_url_0 and (ipr_dtl.notes or ipr_dtl.patents):
if ipr_dtl.generic:
disclosure_c = (
'C. Does this disclosure apply to all IPR owned by the submitter?',
'YES' if ipr_dtl.applies_to_all else 'NO'
)
else:
disclosure_c = (
'''C. If an Internet-Draft or RFC includes multiple parts and it is not
reasonably apparent which part of such Internet-Draft or RFC is alleged
to be covered by the patent information disclosed in Section
V(A) or V(B), it is helpful if the discloser identifies here the sections of
the Internet-Draft or RFC that are alleged to be so
covered.''',
ipr_dtl.document_sections
)
disclosure_data = [
('V. Disclosure of Patent Information (i.e., patents or patent applications required to be disclosed by Section 6 of RFC 3979)'),
('A. For granted patents or published pending patent applications, please provide the following information', ''),
('Patent, Serial, Publication, Registration, or Application/File number(s):', ipr_dtl.patents),
('Date(s) granted or applied for:', ipr_dtl.date_applied),
('Country:', ipr_dtl.country),
('Additional Notes:', ipr_dtl.notes),
#('B. Does your disclosure relate to an unpublished pending patent application?', 'YES' if ipr_dtl.applies_to_all else 'NO'),
('B. Does your disclosure relate to an unpublished pending patent application?', 'YES' if ipr_dtl.is_pending == 1 else 'NO'),
disclosure_c
]
if not ipr_dtl.third_party and ipr_dtl.licensing_option:
lic_idx = ipr_dtl.licensing_option
chosen_declaration = LICENSE_CHOICES[lic_idx-1][1]
sub_opt = bool(
lic_idx == 0 and ipr_dtl.lic_opt_a_sub
or lic_idx == 1 and ipr_dtl.lic_opt_b_sub
or lic_idx == 2 and ipr_dtl.lic_opt_c_sub
)
chosen_declaration += STDONLY_CHOICES[1][1] if sub_opt else ''
chosen_declaration = (mark_safe("<strong>%s</strong>" % chosen_declaration), '')
comments = ipr_dtl.comments or None
lic_checkbox = ipr_dtl.lic_checkbox or None
if comments or lic_checkbox:
comments_notes_label = ('Licensing information, comments, notes or URL for further information:'),
comments_notes = (mark_safe(
"<strong>%s<br /><br />%s</strong>" % (
comments,
'The individual submitting this template represents and warrants that all terms and conditions that must be satisfied for implementers of any covered IETF specification to obtain a license have been disclosed in this IPR disclosure statement.' if lic_checkbox else ''
)),
''
)
else:
comments_notes_label = comments_notes = ''
licensing_data = [
('VI. Licensing Declaration:'),
('The Patent Holder states that its position with respect to licensing any patent claims contained in the patent(s) or patent application(s) disclosed above that would necessarily be infringed by implementation of the technology required by the relevant IETF specification ("Necessary Patent Claims"), for the purpose of implementing such specification, is as follows(select one licensing declaration option only):', ''),
chosen_declaration,
comments_notes_label,
comments_notes
]
if contact_three and contact_three[0].name:
contact_three = contact_three[0]
contact_three_data = [
('VII. Contact Information of Submitter of this Form (if different from IETF Participant in Section III above):'),
('Name:', contact_three.name),
('Title:', contact_three.title),
('Department:', contact_three.department),
('Address1:', contact_three.address1),
('Address2:', contact_three.address2),
('Telephone:', contact_three.telephone),
('Fax:', contact_three.fax),
('Email:', contact_three.email)
]
if ipr_dtl.other_notes:
notes_data = (
('VIII. Other Notes:'),
(mark_safe("<strong>%s</strong>" % ipr_dtl.other_notes), '')
)
if not (not ipr_dtl.legacy_url_0 and (ipr_dtl.notes or ipr_dtl.patents)):
# FIXME: behavior as perl, but is quite confusing and seems wrong
licensing_data = contact_three_data = notes_data = ()
page_data = [
[
('I. %sPatent Holder/Applicant ("Patent Holder"):' % possible),
('Legal Name:', ipr_dtl.legal_name),
],
contact_one_data,
contact_two_data,
document_data,
disclosure_data,
licensing_data,
contact_three_data,
notes_data,
]
return dict(
ipr_title = ipr_dtl.title,
header_text = header_text,
legacy_links = legacy_links,
submitted_date = ipr_dtl.submitted_date,
page_data = page_data,
footer_text = footer_text,
controls = controls,
)
# end admin_detail
@template('ipradmin/create.html')
def admin_create(request):
if request.method == 'POST':
ipr_detail_form = IprDetailForm(request.POST, request.FILES, formtype='create')
ipr_contact_formset = IPRContactFormset(request.POST)
if ipr_detail_form.is_valid() and \
ipr_contact_formset.forms[0].is_valid():
ipr_detail = ipr_detail_form.save()
ipr_contact_formset.forms[0].save(ipr_detail)
if ipr_contact_formset.forms[1].is_valid():
ipr_contact_formset.forms[1].save(ipr_detail)
if ipr_contact_formset.forms[2].is_valid():
ipr_contact_formset.forms[2].save(ipr_detail)
return HttpResponseRedirect(reverse('ipr_admin_list'))
else:
ipr_detail_form = IprDetailForm(formtype='create')
ipr_contact_formset = IPRContactFormset(initial=[
{'contact_type' : 1, 'legend' : "II. Patent Holder's Contact for License Application "},
{'contact_type' : 2, 'legend' : "III. Contact Information for the IETF Participant Whose Personal Belief Triggered the Disclosure in this Template (Optional): "},
{'contact_type' : 3, 'legend' : "VII. Contact Information of Submitter of this Form (if different from IETF Participant in Section III above)"}])
return dict(licensing_option_labels = ('a', 'b', 'c', 'd', 'e', 'f'),
ipr_detail_form = ipr_detail_form,
ipr_contact_formset = ipr_contact_formset)
# end admin_create
@template('ipradmin/update.html')
def admin_update(request, ipr_id):
if request.method == 'POST':
ipr_detail_form = IprDetailForm(
request.POST,
request.FILES,
formtype='update',
instance=IprDetail.objects.get(ipr_id=ipr_id)
)
ipr_contact_formset = IPRContactFormset(
request.POST,
)
if ipr_detail_form.is_valid() and \
ipr_contact_formset.forms[0].is_valid():
ipr_detail = ipr_detail_form.save(commit=False)
if 'update_ipr' in request.POST:
if ipr_detail.third_party:
return HttpResponseRedirect('/ipradmin/admin/notify/%s?from=update' % ipr_id)
else:
redirect_url = ''
else: # remove
redirect_url = reverse('ipr_admin_list')
if 'admin_remove_ipr' in request.POST:
ipr_detail.status = 2
elif 'request_remove_ipr' in request.POST:
ipr_detail.status = 3
ipr_detail.save()
ipr_contact_formset.forms[0].save(ipr_detail)
if ipr_contact_formset.forms[1].is_valid():
ipr_contact_formset.forms[1].save(ipr_detail)
if ipr_contact_formset.forms[2].is_valid():
ipr_contact_formset.forms[2].save(ipr_detail)
return HttpResponseRedirect(redirect_url)
else:
pass
else: # GET
ipr_detail_form = IprDetailForm(
formtype='update',
instance=IprDetail.objects.get(ipr_id=ipr_id)
)
ipr_contact_formset = IPRContactFormset(
initial = get_contact_initial_data(ipr_id)
)
return dict(licensing_option_labels = ('a', 'b', 'c', 'd', 'e', 'f'),
ipr_detail_form = ipr_detail_form,
ipr_contact_formset = ipr_contact_formset)
# end admin_update
def get_contact_initial_data(ipr_id):
c1_data, c2_data, c3_data = (
{'contact_type' : '1', 'legend' : "II. Patent Holder's Contact for License Application "},
{'contact_type' : '2', 'legend' : "III. Contact Information for the IETF Participant Whose Personal Belief Triggered the Disclosure in this Template (Optional): "},
{'contact_type' : '3', 'legend' : "VII. Contact Information of Submitter of this Form (if different from IETF Participant in Section III above)"}
)
ipr_dtl = IprDetail.objects.get(ipr_id=ipr_id)
c1, c2, c3 = [ipr_dtl.contact.filter(contact_type=x).order_by('-pk') for x in [1,2,3]]
if c1:
c1 = c1[0]
c1_data.update({
'name': c1.name,
'title': c1.title,
'department': c1.department,
'address1': c1.address1,
'address2': c1.address2,
'telephone': c1.telephone,
'fax': c1.fax,
'email': c1.email
})
if c2:
c2 = c2[0]
c2_data.update({
'name': c2.name,
'title': c2.title,
'department': c2.department,
'address1': c2.address1,
'address2': c2.address2,
'telephone': c2.telephone,
'fax': c2.fax,
'email': c2.email
})
if c3:
c3 = c3[0]
c3_data.update({
'name': c3.name,
'title': c3.title,
'department': c3.department,
'address1': c3.address1,
'address2': c3.address2,
'telephone': c3.telephone,
'fax': c3.fax,
'email': c3.email
})
return [c1_data, c2_data, c3_data]
@jsonapi
def ajax_rfc_num(request):
if request.method != 'GET' or not request.GET.has_key('term'):
return { 'success' : False, 'error' : 'No term submitted or not GET' }
q = request.GET.get('term')
results = DocAlias.objects.filter(name__startswith='rfc%s' % q)
if results.count() > 20:
results = results[:20]
elif results.count() == 0:
return { 'success' : False, 'error' : "No results" }
response = [ dict(id=r.id, label=unicode(r.name)+" "+r.document.title) for r in results ]
return response
@jsonapi
def ajax_internet_draft(request):
if request.method != 'GET' or not request.GET.has_key('term'):
return { 'success' : False, 'error' : 'No term submitted or not GET' }
q = request.GET.get('term')
results = DocAlias.objects.filter(name__icontains=q)
if results.count() > 20:
results = results[:20]
elif results.count() == 0:
return { 'success' : False, 'error' : "No results" }
response = [dict(id=r.id, label = r.name) for r in results]
return response

View file

@ -0,0 +1 @@
from template import template, jsonapi

36
ietf/secr/lib/template.py Normal file
View file

@ -0,0 +1,36 @@
try:
import json
except ImportError:
import simplejson as json
from django.http import HttpResponse
from django.template import RequestContext
from django.shortcuts import render_to_response
def template(template):
def decorator(fn):
def render(request, *args, **kwargs):
context_data = fn(request, *args, **kwargs)
if isinstance(context_data, HttpResponse):
# View returned an HttpResponse like a redirect
return context_data
else:
# For any other type of data try to populate a template
return render_to_response(template,
context_data,
context_instance=RequestContext(request)
)
return render
return decorator
def jsonapi(fn):
def to_json(request, *args, **kwargs):
context_data = fn(request, *args, **kwargs)
return HttpResponse(json.dumps(context_data),
mimetype='application/json')
return to_json
def render(template, data, request):
return render_to_response(template,
data,
context_instance=RequestContext(request))

View file

View file

@ -0,0 +1,88 @@
from django.conf import settings
'''
RTF quick reference (from Word2007RTFSpec9.doc):
\fs24 : sets the font size to 24 half points
\header : header on all pages
\headerf : header on first page only
\pard : resets any previous paragraph formatting
\plain : resets any previous character formatting
\qr : right-aligned
\tqc : centered tab
\tqr : flush-right tab
\tx : tab position in twips (1440/inch) from the left margin
\nowidctlpar : no window/orphan control
\widctlpar : window/orphan control
'''
def create_blue_sheets(meeting, groups):
file = open(settings.SECR_BLUE_SHEET_PATH, 'w')
header = '''{\\rtf1\\ansi\\ansicpg1252\\uc1 \\deff0\\deflang1033\\deflangfe1033
{\\fonttbl{\\f0\\froman\\fcharset0\\fprq2{\\*\\panose 02020603050405020304}Times New Roman;}}
{\\colortbl;\\red0\\green0\\blue0;\\red0\\green0\\blue255;\\red0\\green255\\blue255;\\red0\\green255\\blue0;
\\red255\\green0\\blue255;\\red255\\green0\\blue0;\\red255\\green255\\blue0;\\red255\\green255\\blue255;
\\red0\\green0\\blue128;\\red0\\green128\\blue128;\\red0\\green128\\blue0;\\red128\\green0\\blue128;
\\red128\\green0\\blue0;\\red128\\green128\\blue0;\\red128\\green128\\blue128;
\\red192\\green192\\blue192;}
\\widowctrl\\ftnbj\\aenddoc\\hyphcaps0\\formshade\\viewkind1\\viewscale100\\pgbrdrhead\\pgbrdrfoot
\\fet0\\sectd \\pgnrestart\\linex0\\endnhere\\titlepg\\sectdefaultcl'''
file.write(header)
for group in groups:
group_header = ''' {\\header \\pard\\plain \\s15\\nowidctlpar\\widctlpar\\tqc\\tx4320\\tqr\\tx8640\\adjustright \\fs20\\cgrid
{ Mailing List: %s \\tab\\tab Meeting # %s %s (%s) \\par }
\\pard \\s15\\nowidctlpar\\widctlpar\\tqc\\tx4320\\tqr\\tx8640\\adjustright
{\\b\\fs24
\\par
\\par \\tab The NOTE WELL statement applies to this meeting. Participants acknowledge that these attendance records will be made available to the public.
\\par
\\par NAME ORGANIZATION
\\par \\tab
\\par }}
{\\footer \\pard\\plain \\s16\\qc\\nowidctlpar\\widctlpar\\tqc\\tx4320\\tqr\\tx8640\\adjustright \\fs20\\cgrid {\\cs17 Page }
{\\field{\\*\\fldinst {\\cs17 PAGE }}}
{ \\par }}
{\\headerf \\pard\\plain \\s15\\qr\\nowidctlpar\\widctlpar\\tqc\\tx4320\\tqr\\tx8640\\adjustright \\fs20\\cgrid
{\\b\\fs24 Meeting # %s %s (%s) \\par }}
{\\footerf \\pard\\plain \\s16\\qc\\nowidctlpar\\widctlpar\\tqc\\tx4320\\tqr\\tx8640\\adjustright \\fs20\\cgrid
{Page 1 \\par }}
\\pard\\plain \\qc\\nowidctlpar\\widctlpar\\adjustright \\fs20\\cgrid
{\\b\\fs32 %s IETF Working Group Roster \\par }
\\pard \\nowidctlpar\\widctlpar\\adjustright
{\\fs28 \\par Working Group Session: %s \\par \\par }
{\\b \\fs24 Mailing List: %s \\tx5300\\tab Actual Start Time: __________ \\par \\par Chairperson:_______________________________ Actual End Time: __________ \\par \\par }
{\\tab \\tab }
{\\par \\tab The NOTE WELL statement applies to this meeting. Participants acknowledge that these attendance records will be made available to the public. \\par
\\par\\b NAME ORGANIZATION
\\par }
\\pard \\fi-90\\li90\\nowidctlpar\\widctlpar\\adjustright
{\\fs16
''' % (group.list_email,
meeting.number,
group.acronym,
group.type,
meeting.number,
group.acronym,
group.type,
meeting.number,
group.name,
group.list_email)
file.write(group_header)
for x in range(1,117):
line = '''\\par %s._________________________________________________ \\tab _____________________________________________________
\\par
''' % x
file.write(line)
footer = '''}
\\pard \\nowidctlpar\\widctlpar\\adjustright
{\\fs16 \\sect }
\\sectd \\pgnrestart\\linex0\\endnhere\\titlepg\\sectdefaultcl
'''
file.write(footer)
file.write('\n}')
file.close()

210
ietf/secr/meetings/forms.py Normal file
View file

@ -0,0 +1,210 @@
from django import forms
from django.db.models import Q
from ietf.group.models import Group
from ietf.meeting.models import Meeting, Room, TimeSlot, Session
from ietf.meeting.timedeltafield import TimedeltaFormField, TimedeltaWidget
from ietf.name.models import TimeSlotTypeName
import datetime
import itertools
import re
DAYS_CHOICES = ((-1,'Saturday'),
(0,'Sunday'),
(1,'Monday'),
(2,'Tuesday'),
(3,'Wednesday'),
(4,'Thursday'),
(5,'Friday'))
# using Django week_day lookup values (Sunday=1)
SESSION_DAYS = ((2,'Monday'),
(3,'Tuesday'),
(4,'Wednesday'),
(5,'Thursday'),
(6,'Friday'))
#----------------------------------------------------------
# Helper Functions
#----------------------------------------------------------
def get_next_slot(slot):
'''Takes a TimeSlot object and returns the next TimeSlot same day and same room, None if there
aren't any. You must check availability of the slot as we sometimes need to get the next
slot whether it's available or not. For use with combine option.
'''
same_day_slots = TimeSlot.objects.filter(meeting=slot.meeting,location=slot.location,time__day=slot.time.day).order_by('time')
try:
i = list(same_day_slots).index(slot)
return same_day_slots[i+1]
except IndexError:
return None
def get_times(meeting,day):
'''
Takes a Meeting object and an integer representing the week day (sunday=1).
Returns a list of tuples for use in a ChoiceField. The value is start_time,
The label is [start_time]-[end_time].
'''
# pick a random room
rooms = Room.objects.filter(meeting=meeting)
if rooms:
room = rooms[0]
else:
room = None
slots = TimeSlot.objects.filter(meeting=meeting,time__week_day=day,location=room).order_by('time')
choices = [ (t.time.strftime('%H%M'), '%s-%s' % (t.time.strftime('%H%M'), t.end_time().strftime('%H%M'))) for t in slots ]
return choices
#----------------------------------------------------------
# Base Classes
#----------------------------------------------------------
class BaseMeetingRoomFormSet(forms.models.BaseInlineFormSet):
def clean(self):
'''Check that any rooms marked for deletion are not in use'''
for form in self.deleted_forms:
room = form.cleaned_data['id']
sessions = Session.objects.filter(timeslot__location=room)
if sessions:
raise forms.ValidationError('Cannot delete meeting room %s. Already assigned to some session.' % room.name)
class TimeSlotModelChoiceField(forms.ModelChoiceField):
'''
Custom ModelChoiceField, changes the label to a more readable format
'''
def label_from_instance(self, obj):
return "%s %s - %s" % (obj.time.strftime('%a %H:%M'),obj.name,obj.location)
class TimeChoiceField(forms.ChoiceField):
'''
We are modifying the time choice field with javascript so the value submitted may not have
been in the initial select list. Just override valid_value validaion.
'''
def valid_value(self, value):
return True
#----------------------------------------------------------
# Forms
#----------------------------------------------------------
class MeetingModelForm(forms.ModelForm):
class Meta:
model = Meeting
exclude = ('type')
def clean_number(self):
number = self.cleaned_data['number']
if not number.isdigit():
raise forms.ValidationError('Meeting number must be an integer')
return number
def save(self, force_insert=False, force_update=False, commit=True):
meeting = super(MeetingModelForm, self).save(commit=False)
meeting.type_id = 'ietf'
if commit:
meeting.save()
return meeting
class MeetingRoomForm(forms.ModelForm):
class Meta:
model = Room
class ExtraSessionForm(forms.Form):
no_notify = forms.BooleanField(required=False, label="Do NOT notify this action")
class NewSessionForm(forms.Form):
day = forms.ChoiceField(choices=SESSION_DAYS)
time = TimeChoiceField()
room = forms.ModelChoiceField(queryset=Room.objects.none)
session = forms.CharField(widget=forms.HiddenInput)
note = forms.CharField(max_length=255, required=False, label='Special Note from Scheduler')
combine = forms.BooleanField(required=False, label='Combine with next session')
# setup the timeslot options based on meeting passed in
def __init__(self,*args,**kwargs):
self.meeting = kwargs.pop('meeting')
super(NewSessionForm, self).__init__(*args,**kwargs)
# attach session object to the form so we can use it in the template
self.session_object = Session.objects.get(id=self.initial['session'])
self.fields['room'].queryset = Room.objects.filter(meeting=self.meeting)
self.fields['time'].choices = get_times(self.meeting,self.initial['day'])
def clean(self):
super(NewSessionForm, self).clean()
if any(self.errors):
return
cleaned_data = self.cleaned_data
time = cleaned_data['time']
day = cleaned_data['day']
room = cleaned_data['room']
if cleaned_data['combine']:
# calculate datetime object from inputs, get current slot, feed to get_next_slot()
day_obj = self.meeting.get_meeting_date(int(day)-1)
hour = datetime.time(int(time[:2]),int(time[2:]))
time_obj = datetime.datetime.combine(day_obj,hour)
slot = TimeSlot.objects.get(meeting=self.meeting,time=time_obj,location=room)
next_slot = get_next_slot(slot)
if not next_slot or next_slot.session != None:
raise forms.ValidationError('There is no next session to combine')
return cleaned_data
class NonSessionEditForm(forms.Form):
name = forms.CharField(help_text='Name that appears on the agenda')
short = forms.CharField(max_length=32,label='Short Name',help_text='Enter an abbreviated session name (used for material file names)')
location = forms.ModelChoiceField(queryset=Room.objects)
group = forms.ModelChoiceField(queryset=Group.objects.filter(acronym__in=('edu','ietf','iepg','tools','iesg','iab','iaoc')),
help_text='''Select a group to associate with this session. For example:<br>
Tutorials = Education,<br>
Code Sprint = Tools Team,<br>
Technical Plenary = IAB,<br>
Administrative Plenary = IAOC or IESG''',empty_label=None)
def __init__(self,*args,**kwargs):
meeting = kwargs.pop('meeting')
self.session = kwargs.pop('session')
super(NonSessionEditForm, self).__init__(*args,**kwargs)
self.fields['location'].queryset = Room.objects.filter(meeting=meeting)
def clean_group(self):
group = self.cleaned_data['group']
if self.session.group != group and self.session.materials.all():
raise forms.ValidationError("ERROR: can't change group after materials have been uploaded")
return group
class TimeSlotForm(forms.Form):
day = forms.ChoiceField(choices=DAYS_CHOICES)
time = forms.TimeField()
duration = TimedeltaFormField(widget=TimedeltaWidget(attrs={'inputs':['hours','minutes']}))
name = forms.CharField(help_text='Name that appears on the agenda')
class NonSessionForm(TimeSlotForm):
short = forms.CharField(max_length=32,label='Short Name',help_text='Enter an abbreviated session name (used for material file names)',required=False)
type = forms.ModelChoiceField(queryset=TimeSlotTypeName.objects.filter(slug__in=('other','reg','break','plenary')),empty_label=None)
group = forms.ModelChoiceField(queryset=Group.objects.filter(acronym__in=('edu','ietf','iepg','tools','iesg','iab','iaoc')),help_text='Required for Session types: other, plenary',required=False)
show_location = forms.BooleanField(required=False)
def clean(self):
super(NonSessionForm, self).clean()
if any(self.errors):
return
cleaned_data = self.cleaned_data
group = cleaned_data['group']
type = cleaned_data['type']
short = cleaned_data['short']
if type.slug in ('other','plenary') and not group:
raise forms.ValidationError('ERROR: a group selection is required')
if type.slug in ('other','plenary') and not short:
raise forms.ValidationError('ERROR: a short name is required')
return cleaned_data
class UploadBlueSheetForm(forms.Form):
file = forms.FileField(help_text='example: bluesheets-84-ancp-01.pdf')
def clean_file(self):
file = self.cleaned_data['file']
if not re.match(r'bluesheets-\d+',file.name):
raise forms.ValidationError('Incorrect filename format')
return file

View file

@ -0,0 +1,54 @@
from django.db import models
#from sec.core.models import Meeting
"""
import datetime
class GeneralInfo(models.Model):
id = models.IntegerField(primary_key=True)
info_name = models.CharField(max_length=150, blank=True)
info_text = models.TextField(blank=True)
info_header = models.CharField(max_length=765, blank=True)
class Meta:
db_table = u'general_info'
class MeetingVenue(models.Model):
meeting_num = models.ForeignKey(Meeting, db_column='meeting_num', unique=True, editable=False)
break_area_name = models.CharField(max_length=255)
reg_area_name = models.CharField(max_length=255)
def __str__(self):
return "IETF %s" % (self.meeting_num_id)
class Meta:
db_table = 'meeting_venues'
verbose_name = "Meeting public areas"
verbose_name_plural = "Meeting public areas"
class NonSessionRef(models.Model):
name = models.CharField(max_length=255)
def __str__(self):
return self.name
class Meta:
db_table = 'non_session_ref'
verbose_name = "Non-session slot name"
class NonSession(models.Model):
non_session_id = models.AutoField(primary_key=True, editable=False)
day_id = models.IntegerField(blank=True, null=True, editable=False)
non_session_ref = models.ForeignKey(NonSessionRef, editable=False)
meeting = models.ForeignKey(Meeting, db_column='meeting_num', editable=False)
time_desc = models.CharField(blank=True, max_length=75, default='0')
show_break_location = models.BooleanField(editable=False, default=True)
def __str__(self):
if self.day_id != None:
return "%s %s %s @%s" % ((self.meeting.start_date + datetime.timedelta(self.day_id)).strftime('%A'), self.time_desc, self.non_session_ref, self.meeting_id)
else:
return "** %s %s @%s" % (self.time_desc, self.non_session_ref, self.meeting_id)
def day(self):
if self.day_id != None:
return (self.meeting.start_date + datetime.timedelta(self.day_id)).strftime('%A')
else:
return ""
class Meta:
db_table = 'non_session'
verbose_name = "Meeting non-session slot"
"""

View file

@ -0,0 +1,26 @@
from django.core.urlresolvers import reverse
from django.test import TestCase
from ietf.meeting.models import Meeting
from ietf.utils.test_data import make_test_data
from pyquery import PyQuery
SECR_USER='secretary'
class MainTestCase(TestCase):
fixtures = ['names']
def test_main(self):
"Main Test"
url = reverse('meetings')
response = self.client.get(url, REMOTE_USER=SECR_USER)
self.assertEquals(response.status_code, 200)
def test_view(self):
"View Test"
draft = make_test_data()
meeting = Meeting.objects.all()[0]
url = reverse('meetings_view', kwargs={'meeting_id':meeting.number})
response = self.client.get(url, REMOTE_USER=SECR_USER)
self.assertEquals(response.status_code, 200)

View file

@ -0,0 +1,22 @@
from django.conf.urls.defaults import *
urlpatterns = patterns('ietf.secr.meetings.views',
url(r'^$', 'main', name='meetings'),
url(r'^add/$', 'add', name='meetings_add'),
url(r'^ajax/get-times/(?P<meeting_id>\d{1,6})/(?P<day>\d)/$', 'ajax_get_times', name='meetings_ajax_get_times'),
url(r'^blue_sheet/$', 'blue_sheet_redirect', name='meetings_blue_sheet_redirect'),
url(r'^(?P<meeting_id>\d{1,6})/$', 'view', name='meetings_view'),
url(r'^(?P<meeting_id>\d{1,6})/blue_sheet/$', 'blue_sheet', name='meetings_blue_sheet'),
url(r'^(?P<meeting_id>\d{1,6})/blue_sheet/generate/$', 'blue_sheet_generate', name='meetings_blue_sheet_generate'),
url(r'^(?P<meeting_id>\d{1,6})/edit/$', 'edit_meeting', name='meetings_edit_meeting'),
url(r'^(?P<meeting_id>\d{1,6})/rooms/$', 'rooms', name='meetings_rooms'),
url(r'^(?P<meeting_id>\d{1,6})/times/$', 'times', name='meetings_times'),
url(r'^(?P<meeting_id>\d{1,6})/times/delete/(?P<time>[0-9\:]+)/$', 'times_delete', name='meetings_times_delete'),
url(r'^(?P<meeting_id>\d{1,6})/non_session/$', 'non_session', name='meetings_non_session'),
url(r'^(?P<meeting_id>\d{1,6})/non_session/edit/(?P<slot_id>\d{1,6})/$', 'non_session_edit', name='meetings_non_session_edit'),
url(r'^(?P<meeting_id>\d{1,6})/non_session/delete/(?P<slot_id>\d{1,6})/$', 'non_session_delete', name='meetings_non_session_delete'),
url(r'^(?P<meeting_id>\d{1,6})/select/$', 'select_group',
name='meetings_select_group'),
url(r'^(?P<meeting_id>\d{1,6})/(?P<acronym>[A-Za-z0-9_\-\+]+)/schedule/$', 'schedule', name='meetings_schedule'),
url(r'^(?P<meeting_id>\d{1,6})/(?P<acronym>[A-Za-z0-9_\-\+]+)/remove/$', 'remove_session', name='meetings_remove_session'),
)

861
ietf/secr/meetings/views.py Normal file
View file

@ -0,0 +1,861 @@
from django.conf import settings
from django.contrib import messages
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
from django.core.urlresolvers import reverse
from django.db.models import Max, Min, Q
from django.forms.formsets import formset_factory
from django.forms.models import inlineformset_factory, modelformset_factory
from django.http import HttpResponseRedirect, HttpResponse
from django.shortcuts import render_to_response, get_object_or_404
from django.template import RequestContext
from django.utils.functional import curry
from django.utils import simplejson
from ietf.utils.mail import send_mail
from ietf.meeting.models import Meeting, Session, Room, TimeSlot
from ietf.group.models import Group
from ietf.name.models import SessionStatusName, TimeSlotTypeName
from ietf.person.models import Person
from ietf.secr.meetings.blue_sheets import create_blue_sheets
from ietf.secr.proceedings.views import build_choices, handle_upload_file
from ietf.secr.sreq.forms import GroupSelectForm
from ietf.secr.sreq.views import get_initial_session, session_conflicts_as_string
from ietf.secr.utils.mail import get_cc_list
from ietf.secr.utils.meeting import get_upload_root
from forms import *
import os
import datetime
# --------------------------------------------------
# Helper Functions
# --------------------------------------------------
def build_timeslots(meeting,room=None):
'''
This function takes a Meeting object and an optional room argument. If room isn't passed we
pre-create the full set of timeslot records using the last meeting as a template.
If room is passed pre-create timeslots for the new room. Call this after saving new rooms
or adding a room.
'''
slots = meeting.timeslot_set.filter(type='session')
if room:
rooms = [room]
else:
rooms = meeting.room_set.all()
if not slots or room:
# if we are just building timeslots for a new room, the room argument was passed,
# then we need to use current meeting times as a template, not the last meeting times
if room:
source_meeting = meeting
else:
source_meeting = get_last_meeting(meeting)
delta = meeting.date - source_meeting.date
initial = []
timeslots = []
time_seen = set()
for t in source_meeting.timeslot_set.filter(type='session'):
if not t.time in time_seen:
time_seen.add(t.time)
timeslots.append(t)
for t in timeslots:
new_time = t.time + delta
for room in rooms:
TimeSlot.objects.create(type_id='session',
meeting=meeting,
name=t.name,
time=new_time,
location=room,
duration=t.duration)
def build_nonsession(meeting):
'''
This function takes a meeting object and creates non-session records
for a new meeting, based on the last meeting
'''
last_meeting = get_last_meeting(meeting)
delta = meeting.date - last_meeting.date
system = Person.objects.get(name='(system)')
for slot in TimeSlot.objects.filter(meeting=last_meeting,type__in=('break','reg','other','plenary')):
new_time = slot.time + delta
session = None
# create Session object for Tutorials to hold materials
if slot.type.slug in ('other','plenary'):
session = Session(meeting=meeting,
name=slot.name,
short=slot.session.short,
group=slot.session.group,
requested_by=system,
status_id='sched')
session.save()
TimeSlot.objects.create(type=slot.type,
meeting=meeting,
session=session,
name=slot.name,
time=new_time,
duration=slot.duration,
show_location=slot.show_location)
def get_last_meeting(meeting):
last_number = int(meeting.number) - 1
return Meeting.objects.get(number=last_number)
def is_combined(session):
'''
Check to see if this session is using two combined timeslots
'''
if session.timeslot_set.count() > 1:
return True
else:
return False
def make_directories(meeting):
'''
This function takes a meeting object and creates the appropriate materials directories
'''
path = get_upload_root(meeting)
os.umask(0)
if not os.path.exists(path):
os.makedirs(path)
for d in ('slides','agenda','minutes','id','rfc','bluesheets'):
if not os.path.exists(os.path.join(path,d)):
os.mkdir(os.path.join(path,d))
def send_notification(request, sessions):
'''
This view generates notifications for schedule sessions
'''
session_info_template = '''{0} Session {1} ({2})
{3}, {4} {5}
Room Name: {6}
---------------------------------------------
'''
group = sessions[0].group
to_email = sessions[0].requested_by.role_email('chair').address
cc_list = get_cc_list(group, request.user.get_profile())
from_email = ('"IETF Secretariat"','agenda@ietf.org')
if sessions.count() == 1:
subject = '%s - Requested session has been scheduled for IETF %s' % (group.acronym, sessions[0].meeting.number)
else:
subject = '%s - Requested sessions have been scheduled for IETF %s' % (group.acronym, sessions[0].meeting.number)
template = 'meetings/session_schedule_notification.txt'
# easier to populate template from timeslot perspective. assuming one-to-one timeslot-session
count = 0
session_info = ''
data = [ (s,s.timeslot_set.all()[0]) for s in sessions ]
for s,t in data:
count += 1
session_info += session_info_template.format(group.acronym,
count,
s.requested_duration,
t.time.strftime('%A'),
t.name,
'%s-%s' % (t.time.strftime('%H%M'),(t.time + t.duration).strftime('%H%M')),
t.location)
# send email
context = {}
context['to_name'] = sessions[0].requested_by
context['agenda_note'] = sessions[0].agenda_note
context['session'] = get_initial_session(sessions)
context['session_info'] = session_info
send_mail(request,
to_email,
from_email,
subject,
template,
context,
cc=cc_list)
def sort_groups(meeting):
'''
Similar to sreq.views.sort_groups
Takes a Django User object and a Meeting object
Returns a tuple scheduled_groups, unscheduled groups.
'''
scheduled_groups = []
unscheduled_groups = []
#sessions = Session.objects.filter(meeting=meeting,status__in=('schedw','apprw','appr','sched','notmeet','canceled'))
sessions = Session.objects.filter(meeting=meeting,status__in=('schedw','apprw','appr','sched','canceled'))
groups_with_sessions = [ s.group for s in sessions ]
gset = set(groups_with_sessions)
sorted_groups_with_sessions = sorted(gset, key = lambda instance: instance.acronym)
slots = TimeSlot.objects.filter(meeting=meeting,session__isnull=False)
groups_with_timeslots = [ s.session.group for s in slots ]
for group in sorted_groups_with_sessions:
if group in groups_with_timeslots:
scheduled_groups.append(group)
else:
unscheduled_groups.append(group)
return scheduled_groups, unscheduled_groups
# -------------------------------------------------
# AJAX Functions
# -------------------------------------------------
def ajax_get_times(request, meeting_id, day):
'''
Ajax function to get timeslot times for a given day.
returns JSON format response: [{id:start_time, value:start_time-end_time},...]
'''
# TODO strip duplicates if there are any
results=[]
room = Room.objects.filter(meeting__number=meeting_id)[0]
slots = TimeSlot.objects.filter(meeting__number=meeting_id,time__week_day=day,location=room).order_by('time')
for slot in slots:
d = {'id': slot.time.strftime('%H%M'), 'value': '%s-%s' % (slot.time.strftime('%H%M'), slot.end_time().strftime('%H%M'))}
results.append(d)
return HttpResponse(simplejson.dumps(results), mimetype='application/javascript')
# --------------------------------------------------
# STANDARD VIEW FUNCTIONS
# --------------------------------------------------
def add(request):
'''
Add a new IETF Meeting. Creates Meeting and Proceeding objects.
**Templates:**
* ``meetings/add.html``
**Template Variables:**
* proceedingform
'''
if request.method == 'POST':
button_text = request.POST.get('submit', '')
if button_text == 'Cancel':
url = reverse('meetings')
return HttpResponseRedirect(url)
form = MeetingModelForm(request.POST)
if form.is_valid():
meeting = form.save()
#Create Physical new meeting directory and subdirectories
make_directories(meeting)
messages.success(request, 'The Meeting was created successfully!')
url = reverse('meetings')
return HttpResponseRedirect(url)
else:
# display initial forms
max_number = Meeting.objects.filter(type='ietf').aggregate(Max('number'))['number__max']
form = MeetingModelForm(initial={'number':int(max_number) + 1})
return render_to_response('meetings/add.html', {
'form': form},
RequestContext(request, {}),
)
def blue_sheet(request, meeting_id):
'''
Blue Sheet view. The user can generate blue sheets or upload scanned bluesheets
'''
meeting = get_object_or_404(Meeting, number=meeting_id)
url = settings.SECR_BLUE_SHEET_URL
if request.method == 'POST':
form = UploadBlueSheetForm(request.POST,request.FILES)
if form.is_valid():
file = request.FILES['file']
handle_upload_file(file,file.name,meeting,'bluesheets')
messages.success(request, 'File Uploaded')
url = reverse('meetings_blue_sheet', kwargs={'meeting_id':meeting.number})
return HttpResponseRedirect(url)
else:
form = UploadBlueSheetForm()
return render_to_response('meetings/blue_sheet.html', {
'meeting': meeting,
'url': url,
'form': form},
RequestContext(request, {}),
)
def blue_sheet_generate(request, meeting_id):
'''
Generate bluesheets
'''
meeting = get_object_or_404(Meeting, number=meeting_id)
groups = Group.objects.filter(session__meeting=meeting).order_by('acronym')
create_blue_sheets(meeting, groups)
messages.success(request, 'Blue Sheets generated')
url = reverse('meetings_blue_sheet', kwargs={'meeting_id':meeting.number})
return HttpResponseRedirect(url)
def blue_sheet_redirect(request):
'''
This is the generic blue sheet URL. It gets the next IETF meeting and redirects
to the meeting specific URL.
'''
today = datetime.date.today()
qs = Meeting.objects.filter(date__gt=today,type='ietf').order_by('date')
if qs:
meeting = qs[0]
else:
meeting = Meeting.objects.filter(type='ietf').order_by('-date')[0]
url = reverse('meetings_blue_sheet', kwargs={'meeting_id':meeting.number})
return HttpResponseRedirect(url)
def edit_meeting(request, meeting_id):
'''
Edit Meeting information.
**Templates:**
* ``meetings/meeting_edit.html``
**Template Variables:**
* meeting, form
'''
meeting = get_object_or_404(Meeting, number=meeting_id)
if request.method == 'POST':
button_text = request.POST.get('submit','')
if button_text == 'Save':
form = MeetingModelForm(request.POST, instance=meeting)
if form.is_valid():
form.save()
messages.success(request,'The meeting entry was changed successfully')
url = reverse('meetings_view', kwargs={'meeting_id':meeting_id})
return HttpResponseRedirect(url)
else:
url = reverse('meetings_view', kwargs={'meeting_id':meeting_id})
return HttpResponseRedirect(url)
else:
form = MeetingModelForm(instance=meeting)
return render_to_response('meetings/edit_meeting.html', {
'meeting': meeting,
'form' : form, },
RequestContext(request,{}),
)
def main(request):
'''
In this view the user can choose a meeting to manage or elect to create a new meeting.
'''
meetings = Meeting.objects.filter(type='ietf').order_by('-number')
if request.method == 'POST':
redirect_url = reverse('meetings_view', kwargs={'meeting_id':request.POST['group']})
return HttpResponseRedirect(redirect_url)
choices = [ (str(x.number),str(x.number)) for x in meetings ]
form = GroupSelectForm(choices=choices)
return render_to_response('meetings/main.html', {
'form': form,
'meetings': meetings},
RequestContext(request, {}),
)
def non_session(request, meeting_id):
'''
Display and add "non-session" time slots, ie. registration, beverage and snack breaks
'''
meeting = get_object_or_404(Meeting, number=meeting_id)
# if the Break/Registration records don't exist yet (new meeting) create them
if not TimeSlot.objects.filter(meeting=meeting,type__in=('break','reg','other')):
build_nonsession(meeting)
slots = TimeSlot.objects.filter(meeting=meeting,type__in=('break','reg','other','plenary')).order_by('-type__name','time')
if request.method == 'POST':
form = NonSessionForm(request.POST)
if form.is_valid():
day = form.cleaned_data['day']
time = form.cleaned_data['time']
name = form.cleaned_data['name']
short = form.cleaned_data['short']
type = form.cleaned_data['type']
group = form.cleaned_data['group']
duration = form.cleaned_data['duration']
t = meeting.date + datetime.timedelta(days=int(day))
new_time = datetime.datetime(t.year,t.month,t.day,time.hour,time.minute)
# create a dummy Session object to hold materials
# NOTE: we're setting group to none here, but the set_room page will force user
# to pick a legitimate group
session = None
if type.slug in ('other','plenary'):
session = Session(meeting=meeting,
name=name,
short=short,
group=group,
requested_by=Person.objects.get(name='(system)'),
status_id='sched')
session.save()
# create TimeSlot object
TimeSlot.objects.create(type=form.cleaned_data['type'],
meeting=meeting,
session=session,
name=name,
time=new_time,
duration=duration,
show_location=form.cleaned_data['show_location'])
messages.success(request, 'Non-Sessions updated successfully')
url = reverse('meetings_non_session', kwargs={'meeting_id':meeting_id})
return HttpResponseRedirect(url)
else:
form = NonSessionForm(initial={'show_location':True})
if TimeSlot.objects.filter(meeting=meeting,type='other',location__isnull=True):
messages.warning(request, 'There are non-session items which do not have a room assigned')
return render_to_response('meetings/non_session.html', {
'slots': slots,
'form': form,
'meeting': meeting},
RequestContext(request, {}),
)
def non_session_delete(request, meeting_id, slot_id):
'''
This function deletes the non-session TimeSlot. For "other" and "plenary" timeslot types
we need to delete the corresponding Session object as well. Check for uploaded material
first.
'''
slot = get_object_or_404(TimeSlot, id=slot_id)
if slot.type_id in ('other','plenary'):
if slot.session.materials.exclude(states__slug='deleted'):
messages.error(request, 'Materials have already been uploaded for "%s". You must delete those before deleting the timeslot.' % slot.name)
url = reverse('meetings_non_session', kwargs={'meeting_id':meeting_id})
return HttpResponseRedirect(url)
else:
slot.session.delete()
slot.delete()
messages.success(request, 'Non-Session timeslot deleted successfully')
url = reverse('meetings_non_session', kwargs={'meeting_id':meeting_id})
return HttpResponseRedirect(url)
def non_session_edit(request, meeting_id, slot_id):
'''
Allows the user to assign a location to this non-session timeslot
'''
meeting = get_object_or_404(Meeting, number=meeting_id)
slot = get_object_or_404(TimeSlot, id=slot_id)
if request.method == 'POST':
button_text = request.POST.get('submit', '')
if button_text == 'Cancel':
url = reverse('meetings_non_session', kwargs={'meeting_id':meeting_id})
return HttpResponseRedirect(url)
form = NonSessionEditForm(request.POST,meeting=meeting, session=slot.session)
if form.is_valid():
location = form.cleaned_data['location']
group = form.cleaned_data['group']
name = form.cleaned_data['name']
short = form.cleaned_data['short']
slot.location = location
slot.name = name
slot.save()
# save group to session object
session = slot.session
session.group = group
session.name = name
session.short = short
session.save()
messages.success(request, 'Location saved')
url = reverse('meetings_non_session', kwargs={'meeting_id':meeting_id})
return HttpResponseRedirect(url)
else:
# we need to pass the session to the form in order to disallow changing
# of group after materials have been uploaded
initial = {'location':slot.location,
'group':slot.session.group,
'name':slot.session.name,
'short':slot.session.short}
form = NonSessionEditForm(meeting=meeting,session=slot.session,initial=initial)
return render_to_response('meetings/non_session_edit.html', {
'meeting': meeting,
'form': form,
'slot': slot},
RequestContext(request, {}),
)
def remove_session(request, meeting_id, acronym):
'''
Remove session from agenda. Disassociate session from timeslot and set status.
According to Wanda this option is used when people cancel, so the Session
request should be deleted as well.
'''
meeting = get_object_or_404(Meeting, number=meeting_id)
group = get_object_or_404(Group, acronym=acronym)
sessions = Session.objects.filter(meeting=meeting,group=group)
now = datetime.datetime.now()
for session in sessions:
timeslot = session.timeslot_set.all()[0]
timeslot.session = None
timeslot.modified = now
timeslot.save()
session.status_id = 'canceled'
session.modified = now
session.save()
messages.success(request, '%s Session removed from agenda' % (group.acronym))
url = reverse('meetings_select_group', kwargs={'meeting_id':meeting.number})
return HttpResponseRedirect(url)
def rooms(request, meeting_id):
'''
Display and edit MeetingRoom records for the specified meeting
'''
meeting = get_object_or_404(Meeting, number=meeting_id)
# if no rooms exist yet (new meeting) formset extra=10
first_time = not bool(meeting.room_set.all())
extra = 10 if first_time else 0
RoomFormset = inlineformset_factory(Meeting, Room, form=MeetingRoomForm, formset=BaseMeetingRoomFormSet, can_delete=True, extra=extra)
if request.method == 'POST':
button_text = request.POST.get('submit', '')
if button_text == 'Cancel':
url = reverse('meetings', kwargs={'meeting_id':meeting_id})
return HttpResponseRedirect(url)
formset = RoomFormset(request.POST, instance=meeting, prefix='room')
if formset.is_valid():
formset.save()
# if we are creating rooms for the first time create full set of timeslots
if first_time:
build_timeslots(meeting)
# otherwise if we're modifying rooms
else:
# add timeslots for new rooms, deleting rooms automatically deletes timeslots
for form in formset.forms[formset.initial_form_count():]:
if form.instance.pk:
build_timeslots(meeting,room=form.instance)
messages.success(request, 'Meeting Rooms changed successfully')
url = reverse('meetings_rooms', kwargs={'meeting_id':meeting_id})
return HttpResponseRedirect(url)
else:
formset = RoomFormset(instance=meeting, prefix='room')
return render_to_response('meetings/rooms.html', {
'meeting': meeting,
'formset': formset},
RequestContext(request, {}),
)
def schedule(request, meeting_id, acronym):
'''
This view handles scheduling session requests to TimeSlots
'''
meeting = get_object_or_404(Meeting, number=meeting_id)
group = get_object_or_404(Group, acronym=acronym)
sessions = Session.objects.filter(meeting=meeting,group=group,status__in=('schedw','apprw','appr','sched','canceled'))
legacy_session = get_initial_session(sessions)
session_conflicts = session_conflicts_as_string(group, meeting)
now = datetime.datetime.now()
# build initial
initial = []
for s in sessions:
d = {'session':s.id,
'note':s.agenda_note}
qs = s.timeslot_set.all()
if qs:
d['room'] = qs[0].location.id
d['day'] = qs[0].time.isoweekday() % 7 + 1 # adjust to django week_day
d['time'] = qs[0].time.strftime('%H%M')
else:
d['day'] = 2
if is_combined(s):
d['combine'] = True
initial.append(d)
# need to use curry here to pass custom variable to form init
NewSessionFormset = formset_factory(NewSessionForm, extra=0)
NewSessionFormset.form = staticmethod(curry(NewSessionForm, meeting=meeting))
if request.method == 'POST':
button_text = request.POST.get('submit', '')
if button_text == 'Cancel':
url = reverse('meetings_select_group', kwargs={'meeting_id':meeting_id})
return HttpResponseRedirect(url)
formset = NewSessionFormset(request.POST,initial=initial)
extra_form = ExtraSessionForm(request.POST)
if formset.is_valid() and extra_form.is_valid():
# TODO formsets don't have has_changed until Django 1.3
has_changed = False
for form in formset.forms:
if form.has_changed():
has_changed = True
id = form.cleaned_data['session']
note = form.cleaned_data['note']
room = form.cleaned_data['room']
time = form.cleaned_data['time']
day = form.cleaned_data['day']
combine = form.cleaned_data.get('combine',None)
session = Session.objects.get(id=id)
if session.timeslot_set.all():
initial_timeslot = session.timeslot_set.all()[0]
else:
initial_timeslot = None
# find new timeslot
new_day = meeting.date + datetime.timedelta(days=int(day)-1)
hour = datetime.time(int(time[:2]),int(time[2:]))
new_time = datetime.datetime.combine(new_day,hour)
qs = TimeSlot.objects.filter(meeting=meeting,time=new_time,location=room)
if qs.filter(session=None):
timeslot = qs.filter(session=None)[0]
else:
# we need to create another, identical timeslot
timeslot = TimeSlot.objects.create(meeting=qs[0].meeting,
type=qs[0].type,
name=qs[0].name,
time=qs[0].time,
duration=qs[0].duration,
location=qs[0].location,
show_location=qs[0].show_location,
modified=now)
messages.warning(request, 'WARNING: There are now two sessions scheduled for the timeslot: %s' % timeslot)
if any(x in form.changed_data for x in ('day','time','room')):
# clear the old timeslot
if initial_timeslot:
# if the initial timeslot is one of multiple we should delete it
tqs = TimeSlot.objects.filter(meeting=meeting,
type='session',
time=initial_timeslot.time,
location=initial_timeslot.location)
if tqs.count() > 1:
initial_timeslot.delete()
else:
initial_timeslot.session = None
initial_timeslot.modified = now
initial_timeslot.save()
if timeslot:
timeslot.session = session
timeslot.modified = now
timeslot.save()
session.status_id = 'sched'
else:
session.status_id = 'schedw'
session.modified = now
session.save()
if 'note' in form.changed_data:
session.agenda_note = note
session.modified = now
session.save()
# COMBINE SECTION ----------------------
if 'combine' in form.changed_data:
next_slot = get_next_slot(timeslot)
if combine:
next_slot.session = session
else:
next_slot.session = None
next_slot.modified = now
next_slot.save()
# ---------------------------------------
# notify. dont send if Tutorial, BOF or indicated on form
notification_message = "No notification has been sent to anyone for this session."
if (has_changed
and not extra_form.cleaned_data.get('no_notify',False)
and group.state.slug != 'bof'
and session.timeslot_set.all()): # and the session is scheduled, else skip
send_notification(request, sessions)
notification_message = "Notification sent."
if has_changed:
messages.success(request, 'Session(s) Scheduled for %s. %s' % (group.acronym, notification_message))
url = reverse('meetings_select_group', kwargs={'meeting_id':meeting_id})
return HttpResponseRedirect(url)
else:
formset = NewSessionFormset(initial=initial)
extra_form = ExtraSessionForm()
return render_to_response('meetings/schedule.html', {
'extra_form': extra_form,
'group': group,
'meeting': meeting,
'show_request': True,
'session': legacy_session,
'formset': formset},
RequestContext(request, {}),
)
def select_group(request, meeting_id):
'''
In this view the user can select the group to schedule. Only those groups that have
submitted session requests appear in the dropdowns.
NOTE: BOF list includes Proposed Working Group type, per Wanda
'''
meeting = get_object_or_404(Meeting, number=meeting_id)
if request.method == 'POST':
group = request.POST.get('group',None)
if group:
redirect_url = reverse('meetings_schedule', kwargs={'meeting_id':meeting_id,'acronym':group})
else:
redirect_url = reverse('meetings_select_group',kwargs={'meeting_id':meeting_id})
messages.error(request, 'No group selected')
return HttpResponseRedirect(redirect_url)
# split groups into scheduled / unscheduled
scheduled_groups, unscheduled_groups = sort_groups(meeting)
# prep group form
wgs = filter(lambda a: a.type_id in ('wg','ag') and a.state_id=='active', unscheduled_groups)
group_form = GroupSelectForm(choices=build_choices(wgs))
# prep BOFs form
bofs = filter(lambda a: a.type_id=='wg' and a.state_id in ('bof','proposed'), unscheduled_groups)
bof_form = GroupSelectForm(choices=build_choices(bofs))
# prep IRTF form
irtfs = filter(lambda a: a.type_id=='rg' and a.state_id in ('active','proposed'), unscheduled_groups)
irtf_form = GroupSelectForm(choices=build_choices(irtfs))
return render_to_response('meetings/select_group.html', {
'group_form': group_form,
'bof_form': bof_form,
'irtf_form': irtf_form,
'scheduled_groups': scheduled_groups,
'meeting': meeting},
RequestContext(request, {}),
)
def times(request, meeting_id):
'''
Display and edit time slots (TimeSlots). It doesn't display every TimeSlot
object for the meeting because there is one timeslot per time per room,
rather it displays all the unique times.
The first time this view is called for a meeting it creates a form with times
prepopulated from the last meeting
'''
meeting = get_object_or_404(Meeting, number=meeting_id)
# build list of timeslots
slots = []
timeslots = []
time_seen = set()
for t in meeting.timeslot_set.filter(type='session'):
if not t.time in time_seen:
time_seen.add(t.time)
timeslots.append(t)
for t in timeslots:
slots.append({'name':t.name,
'time':t.time,
'end_time':t.end_time()})
times = sorted(slots, key=lambda a: a['time'])
if request.method == 'POST':
form = TimeSlotForm(request.POST)
if form.is_valid():
day = form.cleaned_data['day']
time = form.cleaned_data['time']
duration = form.cleaned_data['duration']
name = form.cleaned_data['name']
t = meeting.date + datetime.timedelta(days=int(day))
new_time = datetime.datetime(t.year,t.month,t.day,time.hour,time.minute)
# don't allow creation of timeslots with same start time as existing timeslots
# assert False, (new_time, time_seen)
if new_time in time_seen:
messages.error(request, 'There is already a timeslot for %s. To change you must delete the old one first.' % new_time.strftime('%a %H:%M'))
url = reverse('meetings_times', kwargs={'meeting_id':meeting_id})
return HttpResponseRedirect(url)
for room in meeting.room_set.all():
TimeSlot.objects.create(type_id='session',
meeting=meeting,
name=name,
time=new_time,
location=room,
duration=duration)
messages.success(request, 'Timeslots created')
url = reverse('meetings_times', kwargs={'meeting_id':meeting_id})
return HttpResponseRedirect(url)
else:
form = TimeSlotForm()
return render_to_response('meetings/times.html', {
'form': form,
'meeting': meeting,
'times': times},
RequestContext(request, {}),
)
def times_delete(request, meeting_id, time):
'''
This view handles bulk delete of all timeslots matching time (datetime) for the given
meeting. There is one timeslot for each room.
'''
meeting = get_object_or_404(Meeting, number=meeting_id)
parts = [ int(x) for x in time.split(':') ]
dtime = datetime.datetime(*parts)
if Session.objects.filter(timeslot__time=dtime,timeslot__meeting=meeting):
messages.error(request, 'ERROR deleting timeslot. There is one or more sessions scheduled for this timeslot.')
url = reverse('meetings_times', kwargs={'meeting_id':meeting_id})
return HttpResponseRedirect(url)
TimeSlot.objects.filter(meeting=meeting,time=dtime).delete()
messages.success(request, 'Timeslot deleted')
url = reverse('meetings_times', kwargs={'meeting_id':meeting_id})
return HttpResponseRedirect(url)
def view(request, meeting_id):
'''
View Meeting information.
**Templates:**
* ``meetings/view.html``
**Template Variables:**
* meeting , proceeding
'''
meeting = get_object_or_404(Meeting, number=meeting_id)
return render_to_response('meetings/view.html', {
'meeting': meeting},
RequestContext(request, {}),
)

View file

View file

@ -0,0 +1,58 @@
from django.conf import settings
from django.http import HttpResponseForbidden
from django.shortcuts import render_to_response
from ietf.ietfauth.decorators import has_role
import re
class SecAuthMiddleware(object):
"""
Middleware component that performs custom auth check for every
request except those excluded by SECR_AUTH_UNRESTRICTED_URLS.
Since authentication is performed externally at the apache level
REMOTE_USER should contain the name of the authenticated
user. If the user is a secretariat than access is granted.
Otherwise return a 401 error page.
To use, add the class to MIDDLEWARE_CLASSES and define
SECR_AUTH_UNRESTRICTED_URLS in your settings.py.
The following example allows access to anything under "/interim/"
to non-secretariat users:
SECR_AUTH_UNRESTRICTED_URLS = (
(r'^/interim/'),
Also sets custom request attributes:
user_is_secretariat
user_is_chair
user_is_ad
)
"""
def __init__(self):
self.unrestricted = [re.compile(pattern) for pattern in
settings.SECR_AUTH_UNRESTRICTED_URLS]
def process_view(self, request, view_func, view_args, view_kwargs):
# need to initialize user, it doesn't get set when running tests for example
if request.path.startswith('/secr/'):
user = ''
request.user_is_secretariat = False
if request.user.is_anonymous():
return render_to_response('401.html', {'user':user})
if 'REMOTE_USER' in request.META:
# do custom auth
if has_role(request.user,'Secretariat'):
request.user_is_secretariat = True
return None
return None

View file

View file

@ -0,0 +1,104 @@
from django import forms
from django.conf import settings
from django.db.models import Max
from django.template.defaultfilters import filesizeformat
from ietf.doc.models import Document
from ietf.name.models import DocTypeName
from ietf.meeting.models import Meeting
import os
import re
# ---------------------------------------------
# Globals
# ---------------------------------------------
VALID_SLIDE_EXTENSIONS = ('.doc','.docx','.pdf','.ppt','.pptx','.txt')
VALID_MINUTES_EXTENSIONS = ('.txt','.html','.htm','.pdf')
VALID_AGENDA_EXTENSIONS = ('.txt','.html','.htm')
#----------------------------------------------------------
# Forms
#----------------------------------------------------------
class EditSlideForm(forms.ModelForm):
class Meta:
model = Document
fields = ('title',)
class InterimMeetingForm(forms.Form):
date = forms.DateField(help_text="(YYYY-MM-DD Format, please)")
group_acronym_id = forms.CharField(widget=forms.HiddenInput())
def clean(self):
super(InterimMeetingForm, self).clean()
cleaned_data = self.cleaned_data
# need to use get() here, if the date field isn't valid it won't exist
date = cleaned_data.get('date','')
group_acronym_id = cleaned_data["group_acronym_id"]
qs = Meeting.objects.filter(type='interim',date=date,session__group__acronym=group_acronym_id)
if qs:
raise forms.ValidationError('A meeting already exists for this date.')
return cleaned_data
class ReplaceSlideForm(forms.ModelForm):
file = forms.FileField(label='Select File')
class Meta:
model = Document
fields = ('title',)
def clean_file(self):
file = self.cleaned_data.get('file')
ext = os.path.splitext(file.name)[1].lower()
if ext not in VALID_SLIDE_EXTENSIONS:
raise forms.ValidationError('Only these file types supported for presentation slides: %s' % ','.join(VALID_SLIDE_EXTENSIONS))
if file._size > settings.SECR_MAX_UPLOAD_SIZE:
raise forms.ValidationError('Please keep filesize under %s. Current filesize %s' % (filesizeformat(settings.SECR_MAX_UPLOAD_SIZE), filesizeformat(file._size)))
return file
class UnifiedUploadForm(forms.Form):
acronym = forms.CharField(widget=forms.HiddenInput())
meeting_id = forms.CharField(widget=forms.HiddenInput())
material_type = forms.ModelChoiceField(queryset=DocTypeName.objects.filter(name__in=('minutes','agenda','slides')),empty_label=None)
slide_name = forms.CharField(label='Name of Presentation',max_length=255,required=False,help_text="For presentations only")
file = forms.FileField(label='Select File',help_text='<div id="id_file_help">Note 1: You can only upload a presentation file in txt, pdf, doc, or ppt/pptx. System will not accept presentation files in any other format.<br><br>Note 2: All uploaded files will be available to the public immediately on the Preliminary Page. However, for the Proceedings, ppt/pptx files will be converted to html format and doc files will be converted to pdf format manually by the Secretariat staff.</div>')
def clean_file(self):
file = self.cleaned_data['file']
if file._size > settings.SECR_MAX_UPLOAD_SIZE:
raise forms.ValidationError('Please keep filesize under %s. Current filesize %s' % (filesizeformat(settings.SECR_MAX_UPLOAD_SIZE), filesizeformat(file._size)))
return file
def clean(self):
super(UnifiedUploadForm, self).clean()
# if an invalid file type is supplied no file attribute will exist
if self.errors:
return self.cleaned_data
cleaned_data = self.cleaned_data
material_type = cleaned_data['material_type']
slide_name = cleaned_data['slide_name']
file = cleaned_data['file']
ext = os.path.splitext(file.name)[1].lower()
if material_type.slug == 'slides' and not slide_name:
raise forms.ValidationError('ERROR: Name of Presentaion cannot be blank')
# only supporting PDFs per Alexa 04-05-2011
#if material_type == 1 and not file_ext[1] == '.pdf':
# raise forms.ValidationError('Presentations must be a PDF file')
# validate file extensions based on material type (presentation,agenda,minutes)
# valid extensions per online documentation: meeting-materials.html
# 09-14-11 added ppt, pdf per Alexa
# 04-19-12 txt/html for agenda, +pdf for minutes per Russ
if material_type.slug == 'slides' and ext not in VALID_SLIDE_EXTENSIONS:
raise forms.ValidationError('Only these file types supported for presentation slides: %s' % ','.join(VALID_SLIDE_EXTENSIONS))
if material_type.slug == 'agenda' and ext not in VALID_AGENDA_EXTENSIONS:
raise forms.ValidationError('Only these file types supported for agendas: %s' % ','.join(VALID_AGENDA_EXTENSIONS))
if material_type.slug == 'minutes' and ext not in VALID_MINUTES_EXTENSIONS:
raise forms.ValidationError('Only these file types supported for minutes: %s' % ','.join(VALID_MINUTES_EXTENSIONS))
return cleaned_data

View file

@ -0,0 +1,73 @@
from django.conf import settings
from django.db import models
from django.shortcuts import get_object_or_404
from ietf.meeting.models import Meeting
from ietf.secr.utils.meeting import get_upload_root
import datetime
import os
class InterimManager(models.Manager):
'''A custom manager to limit objects to type=interim'''
def get_query_set(self):
return super(InterimManager, self).get_query_set().filter(type='interim')
class InterimMeeting(Meeting):
'''
This class is a proxy of Meeting. It's purpose is to provide extra methods that are
useful for an interim meeting, to help in templates. Most information is derived from
the session associated with this meeting. We are assuming there is only one.
'''
class Meta:
proxy = True
objects = InterimManager()
def group(self):
return self.session_set.all()[0].group
def agenda(self):
session = self.session_set.all()[0]
agendas = session.materials.exclude(states__slug='deleted').filter(type='agenda')
if agendas:
return agendas[0]
else:
return None
def minutes(self):
session = self.session_set.all()[0]
minutes = session.materials.exclude(states__slug='deleted').filter(type='minutes')
if minutes:
return minutes[0]
else:
return None
def get_proceedings_path(self, group=None):
path = os.path.join(get_upload_root(self),'proceedings.html')
return path
def get_proceedings_url(self, group=None):
'''
If the proceedings file doesn't exist return empty string. For use in templates.
'''
if os.path.exists(self.get_proceedings_path()):
url = "%s/proceedings/interim/%s/%s/proceedings.html" % (
settings.MEDIA_URL,
self.date.strftime('%Y/%m/%d'),
self.group().acronym)
return url
else:
return ''
class Registration(models.Model):
rsn = models.AutoField(primary_key=True)
fname = models.CharField(max_length=255)
lname = models.CharField(max_length=255)
company = models.CharField(max_length=255)
country = models.CharField(max_length=2)
def __unicode__(self):
return "%s %s" % (fname, lname)
class Meta:
db_table = 'registrations'

View file

@ -0,0 +1,50 @@
CREATE TABLE `interim_slides` (
`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
`meeting_num` integer NOT NULL,
`group_acronym_id` integer,
`slide_num` integer,
`slide_type_id` integer NOT NULL,
`slide_name` varchar(255) NOT NULL,
`irtf` integer NOT NULL,
`interim` bool NOT NULL,
`order_num` integer,
`in_q` integer
)
;
CREATE TABLE `interim_minutes` (
`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
`meeting_num` integer NOT NULL,
`group_acronym_id` integer NOT NULL,
`filename` varchar(255) NOT NULL,
`irtf` integer NOT NULL,
`interim` bool NOT NULL
)
;
CREATE TABLE `interim_agenda` (
`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
`meeting_num` integer NOT NULL,
`group_acronym_id` integer NOT NULL,
`filename` varchar(255) NOT NULL,
`irtf` integer NOT NULL,
`interim` bool NOT NULL
)
;
CREATE TABLE `interim_meetings` (
`meeting_num` integer NOT NULL PRIMARY KEY AUTO_INCREMENT,
`start_date` date ,
`end_date` date ,
`city` varchar(255) ,
`state` varchar(255) ,
`country` varchar(255) ,
`time_zone` integer,
`ack` longtext ,
`agenda_html` longtext ,
`agenda_text` longtext ,
`future_meeting` longtext ,
`overview1` longtext ,
`overview2` longtext ,
`group_acronym_id` integer
)
;
alter table interim_meetings auto_increment=201;

View file

@ -0,0 +1,543 @@
'''
proc_utils.py
This module contains all the functions for generating static proceedings pages
'''
from django.conf import settings
from django.shortcuts import render_to_response
from ietf.group.models import Group, Role
from ietf.group.utils import get_charter_text
from ietf.meeting.models import Session, TimeSlot, Meeting
from ietf.meeting.views import agenda_info
from ietf.doc.models import Document, RelatedDocument, DocEvent
from itertools import chain
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_material
from models import InterimMeeting # proxy model
from urllib2 import urlopen
import datetime
import glob
import os
import shutil
import stat
# -------------------------------------------------
# Helper Functions
# -------------------------------------------------
def mycomp(timeslot):
'''
This takes a timeslot object and returns a key to sort by the area acronym or None
'''
try:
group = timeslot.session.group
key = '%s:%s' % (group.parent.acronym, group.acronym)
except AttributeError:
key = None
return key
def get_progress_stats(sdate,edate):
'''
This function takes a date range and produces a dictionary of statistics / objects for use
in a progress report.
'''
data = {}
data['sdate'] = sdate
data['edate'] = edate
# Activty Report Section
new_docs = Document.objects.filter(type='draft').filter(docevent__type='new_revision',
docevent__newrevisiondocevent__rev='00',
docevent__time__gte=sdate,
docevent__time__lte=edate)
data['new'] = new_docs.count()
data['updated'] = 0
data['updated_more'] = 0
for d in new_docs:
updates = d.docevent_set.filter(type='new_revision',time__gte=sdate,time__lte=edate).count()
if updates > 1:
data['updated'] += 1
if updates > 2:
data['updated_more'] +=1
# calculate total documents updated, not counting new, rev=00
result = set()
events = DocEvent.objects.filter(doc__type='draft',time__gte=sdate,time__lte=edate)
for e in events.filter(type='new_revision').exclude(newrevisiondocevent__rev='00'):
result.add(e.doc)
data['total_updated'] = len(result)
# calculate sent last call
data['last_call'] = events.filter(type='sent_last_call').count()
# calculate approved
data['approved'] = events.filter(type='iesg_approved').count()
# get 4 weeks
monday = Meeting.get_ietf_monday()
cutoff = monday + datetime.timedelta(days=3)
ff1_date = cutoff - datetime.timedelta(days=28)
ff2_date = cutoff - datetime.timedelta(days=21)
ff3_date = cutoff - datetime.timedelta(days=14)
ff4_date = cutoff - datetime.timedelta(days=7)
ff_docs = Document.objects.filter(type='draft').filter(docevent__type='new_revision',
docevent__newrevisiondocevent__rev='00',
docevent__time__gte=ff1_date,
docevent__time__lte=cutoff)
ff_new_count = ff_docs.count()
ff_new_percent = format(ff_new_count / float(data['new']),'.0%')
# calculate total documents updated in final four weeks, not counting new, rev=00
result = set()
events = DocEvent.objects.filter(doc__type='draft',time__gte=ff1_date,time__lte=cutoff)
for e in events.filter(type='new_revision').exclude(newrevisiondocevent__rev='00'):
result.add(e.doc)
ff_update_count = len(result)
ff_update_percent = format(ff_update_count / float(data['total_updated']),'.0%')
data['ff_new_count'] = ff_new_count
data['ff_new_percent'] = ff_new_percent
data['ff_update_count'] = ff_update_count
data['ff_update_percent'] = ff_update_percent
# Progress Report Section
data['docevents'] = DocEvent.objects.filter(doc__type='draft',time__gte=sdate,time__lte=edate)
data['action_events'] = data['docevents'].filter(type='iesg_approved')
data['lc_events'] = data['docevents'].filter(type='sent_last_call')
data['new_groups'] = Group.objects.filter(type='wg',
groupevent__changestategroupevent__state='active',
groupevent__time__gte=sdate,
groupevent__time__lte=edate)
data['concluded_groups'] = Group.objects.filter(type='wg',
groupevent__changestategroupevent__state='conclude',
groupevent__time__gte=sdate,
groupevent__time__lte=edate)
data['new_docs'] = Document.objects.filter(type='draft').filter(docevent__type='new_revision',
docevent__time__gte=sdate,
docevent__time__lte=edate).distinct()
data['rfcs'] = DocEvent.objects.filter(type='published_rfc',
doc__type='draft',
time__gte=sdate,
time__lte=edate)
# attach the ftp URL for use in the template
for event in data['rfcs']:
num = get_rfc_num(event.doc)
event.ftp_url = 'ftp://ftp.ietf.org/rfc/rfc%s.txt' % num
data['counts'] = {'std':data['rfcs'].filter(doc__intended_std_level__in=('ps','ds','std')).count(),
'bcp':data['rfcs'].filter(doc__intended_std_level='bcp').count(),
'exp':data['rfcs'].filter(doc__intended_std_level='exp').count(),
'inf':data['rfcs'].filter(doc__intended_std_level='inf').count()}
return data
def write_html(path,content):
f = open(path,'w')
f.write(content)
f.close()
try:
os.chmod(path, 0664)
except OSError:
pass
# -------------------------------------------------
# End Helper Functions
# -------------------------------------------------
def create_interim_directory():
'''
Create static Interim Meeting directory pages that will live in a different URL space than
the secretariat Django project
'''
# produce date sorted output
page = 'proceedings.html'
meetings = InterimMeeting.objects.order_by('-date')
response = render_to_response('proceedings/interim_directory.html',{'meetings': meetings})
path = os.path.join(settings.SECR_INTERIM_LISTING_DIR, page)
f = open(path,'w')
f.write(response.content)
f.close()
# produce group sorted output
page = 'proceedings-bygroup.html'
qs = InterimMeeting.objects.all()
meetings = sorted(qs, key=lambda a: a.group().acronym)
response = render_to_response('proceedings/interim_directory.html',{'meetings': meetings})
path = os.path.join(settings.SECR_INTERIM_LISTING_DIR, page)
f = open(path,'w')
f.write(response.content)
f.close()
def create_proceedings(meeting, group, is_final=False):
'''
This function creates the proceedings html document. It gets called anytime there is an
update to the meeting or the slides for the meeting.
NOTE: execution is aborted if the meeting is older than 79 because the format changed.
'''
# abort, proceedings from meetings before 79 have a different format, don't overwrite
if meeting.type_id == 'ietf' and int(meeting.number) < 79:
return
sessions = Session.objects.filter(meeting=meeting,group=group)
if sessions:
session = sessions[0]
agenda,minutes,slides = get_material(session)
else:
agenda = None
minutes = None
slides = None
chairs = group.role_set.filter(name='chair')
secretaries = group.role_set.filter(name='secr')
if group.parent: # Certain groups like Tools Team do no have a parent
ads = group.parent.role_set.filter(name='ad')
else:
ads = None
tas = group.role_set.filter(name='techadv')
docs = Document.objects.filter(group=group,type='draft').order_by('time')
meeting_root = get_upload_root(meeting)
if meeting.type.slug == 'ietf':
url_root = "%s/proceedings/%s/" % (settings.MEDIA_URL,meeting.number)
else:
url_root = "%s/proceedings/interim/%s/%s/" % (
settings.MEDIA_URL,
meeting.date.strftime('%Y/%m/%d'),
group.acronym)
# Only do these tasks if we are running official proceedings generation,
# otherwise skip them for expediency. This procedure is called any time meeting
# materials are uploaded/deleted, and we don't want to do all this work each time.
if is_final:
# ----------------------------------------------------------------------
# Find active Drafts and RFCs, copy them to id and rfc directories
drafts = docs.filter(states__slug='active')
for draft in drafts:
source = os.path.join(draft.get_file_path(),draft.filename_with_rev())
target = os.path.join(meeting_root,'id')
if not os.path.exists(target):
os.makedirs(target)
if os.path.exists(source):
shutil.copy(source,target)
draft.bytes = os.path.getsize(source)
else:
draft.bytes = 0
draft.url = url_root + "id/%s" % draft.filename_with_rev()
rfcs = docs.filter(states__slug='rfc')
for rfc in rfcs:
# TODO should use get_file_path() here but is incorrect for rfcs
rfc_num = get_rfc_num(rfc)
filename = "rfc%s.txt" % rfc_num
alias = rfc.docalias_set.filter(name='rfc%s' % rfc_num)
source = os.path.join(settings.RFC_PATH,filename)
target = os.path.join(meeting_root,'rfc')
rfc.rmsg = ''
rfc.msg = ''
if not os.path.exists(target):
os.makedirs(target)
shutil.copy(source,target)
rfc.url = url_root + "rfc/%s" % filename
rfc.bytes = os.path.getsize(source)
rfc.num = "RFC %s" % rfc_num
# check related documents
# check obsoletes
related = rfc.relateddocument_set.all()
for item in related.filter(relationship='obs'):
rfc.msg += 'obsoletes %s ' % item.target.name
#rfc.msg += ' '.join(item.__str__().split()[1:])
updates_list = [x.target.name.upper() for x in related.filter(relationship='updates')]
if updates_list:
rfc.msg += 'updates ' + ','.join(updates_list)
# check reverse related
rdocs = RelatedDocument.objects.filter(target=alias)
for item in rdocs.filter(relationship='obs'):
rfc.rmsg += 'obsoleted by RFC %s ' % get_rfc_num(item.source)
updated_list = ['RFC %s' % get_rfc_num(x.source) for x in rdocs.filter(relationship='updates')]
if updated_list:
rfc.msg += 'updated by ' + ','.join(updated_list)
# ----------------------------------------------------------------------
# check for blue sheets
pattern = os.path.join(meeting_root,'bluesheets','bluesheets-%s-%s-*' % (meeting.number,group.acronym.lower()))
files = glob.glob(pattern)
bluesheets = []
for name in files:
basename = os.path.basename(name)
obj = {'name': basename,
'url': url_root + "bluesheets/" + basename}
bluesheets.append(obj)
bluesheets = sorted(bluesheets, key = lambda x: x['name'])
# ----------------------------------------------------------------------
else:
drafts = rfcs = bluesheets = None
# the simplest way to display the charter is to place it in a <pre> block
# however, because this forces a fixed-width font, different than the rest of
# the document we modify the charter by adding replacing linefeeds with <br>'s
if group.charter:
charter = get_charter_text(group).replace('\n','<br />')
ctime = group.charter.time
else:
charter = None
ctime = None
# rather than return the response as in a typical view function we save it as the snapshot
# proceedings.html
response = render_to_response('proceedings/proceedings.html',{
'bluesheets': bluesheets,
'charter': charter,
'ctime': ctime,
'drafts': drafts,
'group': group,
'chairs': chairs,
'secretaries': secretaries,
'ads': ads,
'tas': tas,
'meeting': meeting,
'rfcs': rfcs,
'slides': slides,
'minutes': minutes,
'agenda': agenda}
)
# save proceedings
proceedings_path = get_proceedings_path(meeting,group)
f = open(proceedings_path,'w')
f.write(response.content)
f.close()
try:
os.chmod(proceedings_path, 0664)
except OSError:
pass
# rebuild the directory
if meeting.type.slug == 'interim':
create_interim_directory()
# -------------------------------------------------
# Functions for generating Proceedings Pages
# -------------------------------------------------
def gen_areas(context):
meeting = context['meeting']
gmet, gnot = groups_by_session(None,meeting)
# append proceedings URL
for group in gmet + gnot:
group.proceedings_url = "%s/proceedings/%s/%s.html" % (settings.MEDIA_URL,meeting.number,group.acronym)
for (counter,area) in enumerate(context['areas'], start=1):
groups_met = {'wg':filter(lambda a: a.parent==area and a.state.slug!='bof' and a.type_id=='wg',gmet),
'bof':filter(lambda a: a.parent==area and a.state.slug=='bof' and a.type_id=='wg',gmet),
'ag':filter(lambda a: a.parent==area and a.type_id=='ag',gmet)}
groups_not = {'wg':filter(lambda a: a.parent==area and a.state.slug!='bof' and a.type_id=='wg',gnot),
'bof':filter(lambda a: a.parent==area and a.state.slug=='bof' and a.type_id=='wg',gnot),
'ag':filter(lambda a: a.parent==area and a.type_id=='ag',gnot)}
html = render_to_response('proceedings/area.html',{
'area': area,
'meeting': meeting,
'groups_met': groups_met,
'groups_not': groups_not,
'index': counter}
)
path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting.number,'%s.html' % area.acronym)
write_html(path,html.content)
def gen_acknowledgement(context):
meeting = context['meeting']
html = render_to_response('proceedings/acknowledgement.html',{
'meeting': meeting}
)
path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting.number,'acknowledgement.html')
write_html(path,html.content)
def gen_agenda(context):
meeting = context['meeting']
#timeslots, update, meeting, venue, ads, plenaryw_agenda, plenaryt_agenda = agenda_info(meeting.number)
timeslots = TimeSlot.objects.filter(meeting=meeting)
# sort by area:group then time
sort1 = sorted(timeslots, key = mycomp)
sort2 = sorted(sort1, key = lambda a: a.time)
html = render_to_response('proceedings/agenda.html',{
'meeting': meeting,
'timeslots': sort2}
)
path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting.number,'agenda.html')
write_html(path,html.content)
# get the text agenda from datatracker
url = 'https://datatracker.ietf.org/meeting/%s/agenda.txt' % meeting.number
text = urlopen(url).read()
path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting.number,'agenda.txt')
write_html(path,text)
def gen_attendees(context):
meeting = context['meeting']
attendees = Registration.objects.using('ietf' + meeting.number).all().order_by('lname')
html = render_to_response('proceedings/attendee.html',{
'meeting': meeting,
'attendees': attendees}
)
path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting.number,'attendee.html')
write_html(path,html.content)
def gen_group_pages(context):
meeting = context['meeting']
for group in Group.objects.filter(type__in=('wg','ag','rg'), state__in=('bof','proposed','active')):
create_proceedings(meeting,group,is_final=True)
def gen_index(context):
index = render_to_response('proceedings/index.html',context)
path = os.path.join(settings.SECR_PROCEEDINGS_DIR,context['meeting'].number,'index.html')
write_html(path,index.content)
def gen_irtf(context):
meeting = context['meeting']
irtf_chair = Role.objects.filter(group__acronym='irtf',name='chair')[0]
html = render_to_response('proceedings/irtf.html',{
'irtf_chair':irtf_chair}
)
path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting.number,'irtf.html')
write_html(path,html.content)
def gen_overview(context):
meeting = context['meeting']
ietf_chair = Role.objects.get(group__acronym='ietf',name='chair')
ads = Role.objects.filter(group__type='area',group__state='active',name='ad')
sorted_ads = sorted(ads, key = lambda a: a.person.name_parts()[3])
html = render_to_response('proceedings/overview.html',{
'meeting': meeting,
'ietf_chair': ietf_chair,
'ads': sorted_ads}
)
path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting.number,'overview.html')
write_html(path,html.content)
def gen_plenaries(context):
'''
This function generates pages for the Plenaries. At meeting 85 the Plenary sessions
were combined into one, so we need to handle not finding one of the sessions.
'''
meeting = context['meeting']
# Administration Plenary
try:
admin_session = Session.objects.get(meeting=meeting,name__contains='Administration Plenary')
admin_slides = admin_session.materials.filter(type='slides')
admin_minutes = admin_session.materials.filter(type='minutes')
admin = render_to_response('proceedings/plenary.html',{
'title': 'Administrative',
'meeting': meeting,
'slides': admin_slides,
'minutes': admin_minutes}
)
path = os.path.join(settings.SECR_PROCEEDINGS_DIR,context['meeting'].number,'administrative-plenary.html')
write_html(path,admin.content)
except Session.DoesNotExist:
pass
# Technical Plenary
try:
tech_session = Session.objects.get(meeting=meeting,name__contains='Technical Plenary')
tech_slides = tech_session.materials.filter(type='slides')
tech_minutes = tech_session.materials.filter(type='minutes')
tech = render_to_response('proceedings/plenary.html',{
'title': 'Technical',
'meeting': meeting,
'slides': tech_slides,
'minutes': tech_minutes}
)
path = os.path.join(settings.SECR_PROCEEDINGS_DIR,context['meeting'].number,'technical-plenary.html')
write_html(path,tech.content)
except Session.DoesNotExist:
pass
def gen_progress(context, final=True):
'''
This function generates the Progress Report. This report is actually produced twice. First
for inclusion in the Admin Plenary, then for the final proceedings. When produced the first
time we want to exclude the headers because they are broken links until all the proceedings
are generated.
'''
meeting = context['meeting']
# proceedings are run sometime after the meeting, so end date = the previous meeting
# date and start date = the date of the meeting before that
now = datetime.date.today()
meetings = Meeting.objects.filter(type='ietf',date__lt=now).order_by('-date')
start_date = meetings[1].date
end_date = meetings[0].date
data = get_progress_stats(start_date,end_date)
data['meeting'] = meeting
data['final'] = final
html = render_to_response('proceedings/progress.html',data)
path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting.number,'progress-report.html')
write_html(path,html.content)
def gen_research(context):
meeting = context['meeting']
gmet, gnot = groups_by_session(None,meeting)
groups = filter(lambda a: a.type_id=='rg', gmet)
# append proceedings URL
for group in groups:
group.proceedings_url = "%s/proceedings/%s/%s.html" % (settings.MEDIA_URL,meeting.number,group.acronym)
html = render_to_response('proceedings/rg_irtf.html',{
'meeting': meeting,
'groups': groups}
)
path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting.number,'rg_irtf.html')
write_html(path,html.content)
def gen_training(context):
meeting = context['meeting']
timeslots = context['others']
sessions = [ t.session for t in timeslots ]
for counter,session in enumerate(sessions, start=1):
slides = session.materials.filter(type='slides')
minutes = session.materials.filter(type='minutes')
html = render_to_response('proceedings/training.html',{
'title': '4.%s %s' % (counter, session.name),
'meeting': meeting,
'slides': slides,
'minutes': minutes}
)
path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting.number,'train-%s.html' % counter )
write_html(path,html.content)

View file

@ -0,0 +1,14 @@
# Use this script to generate the proceedings progress report without headers
from ietf import settings
from django.core import management
management.setup_environ(settings)
from ietf.secr.proceedings.proc_utils import gen_progress
from ietf.meeting.models import Meeting
import datetime
import sys
now = datetime.date.today()
meeting = Meeting.objects.filter(date__lte=now).order_by('-date')[0]
gen_progress({'meeting':meeting},final=False)

View file

@ -0,0 +1,70 @@
from django import template
from ietf.person.models import Person
import datetime
register = template.Library()
@register.filter
def abbr_status(value):
"""
Converts RFC Status to a short abbreviation
"""
d = {'Proposed Standard':'PS',
'Draft Standard':'DS',
'Standard':'S',
'Historic':'H',
'Informational':'I',
'Experimental':'E',
'Best Current Practice':'BCP',
'Internet Standard':'IS'}
return d.get(value,value)
@register.filter(name='display_duration')
def display_duration(value):
"""
Maps a session requested duration from select index to
label."""
map = {'0':'None',
'1800':'30 Minutes',
'3600':'1 Hour',
'5400':'1.5 Hours',
'7200':'2 Hours',
'9000':'2.5 Hours'}
return map[value]
@register.filter
def get_published_date(doc):
'''
Returns the published date for a RFC Document
'''
event = doc.latest_event(type='published_rfc')
if event:
return event.time
event = doc.latest_event(type='new_revision')
if event:
return event.time
else:
return None
@register.filter
def is_ppt(value):
'''
Checks if the value ends in ppt or pptx
'''
if value.endswith('ppt') or value.endswith('pptx'):
return True
else:
return False
@register.filter
def smart_login(user):
'''
Expects a Person object. If person is a Secretariat returns "on behalf of the"
'''
if not isinstance (user, Person):
return value
if user.role_set.filter(name='secr',group__acronym='secretariat'):
return '%s, on behalf of the' % user
else:
return '%s, a chair of the' % user

View file

@ -0,0 +1,28 @@
from django.core.urlresolvers import reverse
from django.test import TestCase
from ietf.meeting.models import Meeting
from ietf.utils.test_data import make_test_data
from pyquery import PyQuery
import debug
SECR_USER='secretary'
class MainTestCase(TestCase):
fixtures = ['names']
def test_main(self):
"Main Test"
make_test_data()
url = reverse('proceedings')
response = self.client.get(url, REMOTE_USER=SECR_USER)
self.assertEquals(response.status_code, 200)
def test_view(self):
"View Test"
make_test_data()
meeting = Meeting.objects.all()[0]
url = reverse('meetings_view', kwargs={'meeting_id':meeting.number})
response = self.client.get(url, REMOTE_USER=SECR_USER)
self.assertEquals(response.status_code, 200)

View file

@ -0,0 +1,31 @@
from django.conf.urls.defaults import *
urlpatterns = patterns('ietf.secr.proceedings.views',
url(r'^$', 'main', name='proceedings'),
url(r'^ajax/generate-proceedings/(?P<meeting_num>\d{1,3})/$', 'ajax_generate_proceedings', name='proceedings_ajax_generate_proceedings'),
url(r'^ajax/order-slide/$', 'ajax_order_slide', name='proceedings_ajax_order_slide'),
# special offline URL for testing proceedings build
url(r'^build/(?P<meeting_num>\d{1,3}|interim-\d{4}-[A-Za-z0-9_\-\+]+)/(?P<acronym>[A-Za-z0-9_\-\+]+)/$',
'build', name='proceedings_build'),
url(r'^delete/(?P<slide_id>[A-Za-z0-9._\-\+]+)/$', 'delete_material', name='proceedings_delete_material'),
url(r'^edit-slide/(?P<slide_id>[A-Za-z0-9._\-\+]+)/$', 'edit_slide', name='proceedings_edit_slide'),
url(r'^move-slide/(?P<slide_id>[A-Za-z0-9._\-\+]+)/(?P<direction>(up|down))/$',
'move_slide', name='proceedings_move_slide'),
url(r'^process-pdfs/(?P<meeting_num>\d{1,3})/$', 'process_pdfs', name='proceedings_process_pdfs'),
url(r'^progress-report/(?P<meeting_num>\d{1,3})/$', 'progress_report', name='proceedings_progress_report'),
url(r'^replace-slide/(?P<slide_id>[A-Za-z0-9._\-\+]+)/$', 'replace_slide', name='proceedings_replace_slide'),
url(r'^(?P<meeting_num>\d{1,3})/$', 'select', name='proceedings_select'),
# NOTE: we have two entries here which both map to upload_unified, passing session_id or acronym
url(r'^(?P<meeting_num>\d{1,3}|interim-\d{4}-[A-Za-z0-9_\-\+]+)/(?P<session_id>\d{1,6})/$',
'upload_unified', name='proceedings_upload_unified'),
url(r'^(?P<meeting_num>\d{1,3}|interim-\d{4}-[A-Za-z0-9_\-\+]+)/(?P<acronym>[A-Za-z0-9_\-\+]+)/$',
'upload_unified', name='proceedings_upload_unified'),
# interim stuff
url(r'^interim/$', 'select_interim', name='proceedings_select_interim'),
url(r'^interim/(?P<meeting_num>interim-\d{4}-[A-Za-z0-9_\-\+]+)/delete/$', 'delete_interim_meeting',
name='proceedings_delete_interim_meeting'),
url(r'^interim/(?P<acronym>[A-Za-z0-9_\-\+]+)/$', 'interim', name='proceedings_interim'),
#url(r'^interim/directory/$', 'interim_directory', name='proceedings_interim_directory'),
#url(r'^interim/directory/(?P<sortby>(group|date))/$', 'interim_directory',
# name='proceedings_interim_directory_sort'),
)

View file

@ -0,0 +1,915 @@
from django.conf import settings
from django.contrib import messages
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
from django.core.urlresolvers import reverse
from django.db.models import Max
from django.forms.formsets import formset_factory
from django.forms.models import inlineformset_factory, modelformset_factory
from django.http import HttpResponseRedirect, HttpResponse, HttpResponseForbidden
from django.shortcuts import render_to_response, get_object_or_404
from django.template import RequestContext
from django.template import Context
from django.template.defaultfilters import slugify
from django.template.loader import get_template
from django.utils import simplejson
from django.db.models import Max,Count,get_model
from ietf.secr.lib import jsonapi
from ietf.secr.proceedings.proc_utils import *
from ietf.secr.sreq.forms import GroupSelectForm
from ietf.secr.utils.decorators import check_permissions, sec_only
from ietf.secr.utils.document import get_rfc_num, 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_proceedings_path, get_material
from ietf.doc.models import Document, DocAlias, DocEvent, State, NewRevisionDocEvent, RelatedDocument
from ietf.group.models import Group
from ietf.group.proxy import IETFWG
from ietf.group.utils import get_charter_text
from ietf.ietfauth.decorators import has_role
from ietf.meeting.models import Meeting, Session, TimeSlot
from ietf.name.models import MeetingTypeName, SessionStatusName
from ietf.person.models import Person
from forms import *
from models import InterimMeeting # proxy model
import datetime
import glob
import itertools
import os
import re
import shutil
import zipfile
# -------------------------------------------------
# Helper Functions
# -------------------------------------------------
def build_choices(queryset):
'''
This function takes a queryset (or list) of Groups and builds a list of tuples for use
as choices in a select widget. Using acronym for both value and label.
'''
choices = [ (g.acronym,g.acronym) for g in queryset ]
return sorted(choices, key=lambda choices: choices[1])
def find_index(slide_id, qs):
'''
This function looks up a slide in a queryset of slides,
returning the index.
'''
for i in range(0,qs.count()):
if str(qs[i].pk) == slide_id:
return i
def get_doc_filename(doc):
'''
This function takes a Document of type slides,minute or agenda and returns
the full path to the file on disk. During migration of the system the
filename was saved in external_url, new files will also use this convention.
'''
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)
else:
path = os.path.join(get_upload_root(meeting),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]
def get_next_interim_num(acronym,date):
'''
This function takes a group acronym and date object and returns the next number to use for an
interim meeting. The format is interim-[year]-[acronym]-[1-9]
'''
base = 'interim-%s-%s-' % (date.year, acronym)
# can't use count() to calculate the next number in case one was deleted
meetings = list(Meeting.objects.filter(type='interim',number__startswith=base).order_by('number'))
if meetings:
parts = meetings[-1].number.split('-')
return base + str(int(parts[-1]) + 1)
else:
return base + '1'
def get_next_slide_num(session):
'''
This function takes a session object and returns the
next slide number to use for a newly added slide as a string.
'''
"""
slides = session.materials.filter(type='slides').order_by('-name')
if slides:
# we need this special case for non wg/rg sessions because the name format is different
# it should be changed to match the rest
if session.group.type.slug not in ('wg','rg'):
nums = [ s.name.split('-')[3] for s in slides ]
else:
nums = [ s.name.split('-')[-1] for s in slides ]
"""
if session.meeting.type_id == 'ietf':
pattern = 'slides-%s-%s' % (session.meeting.number,session.group.acronym)
elif session.meeting.type_id == 'interim':
pattern = 'slides-%s' % (session.meeting.number)
slides = Document.objects.filter(type='slides',name__startswith=pattern)
if slides:
nums = [ s.name.split('-')[-1] for s in slides ]
nums.sort(key=int)
return str(int(nums[-1]) + 1)
else:
return '0'
def get_next_order_num(session):
'''
This function takes a session object and returns the
next slide order number to use for a newly added slide as an integer.
'''
max_order = session.materials.aggregate(Max('order'))['order__max']
return max_order + 1 if max_order else 1
# --- These could be properties/methods on meeting
def get_proceedings_path(meeting,group):
if meeting.type_id == 'ietf':
path = os.path.join(get_upload_root(meeting),group.acronym + '.html')
elif meeting.type_id == 'interim':
path = os.path.join(get_upload_root(meeting),'proceedings.html')
return path
def get_proceedings_url(meeting,group=None):
if meeting.type_id == 'ietf':
url = "%s/proceedings/%s/" % (settings.MEDIA_URL,meeting.number)
if group:
url = url + "%s.html" % group.acronym
elif meeting.type_id == 'interim':
url = "%s/proceedings/interim/%s/%s/proceedings.html" % (
settings.MEDIA_URL,
meeting.date.strftime('%Y/%m/%d'),
group.acronym)
return url
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.
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)
if not os.path.exists(path):
os.mkdir(path)
else:
path = os.path.join(get_upload_root(meeting),subdir)
# agendas and minutes can only have one file instance so delete file if it already exists
if subdir in ('agenda','minutes'):
old_files = glob.glob(os.path.join(path,base) + '.*')
for f in old_files:
os.remove(f)
destination = open(os.path.join(path,filename), 'wb+')
for chunk in file.chunks():
destination.write(chunk)
destination.close()
# unzip zipfile
if extension == '.zip':
os.chdir(path)
os.system('unzip %s' % filename)
def make_directories(meeting):
'''
This function takes a meeting object and creates the appropriate materials directories
'''
path = get_upload_root(meeting)
os.umask(0)
for leaf in ('slides','agenda','minutes','id','rfc'):
target = os.path.join(path,leaf)
if not os.path.exists(target):
os.makedirs(target)
def parsedate(d):
'''
This function takes a date object and returns a tuple of year,month,day
'''
return (d.strftime('%Y'),d.strftime('%m'),d.strftime('%d'))
# -------------------------------------------------
# AJAX Functions
# -------------------------------------------------
@sec_only
def ajax_generate_proceedings(request, meeting_num):
'''
Ajax function which takes a meeting number and generates the proceedings
pages for the meeting. It returns a snippet of HTML that gets placed in the
Secretariat Only section of the select page.
'''
meeting = get_object_or_404(Meeting, number=meeting_num)
areas = Group.objects.filter(type='area',state='active').order_by('name')
others = TimeSlot.objects.filter(meeting=meeting,type='other').order_by('time')
context = {'meeting':meeting,
'areas':areas,
'others':others}
proceedings_url = get_proceedings_url(meeting)
# the acknowledgement page can be edited manually so only produce if it doesn't already exist
path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting.number,'acknowledgement.html')
if not os.path.exists(path):
gen_acknowledgement(context)
gen_overview(context)
gen_progress(context)
gen_agenda(context)
gen_attendees(context)
gen_index(context)
gen_areas(context)
gen_plenaries(context)
gen_training(context)
gen_irtf(context)
gen_research(context)
gen_group_pages(context)
# get the time proceedings were generated
path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting.number,'index.html')
last_run = datetime.datetime.fromtimestamp(os.path.getmtime(path))
return render_to_response('includes/proceedings_functions.html',{
'meeting':meeting,
'last_run':last_run,
'proceedings_url':proceedings_url},
RequestContext(request,{}),
)
@jsonapi
def ajax_order_slide(request):
'''
Ajax function to change the order of presentation slides.
This function expects a POST request with the following parameters
order: new order of slide, 0 based
slide_name: slide primary key (name)
'''
if request.method != 'POST' or not request.POST:
return { 'success' : False, 'error' : 'No data submitted or not POST' }
slide_name = request.POST.get('slide_name',None)
order = request.POST.get('order',None)
slide = get_object_or_404(Document, name=slide_name)
# get all the slides for this session
session = slide.session_set.all()[0]
meeting = session.meeting
group = session.group
qs = session.materials.exclude(states__slug='deleted').filter(type='slides').order_by('order')
# move slide and reorder list
slides = list(qs)
index = slides.index(slide)
slides.pop(index)
slides.insert(int(order),slide)
for ord,item in enumerate(slides,start=1):
if item.order != ord:
item.order = ord
item.save()
return {'success':True,'order':order,'slide':slide_name}
# --------------------------------------------------
# STANDARD VIEW FUNCTIONS
# --------------------------------------------------
@sec_only
def build(request,meeting_num,acronym):
'''
This is a utility or test view. It simply rebuilds the proceedings html for the specified
meeting / group.
'''
meeting = Meeting.objects.get(number=meeting_num)
group = get_object_or_404(Group,acronym=acronym)
create_proceedings(meeting,group)
messages.success(request,'proceedings.html was rebuilt')
url = reverse('proceedings_upload_unified', kwargs={'meeting_num':meeting_num,'acronym':acronym})
return HttpResponseRedirect(url)
@check_permissions
def delete_material(request,slide_id):
'''
This view handles deleting meeting materials. We don't actually delete the
document object but set the state to deleted and add a 'deleted' DocEvent.
'''
doc = get_object_or_404(Document, name=slide_id)
# derive other objects
session = doc.session_set.all()[0]
meeting = session.meeting
group = session.group
path = get_full_path(doc)
if path and os.path.exists(path):
os.remove(path)
# leave it related
#session.materials.remove(doc)
state = State.objects.get(type=doc.type,slug='deleted')
doc.set_state(state)
# create deleted_document
DocEvent.objects.create(doc=doc,
by=request.user.get_profile(),
type='deleted')
create_proceedings(meeting,group)
messages.success(request,'The material was deleted successfully')
if group.type.slug in ('wg','rg'):
url = reverse('proceedings_upload_unified', kwargs={'meeting_num':meeting.number,'acronym':group.acronym})
else:
url = reverse('proceedings_upload_unified', kwargs={'meeting_num':meeting.number,'session_id':session.id})
return HttpResponseRedirect(url)
@sec_only
def delete_interim_meeting(request, meeting_num):
'''
This view deletes the specified Interim Meeting and any material that has been
uploaded for it. The pattern in urls.py ensures we don't call this with a regular
meeting number.
'''
meeting = get_object_or_404(Meeting, number=meeting_num)
sessions = Session.objects.filter(meeting=meeting)
group = sessions[0].group
# delete directories
path = get_upload_root(meeting)
# do a quick sanity check on this path before we go and delete it
parts = path.split('/')
assert parts[-1] == group.acronym
if os.path.exists(path):
shutil.rmtree(path)
meeting.delete()
sessions.delete()
url = reverse('proceedings_interim', kwargs={'acronym':group.acronym})
return HttpResponseRedirect(url)
@check_permissions
def edit_slide(request, slide_id):
'''
This view allows the user to edit the name of a slide.
'''
slide = get_object_or_404(Document, name=slide_id)
# derive other objects
session = slide.session_set.all()[0]
meeting = session.meeting
group = session.group
if group.type.slug in ('wg','rg'):
url = reverse('proceedings_upload_unified', kwargs={'meeting_num':meeting.number,'acronym':group.acronym})
else:
url = reverse('proceedings_upload_unified', kwargs={'meeting_num':meeting.number,'session_id':session.id})
if request.method == 'POST': # If the form has been submitted...
button_text = request.POST.get('submit', '')
if button_text == 'Cancel':
return HttpResponseRedirect(url)
form = EditSlideForm(request.POST, instance=slide) # A form bound to the POST data
if form.is_valid():
form.save()
# rebuild proceedings.html
create_proceedings(meeting,group)
return HttpResponseRedirect(url)
else:
form = EditSlideForm(instance=slide)
return render_to_response('proceedings/edit_slide.html',{
'group': group,
'meeting':meeting,
'slide':slide,
'form':form},
RequestContext(request, {}),
)
def interim(request, acronym):
'''
This view presents the user with a list of interim meetings for the specified group.
The user can select a meeting to manage or create a new interim meeting by entering
a date.
'''
group = get_object_or_404(Group, acronym=acronym)
if request.method == 'POST': # If the form has been submitted...
button_text = request.POST.get('submit', '')
if button_text == 'Back':
url = reverse('proceedings_select_interim')
return HttpResponseRedirect(url)
form = InterimMeetingForm(request.POST) # A form bound to the POST data
if form.is_valid():
date = form.cleaned_data['date']
number = get_next_interim_num(acronym,date)
meeting=Meeting.objects.create(type_id='interim',
date=date,
number=number)
# create session to associate this meeting with a group and hold material
Session.objects.create(meeting=meeting,
group=group,
requested_by=request.user.get_profile(),
status_id='sched')
create_interim_directory()
make_directories(meeting)
messages.success(request, 'Meeting created')
url = reverse('proceedings_interim', kwargs={'acronym':acronym})
return HttpResponseRedirect(url)
else:
form = InterimMeetingForm(initial={'group_acronym_id':acronym}) # An unbound form
meetings = Meeting.objects.filter(type='interim',session__group__acronym=acronym).order_by('date')
return render_to_response('proceedings/interim_meeting.html',{
'group': group,
'meetings':meetings,
'form':form},
RequestContext(request, {}),
)
def interim_directory(request, sortby=None):
if sortby == 'group':
qs = InterimMeeting.objects.all()
meetings = sorted(qs, key=lambda a: a.group.acronym)
else:
meetings = InterimMeeting.objects.all().order_by('-date')
return render_to_response('proceedings/interim_directory.html', {
'meetings': meetings},
)
def main(request):
'''
List IETF Meetings. If the user is Secratariat list includes all meetings otherwise
show only those meetings whose corrections submission date has not passed.
**Templates:**
* ``proceedings/main.html``
**Template Variables:**
* meetings, interim_meetings, today
'''
# getting numerous errors when people try to access using the wrong account
try:
person = request.user.get_profile()
except Person.DoesNotExist:
return HttpResponseForbidden('ACCESS DENIED: user=%s' % request.META['REMOTE_USER'])
if has_role(request.user,'Secretariat'):
meetings = Meeting.objects.filter(type='ietf').order_by('-number')
else:
# select meetings still within the cutoff period
meetings = Meeting.objects.filter(type='ietf',date__gt=datetime.datetime.today() - datetime.timedelta(days=settings.SUBMISSION_CORRECTION_DAYS)).order_by('number')
groups = get_my_groups(request.user)
interim_meetings = Meeting.objects.filter(type='interim',session__group__in=groups).order_by('-date')
# tac on group for use in templates
for m in interim_meetings:
m.group = m.session_set.all()[0].group
# we today's date to see if we're past the submissio cutoff
today = datetime.date.today()
return render_to_response('proceedings/main.html',{
'meetings': meetings,
'interim_meetings': interim_meetings,
'today': today},
RequestContext(request,{}),
)
@check_permissions
def move_slide(request, slide_id, direction):
'''
This view will re-order slides. In addition to meeting, group and slide IDs it takes
a direction argument which is a string [up|down].
'''
slide = get_object_or_404(Document, name=slide_id)
# derive other objects
session = slide.session_set.all()[0]
meeting = session.meeting
group = session.group
qs = session.materials.exclude(states__slug='deleted').filter(type='slides').order_by('order')
# if direction is up and we aren't already the first slide
if direction == 'up' and slide_id != str(qs[0].pk):
index = find_index(slide_id, qs)
slide_before = qs[index-1]
slide_before.order, slide.order = slide.order, slide_before.order
slide.save()
slide_before.save()
# if direction is down, more than one slide and we aren't already the last slide
if direction == 'down' and qs.count() > 1 and slide_id != str(qs[qs.count()-1].pk):
index = find_index(slide_id, qs)
slide_after = qs[index+1]
slide_after.order, slide.order = slide.order, slide_after.order
slide.save()
slide_after.save()
if group.type.slug in ('wg','rg'):
url = reverse('proceedings_upload_unified', kwargs={'meeting_num':meeting.number,'acronym':group.acronym})
else:
url = reverse('proceedings_upload_unified', kwargs={'meeting_num':meeting.number,'session_id':session.id})
return HttpResponseRedirect(url)
@sec_only
def process_pdfs(request, meeting_num):
'''
This function is used to update the database once meeting materials in PPT format
are converted to PDF format and uploaded to the server. It basically finds every PowerPoint
slide document for the given meeting and checks to see if there is a PDF version. If there
is external_url is changed. Then when proceedings are generated the URL will refer to the
PDF document.
'''
warn_count = 0
count = 0
meeting = get_object_or_404(Meeting, number=meeting_num)
ppt = Document.objects.filter(session__meeting=meeting,type='slides',external_url__endswith='.ppt').exclude(states__slug='deleted')
pptx = Document.objects.filter(session__meeting=meeting,type='slides',external_url__endswith='.pptx').exclude(states__slug='deleted')
for doc in itertools.chain(ppt,pptx):
base,ext = os.path.splitext(doc.external_url)
pdf_file = base + '.pdf'
path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting_num,'slides',pdf_file)
if os.path.exists(path):
doc.external_url = pdf_file
doc.save()
count += 1
else:
warn_count += 1
if warn_count:
messages.warning(request, '%s PDF files processed. %s PowerPoint files still not converted.' % (count, warn_count))
else:
messages.success(request, '%s PDF files processed' % count)
url = reverse('proceedings_select', kwargs={'meeting_num':meeting_num})
return HttpResponseRedirect(url)
@sec_only
def progress_report(request, meeting_num):
'''
This function generates the proceedings progress report for use at the Plenary.
'''
meeting = get_object_or_404(Meeting, number=meeting_num)
gen_progress({'meeting':meeting},final=False)
url = reverse('proceedings_select', kwargs={'meeting_num':meeting_num})
return HttpResponseRedirect(url)
@check_permissions
def replace_slide(request, slide_id):
'''
This view allows the user to upload a new file to replace a slide.
'''
slide = get_object_or_404(Document, name=slide_id)
# derive other objects
session = slide.session_set.all()[0]
meeting = session.meeting
group = session.group
if group.type.slug in ('wg','rg'):
url = reverse('proceedings_upload_unified', kwargs={'meeting_num':meeting.number,'acronym':group.acronym})
else:
url = reverse('proceedings_upload_unified', kwargs={'meeting_num':meeting.number,'session_id':session.id})
if request.method == 'POST': # If the form has been submitted...
button_text = request.POST.get('submit', '')
if button_text == 'Cancel':
return HttpResponseRedirect(url)
form = ReplaceSlideForm(request.POST,request.FILES,instance=slide) # A form bound to the POST data
if form.is_valid():
new_slide = form.save(commit=False)
new_slide.time = datetime.datetime.now()
file = request.FILES[request.FILES.keys()[0]]
file_ext = os.path.splitext(file.name)[1]
disk_filename = new_slide.name + file_ext
handle_upload_file(file,disk_filename,meeting,'slides')
new_slide.external_url = disk_filename
new_slide.save()
# create DocEvent uploaded
DocEvent.objects.create(doc=slide,
by=request.user.get_profile(),
type='uploaded')
# rebuild proceedings.html
create_proceedings(meeting,group)
return HttpResponseRedirect(url)
else:
form = ReplaceSlideForm(instance=slide)
return render_to_response('proceedings/replace_slide.html',{
'group': group,
'meeting':meeting,
'slide':slide,
'form':form},
RequestContext(request, {}),
)
def select(request, meeting_num):
'''
A screen to select which group you want to upload material for. Users of this view area
Secretariat staff and community (WG Chairs, ADs, etc). Only those groups with sessions
scheduled for the given meeting will appear in drop-downs. For Group and IRTF selects, the
value will be group.acronym to use in pretty URLs. Since Training sessions have no acronym
we'll use the session id.
'''
if request.method == 'POST':
if request.POST.get('group',None):
redirect_url = reverse('proceedings_upload_unified', kwargs={'meeting_num':meeting_num,'acronym':request.POST['group']})
return HttpResponseRedirect(redirect_url)
else:
messages.error(request, 'No Group selected')
meeting = get_object_or_404(Meeting, number=meeting_num)
user = request.user
person = user.get_profile()
groups_session, groups_no_session = groups_by_session(user, meeting)
proceedings_url = get_proceedings_url(meeting)
# get the time proceedings were generated
path = os.path.join(settings.SECR_PROCEEDINGS_DIR,meeting.number,'index.html')
if os.path.exists(path):
last_run = datetime.datetime.fromtimestamp(os.path.getmtime(path))
else:
last_run = None
# initialize group form
wgs = filter(lambda x: x.type_id in ('wg','ag','team'),groups_session)
group_form = GroupSelectForm(choices=build_choices(wgs))
# intialize IRTF form, only show if user is sec or irtf chair
if has_role(user,'Secretariat') or person.role_set.filter(name__slug='chair',group__type__slug__in=('irtf','rg')):
rgs = filter(lambda x: x.type_id == 'rg',groups_session)
irtf_form = GroupSelectForm(choices=build_choices(rgs))
else:
irtf_form = None
# initialize Training form, this select widget needs to have a session id, because it's
# utilmately the session that we associate material with
# NOTE: there are two ways to query for the groups we want, the later seems more specific
if has_role(user,'Secretariat'):
choices = []
#for session in Session.objects.filter(meeting=meeting).exclude(name=""):
for session in Session.objects.filter(meeting=meeting,timeslot__type='other').order_by('name'):
choices.append((session.id,session.timeslot_set.all()[0].name))
training_form = GroupSelectForm(choices=choices)
else:
training_form = None
# iniialize plenary form
if has_role(user,['Secretariat','IETF Chair','IAB Chair']):
choices = []
for session in Session.objects.filter(meeting=meeting,
timeslot__type='plenary').order_by('name'):
choices.append((session.id,session.timeslot_set.all()[0].name))
plenary_form = GroupSelectForm(choices=choices)
else:
plenary_form = None
# count PowerPoint files waiting to be converted
if has_role(user,'Secretariat'):
ppt = Document.objects.filter(session__meeting=meeting,type='slides',external_url__endswith='.ppt').exclude(states__slug='deleted')
pptx = Document.objects.filter(session__meeting=meeting,type='slides',external_url__endswith='.pptx').exclude(states__slug='deleted')
ppt_count = ppt.count() + pptx.count()
else:
ppt_count = 0
return render_to_response('proceedings/select.html', {
'group_form': group_form,
'irtf_form': irtf_form,
'training_form': training_form,
'plenary_form': plenary_form,
'meeting': meeting,
'last_run': last_run,
'proceedings_url': proceedings_url,
'ppt_count': ppt_count},
RequestContext(request,{}),
)
def select_interim(request):
'''
A screen to select which group you want to upload Interim material for. Works for Secretariat staff
and external (ADs, chairs, etc)
'''
if request.method == 'POST':
redirect_url = reverse('proceedings_interim', kwargs={'acronym':request.POST['group']})
return HttpResponseRedirect(redirect_url)
if request.user_is_secretariat:
# initialize working groups form
choices = build_choices(Group.objects.active_wgs())
group_form = GroupSelectForm(choices=choices)
# per Alexa, not supporting Interim IRTF meetings at this time
# intialize IRTF form
#choices = build_choices(Group.objects.filter(type='wg', state='active')
#irtf_form = GroupSelectForm(choices=choices)
else:
# these forms aren't used for non-secretariat
groups = get_my_groups(request.user)
choices = build_choices(groups)
group_form = GroupSelectForm(choices=choices)
irtf_form = None
training_form = None
return render_to_response('proceedings/interim_select.html', {
'group_form': group_form},
#'irtf_form': irtf_form,
RequestContext(request,{}),
)
@check_permissions
def upload_unified(request, meeting_num, acronym=None, session_id=None):
'''
This view is the main view for uploading / re-ordering material for regular and interim
meetings. There are two urls.py entries which map to this view. The acronym_id option is used
most often for groups of regular and interim meetings. session_id is used for uploading
material for Training sessions (where group is not a unique identifier). We could have used
session_id all the time but this makes for an ugly URL which most of the time would be
avoided by using acronym.
'''
meeting = get_object_or_404(Meeting, number=meeting_num)
now = datetime.datetime.now()
if acronym:
group = get_object_or_404(Group, acronym=acronym)
sessions = Session.objects.filter(meeting=meeting,group=group)
session = sessions[0]
session_name = ''
elif session_id:
sessions = None
session = get_object_or_404(Session, id=int(session_id))
group = session.group
session_name = session.name
if request.method == 'POST':
button_text = request.POST.get('submit','')
if button_text == 'Back':
if meeting.type.slug == 'interim':
url = reverse('proceedings_interim', kwargs={'acronym':group.acronym})
else:
url = reverse('proceedings_select', kwargs={'meeting_num':meeting_num})
return HttpResponseRedirect(url)
form = UnifiedUploadForm(request.POST,request.FILES)
if form.is_valid():
material_type = form.cleaned_data['material_type']
slide_name = form.cleaned_data['slide_name']
file = request.FILES[request.FILES.keys()[0]]
file_ext = os.path.splitext(file.name)[1]
# set the filename
if meeting.type.slug == 'ietf':
filename = '%s-%s-%s' % (material_type.slug,meeting.number,group.acronym)
elif meeting.type.slug == 'interim':
filename = '%s-%s' % (material_type.slug,meeting.number)
# NonSession material, use short name for shorter URLs
if session.short:
filename += "-%s" % session.short
elif session_name:
filename += "-%s" % slugify(session_name)
# --------------------------------
if material_type.slug == 'slides':
order_num = get_next_order_num(session)
slide_num = get_next_slide_num(session)
filename += "-%s" % slide_num
disk_filename = filename + file_ext
# create the Document object, in the case of slides the name will always be unique
# so you'll get a new object, agenda and minutes will reuse doc object if it exists
doc, created = Document.objects.get_or_create(type=material_type,
group=group,
name=filename)
doc.external_url = disk_filename
doc.time = now
if created:
doc.rev = '1'
else:
doc.rev = str(int(doc.rev) + 1)
if material_type.slug == 'slides':
doc.order=order_num
if slide_name:
doc.title = slide_name
else:
doc.title = doc.name
else:
doc.title = '%s for %s at %s' % (material_type.slug.capitalize(), group.acronym.upper(), meeting)
doc.save()
DocAlias.objects.get_or_create(name=doc.name, document=doc)
handle_upload_file(file,disk_filename,meeting,material_type.slug)
# set Doc state
state = State.objects.get(type=doc.type,slug='active')
doc.set_state(state)
# create session relationship, per Henrik we should associate documents to all sessions
# for the current meeting (until tools support different materials for diff sessions)
if sessions:
for s in sessions:
s.materials.add(doc)
else:
session.materials.add(doc)
# create NewRevisionDocEvent instead of uploaded, per Ole
NewRevisionDocEvent.objects.create(type='new_revision',
by=request.user.get_profile(),
doc=doc,
rev=doc.rev,
desc='New revision available',
time=now)
create_proceedings(meeting,group)
messages.success(request,'File uploaded sucessfully')
else:
form = UnifiedUploadForm(initial={'meeting_id':meeting.id,'acronym':group.acronym,'material_type':'slides'})
agenda,minutes,slides = get_material(session)
# gather DocEvents
# include deleted material to catch deleted doc events
docs = session.materials.all()
docevents = DocEvent.objects.filter(doc__in=docs)
path = get_proceedings_path(meeting,group)
if os.path.exists(path):
proceedings_url = get_proceedings_url(meeting,group)
else:
proceedings_url = ''
return render_to_response('proceedings/upload_unified.html', {
'docevents': docevents,
'meeting': meeting,
'group': group,
'minutes': minutes,
'agenda': agenda,
'form': form,
'session_name': session_name, # for Tutorials, etc
'slides':slides,
'proceedings_url': proceedings_url},
RequestContext(request, {}),
)

View file

44
ietf/secr/roles/forms.py Normal file
View file

@ -0,0 +1,44 @@
from django import forms
from ietf.secr.areas.models import *
import re
class LiaisonForm(forms.Form):
liaison_name = forms.CharField(max_length=100,label='Name',help_text="To see a list of people type the first name, or last name, or both.")
affiliation = forms.CharField(max_length=50)
# set css class=name-autocomplete for name field (to provide select list)
def __init__(self, *args, **kwargs):
super(LiaisonForm, self).__init__(*args, **kwargs)
self.fields['liaison_name'].widget.attrs['class'] = 'name-autocomplete'
def clean_liaison_name(self):
name = self.cleaned_data.get('liaison_name', '')
# check for tag within parenthesis to ensure name was selected from the list
m = re.search(r'(\d+)', name)
if name and not m:
raise forms.ValidationError("You must select an entry from the list!")
return name
def clean_affiliation(self):
affiliation = self.cleaned_data.get('affiliation', '')
# give error if field ends with "Liaison", application adds this label
m = re.search(r'[L|l]iaison$', affiliation)
if affiliation and m:
raise forms.ValidationError("Don't use 'Liaison' in field. Application adds this.")
return affiliation
class ChairForm(forms.Form):
chair_name = forms.CharField(max_length=100,label='Name',help_text="To see a list of people type the first name, or last name, or both.")
# set css class=name-autocomplete for name field (to provide select list)
def __init__(self, *args, **kwargs):
super(ChairForm, self).__init__(*args, **kwargs)
self.fields['chair_name'].widget.attrs['class'] = 'name-autocomplete'
def clean_chair_name(self):
name = self.cleaned_data.get('chair_name', '')
# check for tag within parenthesis to ensure name was selected from the list
m = re.search(r'(\d+)', name)
if name and not m:
raise forms.ValidationError("You must select an entry from the list!")
return name

Some files were not shown because too many files have changed in this diff Show more