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:
parent
bf7b128ef0
commit
7e67b40a87
0
form_utils/__init__.py
Normal file
0
form_utils/__init__.py
Normal file
12
form_utils/admin.py
Normal file
12
form_utils/admin.py
Normal 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
51
form_utils/fields.py
Normal 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
278
form_utils/forms.py
Normal 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
|
3
form_utils/media/form_utils/js/autoresize.js
Normal file
3
form_utils/media/form_utils/js/autoresize.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('textarea.autoresize').autogrow();
|
||||||
|
});
|
132
form_utils/media/form_utils/js/jquery.autogrow.js
Normal file
132
form_utils/media/form_utils/js/jquery.autogrow.js
Normal 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
0
form_utils/models.py
Normal file
12
form_utils/settings.py
Normal file
12
form_utils/settings.py
Normal 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)
|
16
form_utils/templates/form_utils/better_form.html
Normal file
16
form_utils/templates/form_utils/better_form.html
Normal 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 %}
|
11
form_utils/templates/form_utils/fields_as_lis.html
Normal file
11
form_utils/templates/form_utils/fields_as_lis.html
Normal 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 %}
|
13
form_utils/templates/form_utils/form.html
Normal file
13
form_utils/templates/form_utils/form.html
Normal 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 %}
|
3
form_utils/templatetags/CVS/Entries
Normal file
3
form_utils/templatetags/CVS/Entries
Normal 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
|
1
form_utils/templatetags/CVS/Repository
Normal file
1
form_utils/templatetags/CVS/Repository
Normal file
|
@ -0,0 +1 @@
|
||||||
|
ietfsec/form_utils/templatetags
|
1
form_utils/templatetags/CVS/Root
Normal file
1
form_utils/templatetags/CVS/Root
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/a/cvs
|
6
form_utils/templatetags/__init__.py
Normal file
6
form_utils/templatetags/__init__.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
"""
|
||||||
|
__init__.py for django-form-utils - templatetags
|
||||||
|
|
||||||
|
Time-stamp: <2008-10-13 12:14:37 carljm __init__.py>
|
||||||
|
|
||||||
|
"""
|
42
form_utils/templatetags/form_utils_tags.py
Normal file
42
form_utils/templatetags/form_utils_tags.py
Normal 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
20
form_utils/utils.py
Normal 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
112
form_utils/widgets.py
Normal 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
8
ietf/secr/__init__.py
Normal 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 $"
|
||||||
|
|
0
ietf/secr/announcement/__init__.py
Normal file
0
ietf/secr/announcement/__init__.py
Normal file
172
ietf/secr/announcement/forms.py
Normal file
172
ietf/secr/announcement/forms.py
Normal 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
|
4
ietf/secr/announcement/models.py
Normal file
4
ietf/secr/announcement/models.py
Normal 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
|
80
ietf/secr/announcement/tests.py
Normal file
80
ietf/secr/announcement/tests.py
Normal 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 <ietf-secretariat@ietf.org>',
|
||||||
|
'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)
|
1
ietf/secr/announcement/testurl.list
Normal file
1
ietf/secr/announcement/testurl.list
Normal file
|
@ -0,0 +1 @@
|
||||||
|
200 /secr/announcement/
|
6
ietf/secr/announcement/urls.py
Normal file
6
ietf/secr/announcement/urls.py
Normal 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'),
|
||||||
|
)
|
107
ietf/secr/announcement/views.py
Normal file
107
ietf/secr/announcement/views.py
Normal 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, {}),
|
||||||
|
)
|
0
ietf/secr/areas/__init__.py
Normal file
0
ietf/secr/areas/__init__.py
Normal file
196
ietf/secr/areas/forms.py
Normal file
196
ietf/secr/areas/forms.py
Normal 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
|
1
ietf/secr/areas/models.py
Normal file
1
ietf/secr/areas/models.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
from django.db import models
|
0
ietf/secr/areas/templatetags/__init__.py
Normal file
0
ietf/secr/areas/templatetags/__init__.py
Normal file
10
ietf/secr/areas/templatetags/custom_tags.py
Normal file
10
ietf/secr/areas/templatetags/custom_tags.py
Normal 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
39
ietf/secr/areas/tests.py
Normal 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
12
ietf/secr/areas/urls.py
Normal 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
321
ietf/secr/areas/views.py
Normal 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, {}),
|
||||||
|
)
|
0
ietf/secr/console/__init__.py
Normal file
0
ietf/secr/console/__init__.py
Normal file
3
ietf/secr/console/models.py
Normal file
3
ietf/secr/console/models.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
23
ietf/secr/console/tests.py
Normal file
23
ietf/secr/console/tests.py
Normal 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
|
||||||
|
"""}
|
||||||
|
|
5
ietf/secr/console/urls.py
Normal file
5
ietf/secr/console/urls.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from django.conf.urls.defaults import *
|
||||||
|
|
||||||
|
urlpatterns = patterns('ietf.secr.console.views',
|
||||||
|
url(r'^$', 'main', name='console'),
|
||||||
|
)
|
17
ietf/secr/console/views.py
Normal file
17
ietf/secr/console/views.py
Normal 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, {}),
|
||||||
|
)
|
13
ietf/secr/context_processors.py
Normal file
13
ietf/secr/context_processors.py
Normal 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}
|
0
ietf/secr/drafts/__init__.py
Normal file
0
ietf/secr/drafts/__init__.py
Normal file
282
ietf/secr/drafts/email.py
Normal file
282
ietf/secr/drafts/email.py
Normal 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
|
102386
ietf/secr/drafts/fixtures/email.all
Normal file
102386
ietf/secr/drafts/fixtures/email.all
Normal file
File diff suppressed because it is too large
Load diff
19602
ietf/secr/drafts/fixtures/group.all
Normal file
19602
ietf/secr/drafts/fixtures/group.all
Normal file
File diff suppressed because it is too large
Load diff
132459
ietf/secr/drafts/fixtures/person.all
Normal file
132459
ietf/secr/drafts/fixtures/person.all
Normal file
File diff suppressed because it is too large
Load diff
16392
ietf/secr/drafts/fixtures/role.all
Normal file
16392
ietf/secr/drafts/fixtures/role.all
Normal file
File diff suppressed because it is too large
Load diff
56
ietf/secr/drafts/fixtures/test-email.json
Normal file
56
ietf/secr/drafts/fixtures/test-email.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
224
ietf/secr/drafts/fixtures/test-group.json
Normal file
224
ietf/secr/drafts/fixtures/test-group.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
82
ietf/secr/drafts/fixtures/test-meeting.json
Normal file
82
ietf/secr/drafts/fixtures/test-meeting.json
Normal 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": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
132
ietf/secr/drafts/fixtures/test-person.json
Normal file
132
ietf/secr/drafts/fixtures/test-person.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
62
ietf/secr/drafts/fixtures/test-role.json
Normal file
62
ietf/secr/drafts/fixtures/test-role.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
164
ietf/secr/drafts/fixtures/test-user.json
Normal file
164
ietf/secr/drafts/fixtures/test-user.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
18884
ietf/secr/drafts/fixtures/user.all
Normal file
18884
ietf/secr/drafts/fixtures/user.all
Normal file
File diff suppressed because it is too large
Load diff
387
ietf/secr/drafts/forms.py
Normal file
387
ietf/secr/drafts/forms.py
Normal 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')
|
||||||
|
|
1
ietf/secr/drafts/models.py
Normal file
1
ietf/secr/drafts/models.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
from django.db import models
|
3
ietf/secr/drafts/notifications.py
Normal file
3
ietf/secr/drafts/notifications.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# see add_id5.cfm ~400 for email To addresses
|
||||||
|
# see generateNotification.cfm
|
||||||
|
|
9
ietf/secr/drafts/report_id_activity.py
Normal file
9
ietf/secr/drafts/report_id_activity.py
Normal 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]),
|
||||||
|
|
9
ietf/secr/drafts/report_progress_report.py
Normal file
9
ietf/secr/drafts/report_progress_report.py
Normal 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
27
ietf/secr/drafts/tests.py
Normal 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
25
ietf/secr/drafts/urls.py
Normal 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
1284
ietf/secr/drafts/views.py
Normal file
File diff suppressed because it is too large
Load diff
0
ietf/secr/groups/__init__.py
Normal file
0
ietf/secr/groups/__init__.py
Normal file
174
ietf/secr/groups/forms.py
Normal file
174
ietf/secr/groups/forms.py
Normal 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)
|
1
ietf/secr/groups/models.py
Normal file
1
ietf/secr/groups/models.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
from django.db import models
|
138
ietf/secr/groups/tests.py
Normal file
138
ietf/secr/groups/tests.py
Normal 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)
|
15
ietf/secr/groups/update_text_charters.py
Normal file
15
ietf/secr/groups/update_text_charters.py
Normal 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
16
ietf/secr/groups/urls.py
Normal 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
492
ietf/secr/groups/views.py
Normal 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, {}),
|
||||||
|
)
|
0
ietf/secr/ipradmin/__init__.py
Normal file
0
ietf/secr/ipradmin/__init__.py
Normal file
0
ietf/secr/ipradmin/admin.py
Normal file
0
ietf/secr/ipradmin/admin.py
Normal file
334
ietf/secr/ipradmin/forms.py
Normal file
334
ietf/secr/ipradmin/forms.py
Normal 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',
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
|
28
ietf/secr/ipradmin/managers.py
Normal file
28
ietf/secr/ipradmin/managers.py
Normal 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()
|
193
ietf/secr/ipradmin/models.py
Normal file
193
ietf/secr/ipradmin/models.py
Normal 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'
|
||||||
|
|
||||||
|
'''
|
28
ietf/secr/ipradmin/tests.py
Normal file
28
ietf/secr/ipradmin/tests.py
Normal 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)
|
||||||
|
"""
|
25
ietf/secr/ipradmin/urls.py
Normal file
25
ietf/secr/ipradmin/urls.py
Normal 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
828
ietf/secr/ipradmin/views.py
Normal 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
|
1
ietf/secr/lib/__init__.py
Normal file
1
ietf/secr/lib/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
from template import template, jsonapi
|
36
ietf/secr/lib/template.py
Normal file
36
ietf/secr/lib/template.py
Normal 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))
|
0
ietf/secr/meetings/__init__.py
Normal file
0
ietf/secr/meetings/__init__.py
Normal file
88
ietf/secr/meetings/blue_sheets.py
Normal file
88
ietf/secr/meetings/blue_sheets.py
Normal 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
210
ietf/secr/meetings/forms.py
Normal 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
|
54
ietf/secr/meetings/models.py
Normal file
54
ietf/secr/meetings/models.py
Normal 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"
|
||||||
|
|
||||||
|
"""
|
26
ietf/secr/meetings/tests.py
Normal file
26
ietf/secr/meetings/tests.py
Normal 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)
|
22
ietf/secr/meetings/urls.py
Normal file
22
ietf/secr/meetings/urls.py
Normal 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
861
ietf/secr/meetings/views.py
Normal 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, {}),
|
||||||
|
)
|
0
ietf/secr/middleware/__init__.py
Normal file
0
ietf/secr/middleware/__init__.py
Normal file
58
ietf/secr/middleware/secauth.py
Normal file
58
ietf/secr/middleware/secauth.py
Normal 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
|
||||||
|
|
0
ietf/secr/proceedings/__init__.py
Normal file
0
ietf/secr/proceedings/__init__.py
Normal file
104
ietf/secr/proceedings/forms.py
Normal file
104
ietf/secr/proceedings/forms.py
Normal 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
|
||||||
|
|
||||||
|
|
73
ietf/secr/proceedings/models.py
Normal file
73
ietf/secr/proceedings/models.py
Normal 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'
|
50
ietf/secr/proceedings/new_tables.sql
Normal file
50
ietf/secr/proceedings/new_tables.sql
Normal 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;
|
||||||
|
|
543
ietf/secr/proceedings/proc_utils.py
Normal file
543
ietf/secr/proceedings/proc_utils.py
Normal 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)
|
||||||
|
|
14
ietf/secr/proceedings/report_progress_report.py
Normal file
14
ietf/secr/proceedings/report_progress_report.py
Normal 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)
|
0
ietf/secr/proceedings/templatetags/__init__.py
Normal file
0
ietf/secr/proceedings/templatetags/__init__.py
Normal file
70
ietf/secr/proceedings/templatetags/ams_filters.py
Normal file
70
ietf/secr/proceedings/templatetags/ams_filters.py
Normal 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
|
28
ietf/secr/proceedings/tests.py
Normal file
28
ietf/secr/proceedings/tests.py
Normal 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)
|
31
ietf/secr/proceedings/urls.py
Normal file
31
ietf/secr/proceedings/urls.py
Normal 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'),
|
||||||
|
)
|
915
ietf/secr/proceedings/views.py
Normal file
915
ietf/secr/proceedings/views.py
Normal 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, {}),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
0
ietf/secr/roles/__init__.py
Normal file
0
ietf/secr/roles/__init__.py
Normal file
44
ietf/secr/roles/forms.py
Normal file
44
ietf/secr/roles/forms.py
Normal 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
Loading…
Reference in a new issue