diff --git a/form_utils/__init__.py b/form_utils/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/form_utils/admin.py b/form_utils/admin.py
new file mode 100644
index 000000000..b074bdb99
--- /dev/null
+++ b/form_utils/admin.py
@@ -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
diff --git a/form_utils/fields.py b/form_utils/fields.py
new file mode 100644
index 000000000..d1d00ecf7
--- /dev/null
+++ b/form_utils/fields.py
@@ -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
diff --git a/form_utils/forms.py b/form_utils/forms.py
new file mode 100644
index 000000000..be21ea2c8
--- /dev/null
+++ b/form_utils/forms.py
@@ -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
diff --git a/form_utils/media/form_utils/js/autoresize.js b/form_utils/media/form_utils/js/autoresize.js
new file mode 100644
index 000000000..8c269b866
--- /dev/null
+++ b/form_utils/media/form_utils/js/autoresize.js
@@ -0,0 +1,3 @@
+$(document).ready(function() {
+ $('textarea.autoresize').autogrow();
+ });
diff --git a/form_utils/media/form_utils/js/jquery.autogrow.js b/form_utils/media/form_utils/js/jquery.autogrow.js
new file mode 100644
index 000000000..aeae46545
--- /dev/null
+++ b/form_utils/media/form_utils/js/jquery.autogrow.js
@@ -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('
');
+ 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, ' new');
+ }
+ else
+ {
+ html = html.replace(/\n/g, ' 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);
\ No newline at end of file
diff --git a/form_utils/models.py b/form_utils/models.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/form_utils/settings.py b/form_utils/settings.py
new file mode 100644
index 000000000..a8462e62f
--- /dev/null
+++ b/form_utils/settings.py
@@ -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)
diff --git a/form_utils/templates/form_utils/better_form.html b/form_utils/templates/form_utils/better_form.html
new file mode 100644
index 000000000..d25e429b1
--- /dev/null
+++ b/form_utils/templates/form_utils/better_form.html
@@ -0,0 +1,16 @@
+{% extends "form_utils/form.html" %}
+
+{% block fields %}
+{% for fieldset in form.fieldsets %}
+
+{% endfor %}
+{% endblock %}
diff --git a/form_utils/templates/form_utils/fields_as_lis.html b/form_utils/templates/form_utils/fields_as_lis.html
new file mode 100644
index 000000000..99c79719f
--- /dev/null
+++ b/form_utils/templates/form_utils/fields_as_lis.html
@@ -0,0 +1,11 @@
+{% for field in fields %}
+{% if field.is_hidden %}
+{{ field }}
+{% else %}
+
+
+ ''' % (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
+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 = '''
+
Notification for %s
+
+
+ ''' % (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('This update was notifed to the submitter of the IPR that is being updated on %s.' % 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.
+ Click here 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('
This IPR disclosure does not comply with the formal requirements of Section 6, "IPR Disclosures," of RFC 3979, "Intellectual Property Rights in IETF Technology."
')
+
+ # 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.
+ %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.
+ Click here 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("%s" % 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(
+ "%s
%s" % (
+ 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("%s" % 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
diff --git a/ietf/secr/lib/__init__.py b/ietf/secr/lib/__init__.py
new file mode 100644
index 000000000..10c12c0ce
--- /dev/null
+++ b/ietf/secr/lib/__init__.py
@@ -0,0 +1 @@
+from template import template, jsonapi
diff --git a/ietf/secr/lib/template.py b/ietf/secr/lib/template.py
new file mode 100644
index 000000000..8bfd5127d
--- /dev/null
+++ b/ietf/secr/lib/template.py
@@ -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))
diff --git a/ietf/secr/meetings/__init__.py b/ietf/secr/meetings/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/ietf/secr/meetings/blue_sheets.py b/ietf/secr/meetings/blue_sheets.py
new file mode 100644
index 000000000..6388fb6f6
--- /dev/null
+++ b/ietf/secr/meetings/blue_sheets.py
@@ -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()
diff --git a/ietf/secr/meetings/forms.py b/ietf/secr/meetings/forms.py
new file mode 100644
index 000000000..16aea5cf3
--- /dev/null
+++ b/ietf/secr/meetings/forms.py
@@ -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:
+ Tutorials = Education,
+ Code Sprint = Tools Team,
+ Technical Plenary = IAB,
+ 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
\ No newline at end of file
diff --git a/ietf/secr/meetings/models.py b/ietf/secr/meetings/models.py
new file mode 100644
index 000000000..ac1107ea9
--- /dev/null
+++ b/ietf/secr/meetings/models.py
@@ -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"
+
+"""
\ No newline at end of file
diff --git a/ietf/secr/meetings/tests.py b/ietf/secr/meetings/tests.py
new file mode 100644
index 000000000..223107590
--- /dev/null
+++ b/ietf/secr/meetings/tests.py
@@ -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)
diff --git a/ietf/secr/meetings/urls.py b/ietf/secr/meetings/urls.py
new file mode 100644
index 000000000..0fce66a5c
--- /dev/null
+++ b/ietf/secr/meetings/urls.py
@@ -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\d{1,6})/(?P\d)/$', 'ajax_get_times', name='meetings_ajax_get_times'),
+ url(r'^blue_sheet/$', 'blue_sheet_redirect', name='meetings_blue_sheet_redirect'),
+ url(r'^(?P\d{1,6})/$', 'view', name='meetings_view'),
+ url(r'^(?P\d{1,6})/blue_sheet/$', 'blue_sheet', name='meetings_blue_sheet'),
+ url(r'^(?P\d{1,6})/blue_sheet/generate/$', 'blue_sheet_generate', name='meetings_blue_sheet_generate'),
+ url(r'^(?P\d{1,6})/edit/$', 'edit_meeting', name='meetings_edit_meeting'),
+ url(r'^(?P\d{1,6})/rooms/$', 'rooms', name='meetings_rooms'),
+ url(r'^(?P\d{1,6})/times/$', 'times', name='meetings_times'),
+ url(r'^(?P\d{1,6})/times/delete/(?P