Make new branch from trunk and merge facelift-r9007 into it, fixing a few merge conflicts

- Legacy-Id: 9072
This commit is contained in:
Ole Laursen 2015-02-12 10:43:52 +00:00
commit a225fd5069
804 changed files with 20101 additions and 18387 deletions

25
README Normal file
View file

@ -0,0 +1,25 @@
This is the "facelift" datatracker branch that uses Twitter Bootstrap for
the UI.
You need to install a few new django extensions:
https://pypi.python.org/pypi/django-widget-tweaks
https://pypi.python.org/pypi/django-bootstrap3
https://pypi.python.org/pypi/django-typogrify
The meta goal of this effort is: *** NO CHANGES TO THE PYTHON CODE ***
Whenever changes to the python code are made, they can only fix HTML bugs,
or add comments (tagged with "FACELIFT") about functionality that can be
removed once the facelift templates become default. Or they need to add
functionality that is only called from the new facelift templates.
Javascript that is only used on one template goes into that template.
Javascript that is used by more than one template goes into ietf.js.
CSS that is only used on one template goes into that template.
CSS that is used by more than one template goes into ietf.css. No CSS in the
templates or - god forbid - style tags! (And no CSS or HTML styling in
python code!!)
Templates that use jquery or bootstrap plugins include the css in the pagehead
block, and the js in the js block.

11
TODO Normal file
View file

@ -0,0 +1,11 @@
Major pieces not facelifted: milestone editing, liaison editing, WG workflow customization
Use affix for navigation on active_wgs.html
Figure out why {% if debug %} does not work in the text templates under ietf/templates_facelift/community/public.
Make django generate HTML5 date inputs or use a js-based datepicker.
Deferring ballots does not work. (Seems to be an upstream bug.)
Make tables that are too wide to usefully work on small screens responsive. See http://getbootstrap.com/css/#tables-responsive

3
bootstrap3/__init__.py Normal file
View file

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
__version__ = '5.1.1'

106
bootstrap3/bootstrap.py Normal file
View file

@ -0,0 +1,106 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.conf import settings
from django.utils.importlib import import_module
# Default settings
BOOTSTRAP3_DEFAULTS = {
'jquery_url': '//code.jquery.com/jquery.min.js',
'base_url': '//netdna.bootstrapcdn.com/bootstrap/3.3.2/',
'css_url': None,
'theme_url': None,
'javascript_url': None,
'javascript_in_head': False,
'include_jquery': False,
'horizontal_label_class': 'col-md-2',
'horizontal_field_class': 'col-md-4',
'set_required': True,
'set_placeholder': True,
'required_css_class': '',
'error_css_class': 'has-error',
'success_css_class': 'has-success',
'formset_renderers': {
'default': 'bootstrap3.renderers.FormsetRenderer',
},
'form_renderers': {
'default': 'bootstrap3.renderers.FormRenderer',
},
'field_renderers': {
'default': 'bootstrap3.renderers.FieldRenderer',
'inline': 'bootstrap3.renderers.InlineFieldRenderer',
},
}
# Start with a copy of default settings
BOOTSTRAP3 = BOOTSTRAP3_DEFAULTS.copy()
# Override with user settings from settings.py
BOOTSTRAP3.update(getattr(settings, 'BOOTSTRAP3', {}))
def get_bootstrap_setting(setting, default=None):
"""
Read a setting
"""
return BOOTSTRAP3.get(setting, default)
def bootstrap_url(postfix):
"""
Prefix a relative url with the bootstrap base url
"""
return get_bootstrap_setting('base_url') + postfix
def jquery_url():
"""
Return the full url to jQuery file to use
"""
return get_bootstrap_setting('jquery_url')
def javascript_url():
"""
Return the full url to the Bootstrap JavaScript file
"""
return get_bootstrap_setting('javascript_url') or \
bootstrap_url('js/bootstrap.min.js')
def css_url():
"""
Return the full url to the Bootstrap CSS file
"""
return get_bootstrap_setting('css_url') or \
bootstrap_url('css/bootstrap.min.css')
def theme_url():
"""
Return the full url to the theme CSS file
"""
return get_bootstrap_setting('theme_url')
def get_renderer(renderers, **kwargs):
layout = kwargs.get('layout', '')
path = renderers.get(layout, renderers['default'])
mod, cls = path.rsplit(".", 1)
return getattr(import_module(mod), cls)
def get_formset_renderer(**kwargs):
renderers = get_bootstrap_setting('formset_renderers')
return get_renderer(renderers, **kwargs)
def get_form_renderer(**kwargs):
renderers = get_bootstrap_setting('form_renderers')
return get_renderer(renderers, **kwargs)
def get_field_renderer(**kwargs):
renderers = get_bootstrap_setting('field_renderers')
return get_renderer(renderers, **kwargs)

37
bootstrap3/components.py Normal file
View file

@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.forms.widgets import flatatt
from .text import text_value
def render_icon(icon, title=''):
"""
Render a Bootstrap glyphicon icon
"""
attrs = {
'class': 'glyphicon glyphicon-{icon}'.format(icon=icon),
}
if title:
attrs['title'] = title
return '<span{attrs}></span>'.format(attrs=flatatt(attrs))
def render_alert(content, alert_type=None, dismissable=True):
"""
Render a Bootstrap alert
"""
button = ''
if not alert_type:
alert_type = 'info'
css_classes = ['alert', 'alert-' + text_value(alert_type)]
if dismissable:
css_classes.append('alert-dismissable')
button = '<button type="button" class="close" ' + \
'data-dismiss="alert" aria-hidden="true">&times;</button>'
return '<div class="{css_classes}">{button}{content}</div>'.format(
css_classes=' '.join(css_classes),
button=button,
content=text_value(content),
)

16
bootstrap3/exceptions.py Normal file
View file

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
class BootstrapException(Exception):
"""
Any exception from this package
"""
pass
class BootstrapError(BootstrapException):
"""
Any exception that is an error
"""
pass

173
bootstrap3/forms.py Normal file
View file

@ -0,0 +1,173 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.contrib.admin.widgets import AdminFileWidget
from django.forms import (
HiddenInput, FileInput, CheckboxSelectMultiple, Textarea, TextInput
)
from .bootstrap import (
get_bootstrap_setting, get_form_renderer, get_field_renderer,
get_formset_renderer
)
from .text import text_concat, text_value
from .exceptions import BootstrapError
from .utils import add_css_class, render_tag
from .components import render_icon
FORM_GROUP_CLASS = 'form-group'
def render_formset(formset, **kwargs):
"""
Render a formset to a Bootstrap layout
"""
renderer_cls = get_formset_renderer(**kwargs)
return renderer_cls(formset, **kwargs).render()
def render_formset_errors(form, **kwargs):
"""
Render formset errors to a Bootstrap layout
"""
renderer_cls = get_formset_renderer(**kwargs)
return renderer_cls(form, **kwargs).render_errors()
def render_form(form, **kwargs):
"""
Render a formset to a Bootstrap layout
"""
renderer_cls = get_form_renderer(**kwargs)
return renderer_cls(form, **kwargs).render()
def render_form_errors(form, type='all', **kwargs):
"""
Render form errors to a Bootstrap layout
"""
renderer_cls = get_form_renderer(**kwargs)
return renderer_cls(form, **kwargs).render_errors(type)
def render_field(field, **kwargs):
"""
Render a formset to a Bootstrap layout
"""
renderer_cls = get_field_renderer(**kwargs)
return renderer_cls(field, **kwargs).render()
def render_label(content, label_for=None, label_class=None, label_title=''):
"""
Render a label with content
"""
attrs = {}
if label_for:
attrs['for'] = label_for
if label_class:
attrs['class'] = label_class
if label_title:
attrs['title'] = label_title
return render_tag('label', attrs=attrs, content=content)
def render_button(
content, button_type=None, icon=None, button_class='', size='',
href=''):
"""
Render a button with content
"""
attrs = {}
classes = add_css_class('btn', button_class)
size = text_value(size).lower().strip()
if size == 'xs':
classes = add_css_class(classes, 'btn-xs')
elif size == 'sm' or size == 'small':
classes = add_css_class(classes, 'btn-sm')
elif size == 'lg' or size == 'large':
classes = add_css_class(classes, 'btn-lg')
elif size == 'md' or size == 'medium':
pass
elif size:
raise BootstrapError(
'Parameter "size" should be "xs", "sm", "lg" or ' +
'empty ("{}" given).'.format(size))
if button_type:
if button_type == 'submit':
classes = add_css_class(classes, 'btn-primary')
elif button_type not in ('reset', 'button', 'link'):
raise BootstrapError(
'Parameter "button_type" should be "submit", "reset", ' +
'"button", "link" or empty ("{}" given).'.format(button_type))
attrs['type'] = button_type
attrs['class'] = classes
icon_content = render_icon(icon) if icon else ''
if href:
attrs['href'] = href
tag = 'a'
else:
tag = 'button'
return render_tag(
tag, attrs=attrs, content=text_concat(
icon_content, content, separator=' '))
def render_field_and_label(
field, label, field_class='', label_for=None, label_class='',
layout='', **kwargs):
"""
Render a field with its label
"""
if layout == 'horizontal':
if not label_class:
label_class = get_bootstrap_setting('horizontal_label_class')
if not field_class:
field_class = get_bootstrap_setting('horizontal_field_class')
if not label:
label = '&#160;'
label_class = add_css_class(label_class, 'control-label')
html = field
if field_class:
html = '<div class="{klass}">{html}</div>'.format(
klass=field_class, html=html)
if label:
html = render_label(
label, label_for=label_for, label_class=label_class) + html
return html
def render_form_group(content, css_class=FORM_GROUP_CLASS):
"""
Render a Bootstrap form group
"""
return '<div class="{klass}">{content}</div>'.format(
klass=css_class,
content=content,
)
def is_widget_required_attribute(widget):
"""
Is this widget required?
"""
if not get_bootstrap_setting('set_required'):
return False
if not widget.is_required:
return False
if isinstance(
widget, (
AdminFileWidget, HiddenInput, FileInput,
CheckboxSelectMultiple)):
return False
return True
def is_widget_with_placeholder(widget):
"""
Is this a widget that should have a placeholder?
Only text, search, url, tel, e-mail, password, number have placeholders
These are all derived form TextInput, except for Textarea
"""
return isinstance(widget, (TextInput, Textarea))

3
bootstrap3/models.py Normal file
View file

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
# Empty models.py, required file for Django tests

485
bootstrap3/renderers.py Normal file
View file

@ -0,0 +1,485 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.contrib.auth.forms import ReadOnlyPasswordHashWidget
from django.forms import (
TextInput, DateInput, FileInput, CheckboxInput,
ClearableFileInput, Select, RadioSelect, CheckboxSelectMultiple
)
from django.forms.extras import SelectDateWidget
from django.forms.forms import BaseForm, BoundField
from django.forms.formsets import BaseFormSet
from django.utils.html import conditional_escape, strip_tags
from django.template import Context
from django.template.loader import get_template
from django.utils.safestring import mark_safe
from .bootstrap import get_bootstrap_setting
from .text import text_value
from .exceptions import BootstrapError
from .utils import add_css_class
from .forms import (
render_form, render_field, render_label, render_form_group,
is_widget_with_placeholder, is_widget_required_attribute, FORM_GROUP_CLASS
)
class BaseRenderer(object):
def __init__(self, *args, **kwargs):
self.layout = kwargs.get('layout', '')
self.form_group_class = kwargs.get(
'form_group_class', FORM_GROUP_CLASS)
self.field_class = kwargs.get('field_class', '')
self.label_class = kwargs.get('label_class', '')
self.show_help = kwargs.get('show_help', True)
self.show_label = kwargs.get('show_label', True)
self.exclude = kwargs.get('exclude', '')
self.set_required = kwargs.get('set_required', True)
self.size = self.parse_size(kwargs.get('size', ''))
self.horizontal_label_class = kwargs.get(
'horizontal_label_class',
get_bootstrap_setting('horizontal_label_class')
)
self.horizontal_field_class = kwargs.get(
'horizontal_field_class',
get_bootstrap_setting('horizontal_field_class')
)
def parse_size(self, size):
size = text_value(size).lower().strip()
if size in ('sm', 'small'):
return 'small'
if size in ('lg', 'large'):
return 'large'
if size in ('md', 'medium', ''):
return 'medium'
raise BootstrapError('Invalid value "%s" for parameter "size" (expected "sm", "md", "lg" or "").' % size)
def get_size_class(self, prefix='input'):
if self.size == 'small':
return prefix + '-sm'
if self.size == 'large':
return prefix + '-lg'
return ''
class FormsetRenderer(BaseRenderer):
"""
Default formset renderer
"""
def __init__(self, formset, *args, **kwargs):
if not isinstance(formset, BaseFormSet):
raise BootstrapError(
'Parameter "formset" should contain a valid Django Formset.')
self.formset = formset
super(FormsetRenderer, self).__init__(*args, **kwargs)
def render_management_form(self):
return text_value(self.formset.management_form)
def render_form(self, form, **kwargs):
return render_form(form, **kwargs)
def render_forms(self):
rendered_forms = []
for form in self.formset.forms:
rendered_forms.append(self.render_form(
form,
layout=self.layout,
form_group_class=self.form_group_class,
field_class=self.field_class,
label_class=self.label_class,
show_help=self.show_help,
exclude=self.exclude,
set_required=self.set_required,
size=self.size,
horizontal_label_class=self.horizontal_label_class,
horizontal_field_class=self.horizontal_field_class,
))
return '\n'.join(rendered_forms)
def get_formset_errors(self):
return self.formset.non_form_errors()
def render_errors(self):
formset_errors = self.get_formset_errors()
if formset_errors:
return get_template(
'bootstrap3/form_errors.html').render(
Context({
'errors': formset_errors,
'form': self.formset,
'layout': self.layout,
})
)
return ''
def render(self):
return self.render_errors() + self.render_management_form() + \
self.render_forms()
class FormRenderer(BaseRenderer):
"""
Default form renderer
"""
def __init__(self, form, *args, **kwargs):
if not isinstance(form, BaseForm):
raise BootstrapError(
'Parameter "form" should contain a valid Django Form.')
self.form = form
super(FormRenderer, self).__init__(*args, **kwargs)
# Handle form.empty_permitted
if self.form.empty_permitted:
self.set_required = False
def render_fields(self):
rendered_fields = []
for field in self.form:
rendered_fields.append(render_field(
field,
layout=self.layout,
form_group_class=self.form_group_class,
field_class=self.field_class,
label_class=self.label_class,
show_help=self.show_help,
exclude=self.exclude,
set_required=self.set_required,
size=self.size,
horizontal_label_class=self.horizontal_label_class,
horizontal_field_class=self.horizontal_field_class,
))
return '\n'.join(rendered_fields)
def get_fields_errors(self):
form_errors = []
for field in self.form:
if field.is_hidden and field.errors:
form_errors += field.errors
return form_errors
def render_errors(self, type='all'):
form_errors = None
if type == 'all':
form_errors = self.get_fields_errors() + \
self.form.non_field_errors()
elif type == 'fields':
form_errors = self.get_fields_errors()
elif type == 'non_fields':
form_errors = self.form.non_field_errors()
if form_errors:
return get_template(
'bootstrap3/form_errors.html').render(
Context({
'errors': form_errors,
'form': self.form,
'layout': self.layout,
})
)
return ''
def render(self):
return self.render_errors() + self.render_fields()
class FieldRenderer(BaseRenderer):
"""
Default field renderer
"""
def __init__(self, field, *args, **kwargs):
if not isinstance(field, BoundField):
raise BootstrapError(
'Parameter "field" should contain a valid Django BoundField.')
self.field = field
super(FieldRenderer, self).__init__(*args, **kwargs)
self.widget = field.field.widget
self.initial_attrs = self.widget.attrs.copy()
self.field_help = text_value(mark_safe(field.help_text)) \
if self.show_help and field.help_text else ''
self.field_errors = [conditional_escape(text_value(error))
for error in field.errors]
if get_bootstrap_setting('set_placeholder'):
self.placeholder = field.label
else:
self.placeholder = ''
self.addon_before = kwargs.get('addon_before', self.initial_attrs.pop('addon_before', ''))
self.addon_after = kwargs.get('addon_after', self.initial_attrs.pop('addon_after', ''))
# These are set in Django or in the global BOOTSTRAP3 settings, and
# they can be overwritten in the template
error_css_class = kwargs.get('error_css_class', '')
required_css_class = kwargs.get('required_css_class', '')
bound_css_class = kwargs.get('bound_css_class', '')
if error_css_class:
self.error_css_class = error_css_class
else:
self.error_css_class = getattr(
field.form, 'error_css_class',
get_bootstrap_setting('error_css_class'))
if required_css_class:
self.required_css_class = required_css_class
else:
self.required_css_class = getattr(
field.form, 'required_css_class',
get_bootstrap_setting('required_css_class'))
if bound_css_class:
self.success_css_class = bound_css_class
else:
self.success_css_class = getattr(
field.form, 'bound_css_class',
get_bootstrap_setting('success_css_class'))
# Handle form.empty_permitted
if self.field.form.empty_permitted:
self.set_required = False
self.required_css_class = ''
def restore_widget_attrs(self):
self.widget.attrs = self.initial_attrs
def add_class_attrs(self):
classes = self.widget.attrs.get('class', '')
if isinstance(self.widget, ReadOnlyPasswordHashWidget):
classes = add_css_class(
classes, 'form-control-static', prepend=True)
elif not isinstance(self.widget, (CheckboxInput,
RadioSelect,
CheckboxSelectMultiple,
FileInput)):
classes = add_css_class(classes, 'form-control', prepend=True)
# For these widget types, add the size class here
classes = add_css_class(classes, self.get_size_class())
self.widget.attrs['class'] = classes
def add_placeholder_attrs(self):
placeholder = self.widget.attrs.get('placeholder', self.placeholder)
if placeholder and is_widget_with_placeholder(self.widget):
self.widget.attrs['placeholder'] = placeholder
def add_help_attrs(self):
if not isinstance(self.widget, CheckboxInput):
self.widget.attrs['title'] = self.widget.attrs.get(
'title', strip_tags(self.field_help))
def add_required_attrs(self):
if self.set_required and is_widget_required_attribute(self.widget):
self.widget.attrs['required'] = 'required'
def add_widget_attrs(self):
self.add_class_attrs()
self.add_placeholder_attrs()
self.add_help_attrs()
self.add_required_attrs()
def list_to_class(self, html, klass):
classes = add_css_class(klass, self.get_size_class())
mapping = [
('<ul', '<div'),
('</ul>', '</div>'),
('<li', '<div class="{klass}"'.format(klass=classes)),
('</li>', '</div>'),
]
for k, v in mapping:
html = html.replace(k, v)
return html
def put_inside_label(self, html):
content = '{field} {label}'.format(field=html, label=self.field.label)
return render_label(
content=content, label_for=self.field.id_for_label,
label_title=strip_tags(self.field_help))
def fix_date_select_input(self, html):
div1 = '<div class="col-xs-4">'
div2 = '</div>'
html = html.replace('<select', div1 + '<select')
html = html.replace('</select>', '</select>' + div2)
return '<div class="row bootstrap3-multi-input">' + html + '</div>'
def fix_clearable_file_input(self, html):
"""
Fix a clearable file input
TODO: This needs improvement
Currently Django returns
Currently:
<a href="dummy.txt">dummy.txt</a>
<input id="file4-clear_id" name="file4-clear" type="checkbox" />
<label for="file4-clear_id">Clear</label><br />
Change: <input id="id_file4" name="file4" type="file" />
<span class=help-block></span>
</div>
"""
# TODO This needs improvement
return '<div class="row bootstrap3-multi-input">' + \
'<div class="col-xs-12">' + html + '</div></div>'
def post_widget_render(self, html):
if isinstance(self.widget, RadioSelect):
html = self.list_to_class(html, 'radio')
elif isinstance(self.widget, CheckboxSelectMultiple):
html = self.list_to_class(html, 'checkbox')
elif isinstance(self.widget, SelectDateWidget):
html = self.fix_date_select_input(html)
elif isinstance(self.widget, ClearableFileInput):
html = self.fix_clearable_file_input(html)
elif isinstance(self.widget, CheckboxInput):
html = self.put_inside_label(html)
return html
def wrap_widget(self, html):
if isinstance(self.widget, CheckboxInput):
checkbox_class = add_css_class('checkbox', self.get_size_class())
html = \
'<div class="{klass}">{content}</div>'.format(
klass=checkbox_class, content=html
)
return html
def make_input_group(self, html):
if (
(self.addon_before or self.addon_after) and
isinstance(self.widget, (TextInput, DateInput, Select))
):
before = '<span class="input-group-addon">{addon}</span>'.format(
addon=self.addon_before) if self.addon_before else ''
after = '<span class="input-group-addon">{addon}</span>'.format(
addon=self.addon_after) if self.addon_after else ''
html = \
'<div class="input-group">' + \
'{before}{html}{after}</div>'.format(
before=before,
after=after,
html=html
)
return html
def append_to_field(self, html):
help_text_and_errors = [self.field_help] + self.field_errors \
if self.field_help else self.field_errors
if help_text_and_errors:
help_html = get_template(
'bootstrap3/field_help_text_and_errors.html'
).render(Context({
'field': self.field,
'help_text_and_errors': help_text_and_errors,
'layout': self.layout,
}))
html += '<span class="help-block">{help}</span>'.format(
help=help_html)
return html
def get_field_class(self):
field_class = self.field_class
if not field_class and self.layout == 'horizontal':
field_class = self.horizontal_field_class
return field_class
def wrap_field(self, html):
field_class = self.get_field_class()
if field_class:
html = '<div class="{klass}">{html}</div>'.format(
klass=field_class, html=html)
return html
def get_label_class(self):
label_class = self.label_class
if not label_class and self.layout == 'horizontal':
label_class = self.horizontal_label_class
label_class = text_value(label_class)
if not self.show_label:
label_class = add_css_class(label_class, 'sr-only')
return add_css_class(label_class, 'control-label')
def get_label(self):
if isinstance(self.widget, CheckboxInput):
label = None
else:
label = self.field.label
if self.layout == 'horizontal' and not label:
return '&#160;'
return label
def add_label(self, html):
label = self.get_label()
if label:
html = render_label(
label,
label_for=self.field.id_for_label,
label_class=self.get_label_class()) + html
return html
def get_form_group_class(self):
form_group_class = self.form_group_class
if self.field.errors and self.error_css_class:
form_group_class = add_css_class(
form_group_class, self.error_css_class)
if self.field.field.required and self.required_css_class:
form_group_class = add_css_class(
form_group_class, self.required_css_class)
if self.field_errors:
form_group_class = add_css_class(form_group_class, 'has-error')
elif self.field.form.is_bound:
form_group_class = add_css_class(
form_group_class, self.success_css_class)
if self.layout == 'horizontal':
form_group_class = add_css_class(
form_group_class, self.get_size_class(prefix='form-group'))
return form_group_class
def wrap_label_and_field(self, html):
return render_form_group(html, self.get_form_group_class())
def render(self):
# See if we're not excluded
if self.field.name in self.exclude.replace(' ', '').split(','):
return ''
# Hidden input requires no special treatment
if self.field.is_hidden:
return text_value(self.field)
# Render the widget
self.add_widget_attrs()
html = self.field.as_widget(attrs=self.widget.attrs)
self.restore_widget_attrs()
# Start post render
html = self.post_widget_render(html)
html = self.wrap_widget(html)
html = self.make_input_group(html)
html = self.append_to_field(html)
html = self.wrap_field(html)
html = self.add_label(html)
html = self.wrap_label_and_field(html)
return html
class InlineFieldRenderer(FieldRenderer):
"""
Inline field renderer
"""
def add_error_attrs(self):
field_title = self.widget.attrs.get('title', '')
field_title += ' ' + ' '.join(
[strip_tags(e) for e in self.field_errors])
self.widget.attrs['title'] = field_title.strip()
def add_widget_attrs(self):
super(InlineFieldRenderer, self).add_widget_attrs()
self.add_error_attrs()
def append_to_field(self, html):
return html
def get_field_class(self):
return self.field_class
def get_label_class(self):
return add_css_class(self.label_class, 'sr-only')

56
bootstrap3/templates.py Normal file
View file

@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import re
from django.template import Variable, VariableDoesNotExist
from django.template.base import (
FilterExpression, kwarg_re, TemplateSyntaxError
)
# Extra features for template file handling
QUOTED_STRING = re.compile(r'^["\'](?P<noquotes>.+)["\']$')
def handle_var(value, context):
# Resolve FilterExpression and Variable immediately
if isinstance(value, FilterExpression) or isinstance(value, Variable):
return value.resolve(context)
# Return quoted strings unquotes, from djangosnippets.org/snippets/886
stringval = QUOTED_STRING.search(value)
if stringval:
return stringval.group('noquotes')
# Resolve variable or return string value
try:
return Variable(value).resolve(context)
except VariableDoesNotExist:
return value
def parse_token_contents(parser, token):
bits = token.split_contents()
tag = bits.pop(0)
args = []
kwargs = {}
asvar = None
if len(bits) >= 2 and bits[-2] == 'as':
asvar = bits[-1]
bits = bits[:-2]
if len(bits):
for bit in bits:
match = kwarg_re.match(bit)
if not match:
raise TemplateSyntaxError(
'Malformed arguments to tag "{}"'.format(tag))
name, value = match.groups()
if name:
kwargs[name] = parser.compile_filter(value)
else:
args.append(parser.compile_filter(value))
return {
'tag': tag,
'args': args,
'kwargs': kwargs,
'asvar': asvar,
}

View file

@ -0,0 +1,27 @@
<!DOCTYPE html>
{% load bootstrap3 %}
<html{% if request.LANGUAGE_CODE %} lang="{{ request.LANGUAGE_CODE }}"{% endif %}>
<head>
<meta charset="utf-8">
<!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=edge" /><![endif]-->
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block bootstrap3_title %}django-bootstrap3 template title{% endblock %}</title>
{% bootstrap_css %}
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
<![endif]-->
{% if 'javascript_in_head'|bootstrap_setting %}{% bootstrap_javascript jquery=True %}{% endif %}
{% block bootstrap3_extra_head %}{% endblock %}
</head>
<body>
{% block bootstrap3_content %}django-bootstrap3 template content{% endblock %}
{% if not 'javascript_in_head'|bootstrap_setting %}{% bootstrap_javascript jquery=True %}{% endif %}
{% block bootstrap3_extra_script %}{% endblock %}
</body>
</html>

View file

@ -0,0 +1 @@
{{ help_text_and_errors|join:' ' }}

View file

@ -0,0 +1,6 @@
<div class="alert alert-danger alert-dismissable alert-link">
<button class="close" type="button" data-dismiss="alert" aria-hidden="true">&#215;</button>
{% for error in errors %}
{{ error }}{% if not forloop.last %}<br>{% endif %}
{% endfor %}
</div>

View file

@ -0,0 +1,6 @@
{% for message in messages %}
<div class="alert{% if message.tags %} alert-{% if message.tags == 'error' %}danger{% else %}{{ message.tags }}{% endif %}{% endif %} alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&#215;</button>
{{ message|safe }}
</div>
{% endfor %}

View file

@ -0,0 +1,33 @@
{% with bootstrap_pagination_url=bootstrap_pagination_url|default:"?" %}
<ul class="{{ pagination_css_classes }}">
<li class="prev{% if current_page == 1 %} disabled{% endif %}">
<a href="{% if current_page == 1 %}#{% else %}{{ bootstrap_pagination_url }}{{ parameter_name }}=1{% endif %}">&laquo;</a>
</li>
{% if pages_back %}
<li>
<a href="{{ bootstrap_pagination_url }}{{ parameter_name }}={{ pages_back }}">&hellip;</a>
</li>
{% endif %}
{% for p in pages_shown %}
<li{% if current_page == p %} class="active"{% endif %}>
<a href="{% if current_page == p %}#{% else %}{{ bootstrap_pagination_url }}{{ parameter_name }}={{ p }}{% endif %}">{{ p }}</a>
</li>
{% endfor %}
{% if pages_forward %}
<li>
<a href="{{ bootstrap_pagination_url }}{{ parameter_name }}={{ pages_forward }}">&hellip;</a>
</li>
{% endif %}
<li class="last{% if current_page == num_pages %} disabled{% endif %}">
<a href="{% if current_page == num_pages %}#{% else %}{{ bootstrap_pagination_url }}{{ parameter_name }}={{ num_pages }}{% endif %}">&raquo;</a>
</li>
</ul>
{% endwith %}

View file

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

View file

@ -0,0 +1,637 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import re
from math import floor
from django import template
from django.template.loader import get_template
from ..bootstrap import (
css_url, javascript_url, jquery_url, theme_url, get_bootstrap_setting
)
from ..utils import render_link_tag
from ..forms import (
render_button, render_field, render_field_and_label, render_form,
render_form_group, render_formset,
render_label, render_form_errors, render_formset_errors
)
from ..components import render_icon, render_alert
from ..templates import handle_var, parse_token_contents
from ..text import force_text
register = template.Library()
@register.filter
def bootstrap_setting(value):
"""
A simple way to read bootstrap settings in a template.
Please consider this filter private for now, do not use it in your own
templates.
"""
return get_bootstrap_setting(value)
@register.simple_tag
def bootstrap_jquery_url():
"""
**Tag name**::
bootstrap_jquery_url
Return the full url to jQuery file to use
Default value: ``//code.jquery.com/jquery.min.js``
This value is configurable, see Settings section
**usage**::
{% bootstrap_jquery_url %}
**example**::
{% bootstrap_jquery_url %}
"""
return jquery_url()
@register.simple_tag
def bootstrap_javascript_url():
"""
Return the full url to the Bootstrap JavaScript library
Default value: ``None``
This value is configurable, see Settings section
**Tag name**::
bootstrap_javascript_url
**usage**::
{% bootstrap_javascript_url %}
**example**::
{% bootstrap_javascript_url %}
"""
return javascript_url()
@register.simple_tag
def bootstrap_css_url():
"""
Return the full url to the Bootstrap CSS library
Default value: ``None``
This value is configurable, see Settings section
**Tag name**::
bootstrap_css_url
**usage**::
{% bootstrap_css_url %}
**example**::
{% bootstrap_css_url %}
"""
return css_url()
@register.simple_tag
def bootstrap_theme_url():
"""
Return the full url to a Bootstrap theme CSS library
Default value: ``None``
This value is configurable, see Settings section
**Tag name**::
bootstrap_css_url
**usage**::
{% bootstrap_css_url %}
**example**::
{% bootstrap_css_url %}
"""
return theme_url()
@register.simple_tag
def bootstrap_css():
"""
Return HTML for Bootstrap CSS
Adjust url in settings. If no url is returned, we don't want this statement
to return any HTML.
This is intended behavior.
Default value: ``FIXTHIS``
This value is configurable, see Settings section
**Tag name**::
bootstrap_css
**usage**::
{% bootstrap_css %}
**example**::
{% bootstrap_css %}
"""
urls = [url for url in [bootstrap_css_url(), bootstrap_theme_url()] if url]
return ''.join([render_link_tag(url) for url in urls])
@register.simple_tag
def bootstrap_javascript(jquery=None):
"""
Return HTML for Bootstrap JavaScript.
Adjust url in settings. If no url is returned, we don't want this
statement to return any HTML.
This is intended behavior.
Default value: ``None``
This value is configurable, see Settings section
**Tag name**::
bootstrap_javascript
**Parameters**:
:jquery: Truthy to include jQuery as well as Bootstrap
**usage**::
{% bootstrap_javascript %}
**example**::
{% bootstrap_javascript jquery=1 %}
"""
javascript = ''
# See if we have to include jQuery
if jquery is None:
jquery = get_bootstrap_setting('include_jquery', False)
# NOTE: No async on scripts, not mature enough. See issue #52 and #56
if jquery:
url = bootstrap_jquery_url()
if url:
javascript += '<script src="{url}"></script>'.format(url=url)
url = bootstrap_javascript_url()
if url:
javascript += '<script src="{url}"></script>'.format(url=url)
return javascript
@register.simple_tag
def bootstrap_formset(*args, **kwargs):
"""
Render a formset
**Tag name**::
bootstrap_formset
**Parameters**:
:args:
:kwargs:
**usage**::
{% bootstrap_formset formset %}
**example**::
{% bootstrap_formset formset layout='horizontal' %}
"""
return render_formset(*args, **kwargs)
@register.simple_tag
def bootstrap_formset_errors(*args, **kwargs):
"""
Render form errors
**Tag name**::
bootstrap_form_errors
**Parameters**:
:args:
:kwargs:
**usage**::
{% bootstrap_form_errors form %}
**example**::
{% bootstrap_form_errors form layout='inline' %}
"""
return render_formset_errors(*args, **kwargs)
@register.simple_tag
def bootstrap_form(*args, **kwargs):
"""
Render a form
**Tag name**::
bootstrap_form
**Parameters**:
:args:
:kwargs:
**usage**::
{% bootstrap_form form %}
**example**::
{% bootstrap_form form layout='inline' %}
"""
return render_form(*args, **kwargs)
@register.simple_tag
def bootstrap_form_errors(*args, **kwargs):
"""
Render form errors
**Tag name**::
bootstrap_form_errors
**Parameters**:
:args:
:kwargs:
**usage**::
{% bootstrap_form_errors form %}
**example**::
{% bootstrap_form_errors form layout='inline' %}
"""
return render_form_errors(*args, **kwargs)
@register.simple_tag
def bootstrap_field(*args, **kwargs):
"""
Render a field
**Tag name**::
bootstrap_field
**Parameters**:
:args:
:kwargs:
**usage**::
{% bootstrap_field form_field %}
**example**::
{% bootstrap_field form_field %}
"""
return render_field(*args, **kwargs)
@register.simple_tag()
def bootstrap_label(*args, **kwargs):
"""
Render a label
**Tag name**::
bootstrap_label
**Parameters**:
:args:
:kwargs:
**usage**::
{% bootstrap_label FIXTHIS %}
**example**::
{% bootstrap_label FIXTHIS %}
"""
return render_label(*args, **kwargs)
@register.simple_tag
def bootstrap_button(*args, **kwargs):
"""
Render a button
**Tag name**::
bootstrap_button
**Parameters**:
:args:
:kwargs:
**usage**::
{% bootstrap_button FIXTHIS %}
**example**::
{% bootstrap_button FIXTHIS %}
"""
return render_button(*args, **kwargs)
@register.simple_tag
def bootstrap_icon(icon, **kwargs):
"""
Render an icon
**Tag name**::
bootstrap_icon
**Parameters**:
:icon: icon name
**usage**::
{% bootstrap_icon "icon_name" %}
**example**::
{% bootstrap_icon "star" %}
"""
return render_icon(icon, **kwargs)
@register.simple_tag
def bootstrap_alert(content, alert_type='info', dismissable=True):
"""
Render an alert
**Tag name**::
bootstrap_alert
**Parameters**:
:content: HTML content of alert
:alert_type: one of 'info', 'warning', 'danger' or 'success'
:dismissable: boolean, is alert dismissable
**usage**::
{% bootstrap_alert "my_content" %}
**example**::
{% bootstrap_alert "Something went wrong" alert_type='error' %}
"""
return render_alert(content, alert_type, dismissable)
@register.tag('buttons')
def bootstrap_buttons(parser, token):
"""
Render buttons for form
**Tag name**::
bootstrap_buttons
**Parameters**:
:parser:
:token:
**usage**::
{% bootstrap_buttons FIXTHIS %}
**example**::
{% bootstrap_buttons FIXTHIS %}
"""
kwargs = parse_token_contents(parser, token)
kwargs['nodelist'] = parser.parse(('endbuttons', ))
parser.delete_first_token()
return ButtonsNode(**kwargs)
class ButtonsNode(template.Node):
def __init__(self, nodelist, args, kwargs, asvar, **kwargs2):
self.nodelist = nodelist
self.args = args
self.kwargs = kwargs
self.asvar = asvar
def render(self, context):
output_kwargs = {}
for key in self.kwargs:
output_kwargs[key] = handle_var(self.kwargs[key], context)
buttons = []
submit = output_kwargs.get('submit', None)
reset = output_kwargs.get('reset', None)
if submit:
buttons.append(bootstrap_button(submit, 'submit'))
if reset:
buttons.append(bootstrap_button(reset, 'reset'))
buttons = ' '.join(buttons) + self.nodelist.render(context)
output_kwargs.update({
'label': None,
'field': buttons,
})
output = render_form_group(render_field_and_label(**output_kwargs))
if self.asvar:
context[self.asvar] = output
return ''
else:
return output
@register.simple_tag(takes_context=True)
def bootstrap_messages(context, *args, **kwargs):
"""
Show django.contrib.messages Messages in Bootstrap alert containers.
In order to make the alerts dismissable (with the close button),
we have to set the jquery parameter too when using the
bootstrap_javascript tag.
**Tag name**::
bootstrap_messages
**Parameters**:
:context:
:args:
:kwargs:
**usage**::
{% bootstrap_messages FIXTHIS %}
**example**::
{% bootstrap_javascript jquery=1 %}
{% bootstrap_messages FIXTHIS %}
"""
return get_template('bootstrap3/messages.html').render(context)
@register.inclusion_tag('bootstrap3/pagination.html')
def bootstrap_pagination(page, **kwargs):
"""
Render pagination for a page
**Tag name**::
bootstrap_pagination
**Parameters**:
:page:
:parameter_name: Name of paging URL parameter (default: "page")
:kwargs:
**usage**::
{% bootstrap_pagination FIXTHIS %}
**example**::
{% bootstrap_pagination FIXTHIS %}
"""
pagination_kwargs = kwargs.copy()
pagination_kwargs['page'] = page
return get_pagination_context(**pagination_kwargs)
def get_pagination_context(page, pages_to_show=11,
url=None, size=None, extra=None,
parameter_name='page'):
"""
Generate Bootstrap pagination context from a page object
"""
pages_to_show = int(pages_to_show)
if pages_to_show < 1:
raise ValueError("Pagination pages_to_show should be a positive " +
"integer, you specified {pages}".format(
pages=pages_to_show))
num_pages = page.paginator.num_pages
current_page = page.number
half_page_num = int(floor(pages_to_show / 2)) - 1
if half_page_num < 0:
half_page_num = 0
first_page = current_page - half_page_num
if first_page <= 1:
first_page = 1
if first_page > 1:
pages_back = first_page - half_page_num
if pages_back < 1:
pages_back = 1
else:
pages_back = None
last_page = first_page + pages_to_show - 1
if pages_back is None:
last_page += 1
if last_page > num_pages:
last_page = num_pages
if last_page < num_pages:
pages_forward = last_page + half_page_num
if pages_forward > num_pages:
pages_forward = num_pages
else:
pages_forward = None
if first_page > 1:
first_page -= 1
if pages_back is not None and pages_back > 1:
pages_back -= 1
else:
pages_back = None
pages_shown = []
for i in range(first_page, last_page + 1):
pages_shown.append(i)
# Append proper character to url
if url:
# Remove existing page GET parameters
url = force_text(url)
url = re.sub(r'\?{0}\=[^\&]+'.format(parameter_name), '?', url)
url = re.sub(r'\&{0}\=[^\&]+'.format(parameter_name), '', url)
# Append proper separator
if '?' in url:
url += '&'
else:
url += '?'
# Append extra string to url
if extra:
if not url:
url = '?'
url += force_text(extra) + '&'
if url:
url = url.replace('?&', '?')
# Set CSS classes, see http://getbootstrap.com/components/#pagination
pagination_css_classes = ['pagination']
if size == 'small':
pagination_css_classes.append('pagination-sm')
elif size == 'large':
pagination_css_classes.append('pagination-lg')
# Build context object
return {
'bootstrap_pagination_url': url,
'num_pages': num_pages,
'current_page': current_page,
'first_page': first_page,
'last_page': last_page,
'pages_shown': pages_shown,
'pages_back': pages_back,
'pages_forward': pages_forward,
'pagination_css_classes': ' '.join(pagination_css_classes),
'parameter_name': parameter_name,
}

447
bootstrap3/tests.py Normal file
View file

@ -0,0 +1,447 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import re
from django.test import TestCase
from django import forms
from django.template import Template, Context
from .text import text_value, text_concat
from .exceptions import BootstrapError
from .utils import add_css_class
RADIO_CHOICES = (
('1', 'Radio 1'),
('2', 'Radio 2'),
)
MEDIA_CHOICES = (
('Audio', (
('vinyl', 'Vinyl'),
('cd', 'CD'),
)
),
('Video', (
('vhs', 'VHS Tape'),
('dvd', 'DVD'),
)
),
('unknown', 'Unknown'),
)
class TestForm(forms.Form):
"""
Form with a variety of widgets to test bootstrap3 rendering.
"""
date = forms.DateField(required=False)
subject = forms.CharField(
max_length=100,
help_text='my_help_text',
required=True,
widget=forms.TextInput(attrs={'placeholder': 'placeholdertest'}),
)
message = forms.CharField(required=False, help_text='<i>my_help_text</i>')
sender = forms.EmailField(label='Sender © unicode')
secret = forms.CharField(initial=42, widget=forms.HiddenInput)
cc_myself = forms.BooleanField(
required=False, help_text='You will get a copy in your mailbox.')
select1 = forms.ChoiceField(choices=RADIO_CHOICES)
select2 = forms.MultipleChoiceField(
choices=RADIO_CHOICES,
help_text='Check as many as you like.',
)
select3 = forms.ChoiceField(choices=MEDIA_CHOICES)
select4 = forms.MultipleChoiceField(
choices=MEDIA_CHOICES,
help_text='Check as many as you like.',
)
category1 = forms.ChoiceField(
choices=RADIO_CHOICES, widget=forms.RadioSelect)
category2 = forms.MultipleChoiceField(
choices=RADIO_CHOICES,
widget=forms.CheckboxSelectMultiple,
help_text='Check as many as you like.',
)
category3 = forms.ChoiceField(
widget=forms.RadioSelect, choices=MEDIA_CHOICES)
category4 = forms.MultipleChoiceField(
choices=MEDIA_CHOICES,
widget=forms.CheckboxSelectMultiple,
help_text='Check as many as you like.',
)
addon = forms.CharField(
widget=forms.TextInput(attrs={'addon_before': 'before', 'addon_after': 'after'}),
)
required_css_class = 'bootstrap3-req'
def clean(self):
cleaned_data = super(TestForm, self).clean()
raise forms.ValidationError(
"This error was added to show the non field errors styling.")
return cleaned_data
class TestFormWithoutRequiredClass(TestForm):
required_css_class = ''
def render_template(text, **context_args):
"""
Create a template ``text`` that first loads bootstrap3.
"""
template = Template("{% load bootstrap3 %}" + text)
if 'form' not in context_args:
context_args['form'] = TestForm()
return template.render(Context(context_args))
def render_formset(formset=None, **context_args):
"""
Create a template that renders a formset
"""
context_args['formset'] = formset
return render_template('{% bootstrap_formset formset %}', **context_args)
def render_form(form=None, **context_args):
"""
Create a template that renders a form
"""
if form:
context_args['form'] = form
return render_template('{% bootstrap_form form %}', **context_args)
def render_form_field(field, **context_args):
"""
Create a template that renders a field
"""
form_field = 'form.%s' % field
return render_template(
'{% bootstrap_field ' + form_field + ' %}', **context_args)
def render_field(field, **context_args):
"""
Create a template that renders a field
"""
context_args['field'] = field
return render_template('{% bootstrap_field field %}', **context_args)
class SettingsTest(TestCase):
def test_settings(self):
from .bootstrap import BOOTSTRAP3
self.assertTrue(BOOTSTRAP3)
def test_settings_filter(self):
res = render_template(
'{% load bootstrap3 %}' +
'{{ "required_css_class"|bootstrap_setting }}')
self.assertEqual(res.strip(), 'bootstrap3-req')
res = render_template(
'{% load bootstrap3 %}' +
'{% if "javascript_in_head"|bootstrap_setting %}' +
'head{% else %}body{% endif %}'
)
self.assertEqual(res.strip(), 'head')
def test_required_class(self):
form = TestForm()
res = render_template('{% bootstrap_form form %}', form=form)
self.assertIn('bootstrap3-req', res)
def test_error_class(self):
form = TestForm({})
res = render_template('{% bootstrap_form form %}', form=form)
self.assertIn('bootstrap3-err', res)
def test_bound_class(self):
form = TestForm({'sender': 'sender'})
res = render_template('{% bootstrap_form form %}', form=form)
self.assertIn('bootstrap3-bound', res)
class TemplateTest(TestCase):
def test_empty_template(self):
res = render_template('')
self.assertEqual(res.strip(), '')
def test_text_template(self):
res = render_template('some text')
self.assertEqual(res.strip(), 'some text')
def test_bootstrap_template(self):
template = Template((
'{% extends "bootstrap3/bootstrap3.html" %}' +
'{% block bootstrap3_content %}' +
'test_bootstrap3_content' +
'{% endblock %}'
))
res = template.render(Context({}))
self.assertIn('test_bootstrap3_content', res)
def test_javascript_without_jquery(self):
res = render_template('{% bootstrap_javascript jquery=0 %}')
self.assertIn('bootstrap', res)
self.assertNotIn('jquery', res)
def test_javascript_with_jquery(self):
res = render_template('{% bootstrap_javascript jquery=1 %}')
self.assertIn('bootstrap', res)
self.assertIn('jquery', res)
class FormSetTest(TestCase):
def test_illegal_formset(self):
with self.assertRaises(BootstrapError):
render_formset(formset='illegal')
class FormTest(TestCase):
def test_illegal_form(self):
with self.assertRaises(BootstrapError):
render_form(form='illegal')
def test_field_names(self):
form = TestForm()
res = render_form(form)
for field in form:
self.assertIn('name="%s"' % field.name, res)
def test_field_addons(self):
form = TestForm()
res = render_form(form)
self.assertIn('<div class="input-group"><span class="input-group-addon">before</span><input', res)
self.assertIn('/><span class="input-group-addon">after</span></div>', res)
def test_exclude(self):
form = TestForm()
res = render_template(
'{% bootstrap_form form exclude="cc_myself" %}', form=form)
self.assertNotIn('cc_myself', res)
def test_layout_horizontal(self):
form = TestForm()
res = render_template(
'{% bootstrap_form form layout="horizontal" %}', form=form)
self.assertIn('col-md-2', res)
self.assertIn('col-md-4', res)
res = render_template(
'{% bootstrap_form form layout="horizontal" ' +
'horizontal_label_class="hlabel" ' +
'horizontal_field_class="hfield" %}',
form=form
)
self.assertIn('hlabel', res)
self.assertIn('hfield', res)
def test_buttons_tag(self):
form = TestForm()
res = render_template(
'{% buttons layout="horizontal" %}{% endbuttons %}', form=form)
self.assertIn('col-md-2', res)
self.assertIn('col-md-4', res)
class FieldTest(TestCase):
def test_illegal_field(self):
with self.assertRaises(BootstrapError):
render_field(field='illegal')
def test_show_help(self):
res = render_form_field('subject')
self.assertIn('my_help_text', res)
self.assertNotIn('<i>my_help_text</i>', res)
res = render_template('{% bootstrap_field form.subject show_help=0 %}')
self.assertNotIn('my_help_text', res)
def test_subject(self):
res = render_form_field('subject')
self.assertIn('type="text"', res)
self.assertIn('placeholder="placeholdertest"', res)
def test_required_field(self):
required_field = render_form_field('subject')
self.assertIn('required', required_field)
self.assertIn('bootstrap3-req', required_field)
not_required_field = render_form_field('message')
self.assertNotIn('required', not_required_field)
# Required field with required=0
form_field = 'form.subject'
rendered = render_template(
'{% bootstrap_field ' + form_field + ' set_required=0 %}')
self.assertNotIn('required', rendered)
# Required settings in field
form_field = 'form.subject'
rendered = render_template(
'{% bootstrap_field ' +
form_field +
' required_css_class="test-required" %}')
self.assertIn('test-required', rendered)
def test_empty_permitted(self):
form = TestForm()
res = render_form_field('subject', form=form)
self.assertIn('required', res)
form.empty_permitted = True
res = render_form_field('subject', form=form)
self.assertNotIn('required', res)
def test_input_group(self):
res = render_template(
'{% bootstrap_field form.subject addon_before="$" ' +
'addon_after=".00" %}'
)
self.assertIn('class="input-group"', res)
self.assertIn('class="input-group-addon">$', res)
self.assertIn('class="input-group-addon">.00', res)
def test_size(self):
def _test_size(param, klass):
res = render_template(
'{% bootstrap_field form.subject size="' + param + '" %}')
self.assertIn(klass, res)
def _test_size_medium(param):
res = render_template(
'{% bootstrap_field form.subject size="' + param + '" %}')
self.assertNotIn('input-lg', res)
self.assertNotIn('input-sm', res)
self.assertNotIn('input-md', res)
_test_size('sm', 'input-sm')
_test_size('small', 'input-sm')
_test_size('lg', 'input-lg')
_test_size('large', 'input-lg')
_test_size_medium('md')
_test_size_medium('medium')
_test_size_medium('')
class ComponentsTest(TestCase):
def test_icon(self):
res = render_template('{% bootstrap_icon "star" %}')
self.assertEqual(
res.strip(), '<span class="glyphicon glyphicon-star"></span>')
res = render_template(
'{% bootstrap_icon "star" title="alpha centauri" %}')
self.assertEqual(
res.strip(),
'<span class="glyphicon glyphicon-star" ' +
'title="alpha centauri"></span>')
def test_alert(self):
res = render_template(
'{% bootstrap_alert "content" alert_type="danger" %}')
self.assertEqual(
res.strip(),
'<div class="alert alert-danger alert-dismissable">' +
'<button type="button" class="close" data-dismiss="alert" ' +
'aria-hidden="true">' +
'&times;</button>content</div>'
)
class MessagesTest(TestCase):
def test_messages(self):
class FakeMessage(object):
"""
Follows the `django.contrib.messages.storage.base.Message` API.
"""
def __init__(self, message, tags):
self.tags = tags
self.message = message
def __str__(self):
return self.message
pattern = re.compile(r'\s+')
messages = [FakeMessage("hello", "warning")]
res = render_template(
'{% bootstrap_messages messages %}', messages=messages)
expected = """
<div class="alert alert-warning alert-dismissable">
<button type="button" class="close" data-dismiss="alert"
aria-hidden="true">&#215;</button>
hello
</div>
"""
self.assertEqual(
re.sub(pattern, '', res),
re.sub(pattern, '', expected)
)
messages = [FakeMessage("hello", "error")]
res = render_template(
'{% bootstrap_messages messages %}', messages=messages)
expected = """
<div class="alert alert-danger alert-dismissable">
<button type="button" class="close" data-dismiss="alert"
aria-hidden="true">&#215;</button>
hello
</div>
"""
self.assertEqual(
re.sub(pattern, '', res),
re.sub(pattern, '', expected)
)
messages = [FakeMessage("hello", None)]
res = render_template(
'{% bootstrap_messages messages %}', messages=messages)
expected = """
<div class="alert alert-dismissable">
<button type="button" class="close" data-dismiss="alert"
aria-hidden="true">&#215;</button>
hello
</div>
"""
self.assertEqual(
re.sub(pattern, '', res),
re.sub(pattern, '', expected)
)
class TextTest(TestCase):
def test_add_css_class(self):
css_classes = "one two"
css_class = "three four"
classes = add_css_class(css_classes, css_class)
self.assertEqual(classes, "one two three four")
classes = add_css_class(css_classes, css_class, prepend=True)
self.assertEqual(classes, "three four one two")
class HtmlTest(TestCase):
def test_text_value(self):
self.assertEqual(text_value(''), "")
self.assertEqual(text_value(' '), " ")
self.assertEqual(text_value(None), "")
self.assertEqual(text_value(1), "1")
def test_text_concat(self):
self.assertEqual(text_concat(1, 2), "12")
self.assertEqual(text_concat(1, 2, separator='='), "1=2")
self.assertEqual(text_concat(None, 2, separator='='), "2")
class ButtonTest(TestCase):
def test_button(self):
res = render_template(
"{% bootstrap_button 'button' size='lg' %}")
self.assertEqual(
res.strip(), '<button class="btn btn-lg">button</button>')
res = render_template(
"{% bootstrap_button 'button' size='lg' href='#' %}")
self.assertIn(
res.strip(),
'<a class="btn btn-lg" href="#">button</a><a href="#" ' +
'class="btn btn-lg">button</a>')

26
bootstrap3/text.py Normal file
View file

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
try:
from django.utils.encoding import force_text
except ImportError:
from django.utils.encoding import force_unicode as force_text
def text_value(value):
"""
Force a value to text, render None as an empty string
"""
if value is None:
return ''
return force_text(value)
def text_concat(*args, **kwargs):
"""
Concatenate several values as a text string with an optional separator
"""
separator = text_value(kwargs.get('separator', ''))
values = filter(None, [text_value(v) for v in args])
return separator.join(values)

65
bootstrap3/utils.py Normal file
View file

@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.forms.widgets import flatatt
from .text import text_value
# Handle HTML and CSS manipulation
def split_css_classes(css_classes):
"""
Turn string into a list of CSS classes
"""
classes_list = text_value(css_classes).split(' ')
return [c for c in classes_list if c]
def add_css_class(css_classes, css_class, prepend=False):
"""
Add a CSS class to a string of CSS classes
"""
classes_list = split_css_classes(css_classes)
classes_to_add = [c for c in split_css_classes(css_class)
if c not in classes_list]
if prepend:
classes_list = classes_to_add + classes_list
else:
classes_list += classes_to_add
return ' '.join(classes_list)
def remove_css_class(css_classes, css_class):
"""
Remove a CSS class from a string of CSS classes
"""
remove = set(split_css_classes(css_class))
classes_list = [c for c in split_css_classes(css_classes)
if c not in remove]
return ' '.join(classes_list)
def render_link_tag(url, rel='stylesheet', media='all'):
"""
Build a link tag
"""
return render_tag(
'link',
attrs={'href': url, 'rel': rel, 'media': media},
close=False)
def render_tag(tag, attrs=None, content=None, close=True):
"""
Render a HTML tag
"""
builder = '<{tag}{attrs}>{content}'
if content or close:
builder += '</{tag}>'
return builder.format(
tag=tag,
attrs=flatatt(attrs) if attrs else '',
content=text_value(content),
)

View file

@ -1,5 +1,6 @@
from django import template
from django.template.loader import render_to_string
from django.conf import settings
from ietf.community.models import CommunityList
from ietf.group.models import Role
@ -7,47 +8,24 @@ from ietf.group.models import Role
register = template.Library()
class CommunityListNode(template.Node):
def __init__(self, user, var_name):
self.user = user
self.var_name = var_name
def render(self, context):
user = self.user.resolve(context)
if not (user and hasattr(user, "is_authenticated") and user.is_authenticated() ):
return ''
lists = {'personal': CommunityList.objects.get_or_create(user=user)[0]}
try:
person = user.person
groups = []
managed_areas = [i.group for i in Role.objects.filter(name__slug='ad', email__in=person.email_set.all())]
for area in managed_areas:
groups.append(CommunityList.objects.get_or_create(group=area)[0])
managed_wg = [i.group for i in Role.objects.filter(name__slug='chair', group__type__slug='wg', email__in=person.email_set.all())]
for wg in managed_wg:
groups.append(CommunityList.objects.get_or_create(group=wg)[0])
lists['group'] = groups
except:
pass
context.update({self.var_name: lists})
@register.assignment_tag
def get_user_managed_lists(user):
if not (user and hasattr(user, "is_authenticated") and user.is_authenticated()):
return ''
@register.tag
def get_user_managed_lists(parser, token):
firstbits = token.contents.split(None, 2)
if len(firstbits) != 3:
raise template.TemplateSyntaxError("'get_user_managed_lists' tag takes three arguments")
user = parser.compile_filter(firstbits[1])
lastbits_reversed = firstbits[2][::-1].split(None, 2)
if lastbits_reversed[1][::-1] != 'as':
raise template.TemplateSyntaxError("next-to-last argument to 'get_user_managed_lists' tag must"
" be 'as'")
var_name = lastbits_reversed[0][::-1]
return CommunityListNode(user, var_name)
lists = {'personal': CommunityList.objects.get_or_create(user=user)[0]}
try:
person = user.person
groups = []
managed_areas = [i.group for i in Role.objects.filter(name__slug='ad', email__in=person.email_set.all())]
for area in managed_areas:
groups.append(CommunityList.objects.get_or_create(group=area)[0])
managed_wg = [i.group for i in Role.objects.filter(name__slug='chair', group__type__slug='wg', email__in=person.email_set.all())]
for wg in managed_wg:
groups.append(CommunityList.objects.get_or_create(group=wg)[0])
lists['group'] = groups
except:
pass
return lists
@register.inclusion_tag('community/display_field.html', takes_context=False)
def show_field(field, doc):
@ -56,25 +34,11 @@ def show_field(field, doc):
}
class CommunityListViewNode(template.Node):
def __init__(self, clist):
self.clist = clist
def render(self, context):
clist = self.clist.resolve(context)
if not clist.cached:
@register.simple_tag
def get_clist_view(clist):
if settings.DEBUG or not clist.cached:
clist.cached = render_to_string('community/raw_view.html',
{'cl': clist,
'dc': clist.get_display_config()})
clist.save()
return clist.cached
@register.tag
def get_clist_view(parser, token):
firstbits = token.contents.split(None, 1)
if len(firstbits) != 2:
raise template.TemplateSyntaxError("'get_clist_view' tag takes one argument")
clist = parser.compile_filter(firstbits[1])
return CommunityListViewNode(clist)

View file

@ -1,4 +1,6 @@
{% extends "base.html" %}
{% extends "ietf.html" %}
{% load bootstrap3 %}
{% block content %}
<h1>Template: {{ template }}</h1>
@ -12,15 +14,15 @@
<dt>Template type</dt>
<dd>{{ template.type.name }}
{% if template.type.slug == "rst" %}
<p>This template uses the syntax of reStructuredText. Get a quick reference at <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">http://docutils.sourceforge.net/docs/user/rst/quickref.html</a>.</p>
<p>You can do variable interpolation with $varialbe if the template allows any variable.</p>
<p class="help-block">This template uses the syntax of reStructuredText. Get a quick reference at <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">http://docutils.sourceforge.net/docs/user/rst/quickref.html</a>.</p>
<p class="help-block">You can do variable interpolation with $varialbe if the template allows any variable.</p>
{% endif %}
{% if template.type.slug == "django" %}
<p>This template uses the syntax of the default django template framework. Get more info at <a href="https://docs.djangoproject.com/en/dev/topics/templates/">https://docs.djangoproject.com/en/dev/topics/templates/</a>.</p>
<p>You can do variable interpolation with the current django markup &#123;&#123;variable&#125;&#125; if the template allows any variable.</p>
<p class="help-block">This template uses the syntax of the default django template framework. Get more info at <a href="https://docs.djangoproject.com/en/dev/topics/templates/">https://docs.djangoproject.com/en/dev/topics/templates/</a>.</p>
<p class="help-block">You can do variable interpolation with the current django markup &#123;&#123;variable&#125;&#125; if the template allows any variable.</p>
{% endif %}
{% if template.type.slug == "plain" %}
<p>This template uses plain text, so no markup is used. You can do variable interpolation with $variable if the template allows any variable.</p>
<p class="help-block">This template uses plain text, so no markup is used. You can do variable interpolation with $variable if the template allows any variable.</p>
{% endif %}
</dd>
{% if template.variables %}
@ -30,8 +32,13 @@
</dl>
<h2>Edit template content</h2>
<form action="" method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Submit changes" />
<form role="form" method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button class="btn btn-default" type="submit">Save changes</button>
{% endbuttons %}
</form>
{% endblock content %}

View file

@ -1,4 +1,4 @@
{% extends "base.html" %}
{% extends "ietf.html" %}
{% block content %}
<h1>Defined templates for group {{ group }}</h1>
@ -6,7 +6,7 @@
{% if template_list %}
<ul>
{% for template in template_list %}
<li><a href="{% url template_edit group.acronym template.id %}">{{ template }}</a></li>
<li><a href="{% url "template_edit" group.acronym template.id %}">{{ template }}</a></li>
{% endfor %}
</ul>
{% else %}

View file

@ -1,6 +1,5 @@
from django.http import HttpResponseForbidden, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render_to_response
from django.template import RequestContext
from django.shortcuts import get_object_or_404, render
from ietf.dbtemplate.models import DBTemplate
from ietf.dbtemplate.forms import DBTemplateForm
@ -15,10 +14,10 @@ def template_list(request, acronym):
return HttpResponseForbidden("You are not authorized to access this view")
template_list = DBTemplate.objects.filter(group=group)
return render_to_response('dbtemplate/template_list.html',
return render(request, 'dbtemplate/template_list.html',
{'template_list': template_list,
'group': group,
}, RequestContext(request))
})
def template_edit(request, acronym, template_id, base_template='dbtemplate/template_edit.html', formclass=DBTemplateForm, extra_context=None):
@ -43,4 +42,4 @@ def template_edit(request, acronym, template_id, base_template='dbtemplate/templ
'form': form,
}
context.update(extra_context)
return render_to_response(base_template, context, RequestContext(request))
return render(request, base_template, context)

113
ietf/doc/fields.py Normal file
View file

@ -0,0 +1,113 @@
import json
from django.utils.html import escape
from django import forms
from django.core.urlresolvers import reverse as urlreverse
from ietf.doc.models import Document, DocAlias
from ietf.doc.utils import uppercase_std_abbreviated_name
def select2_id_doc_name_json(objs):
return json.dumps([{ "id": o.pk, "text": escape(uppercase_std_abbreviated_name(o.name)) } for o in objs])
# FIXME: select2 version 4 uses a standard select for the AJAX case -
# switching to that would allow us to derive from the standard
# multi-select machinery in Django instead of the manual CharField
# stuff below
class SearchableDocumentsField(forms.CharField):
"""Server-based multi-select field for choosing documents using
select2.js.
The field uses a comma-separated list of primary keys in a
CharField element as its API with some extra attributes used by
the Javascript part."""
def __init__(self,
max_entries=None, # max number of selected objs
model=Document,
hint_text="Type in name to search for document",
doc_type="draft",
*args, **kwargs):
kwargs["max_length"] = 10000
self.max_entries = max_entries
self.doc_type = doc_type
self.model = model
super(SearchableDocumentsField, self).__init__(*args, **kwargs)
self.widget.attrs["class"] = "select2-field form-control"
self.widget.attrs["data-placeholder"] = hint_text
if self.max_entries != None:
self.widget.attrs["data-max-entries"] = self.max_entries
def parse_select2_value(self, value):
return [x.strip() for x in value.split(",") if x.strip()]
def prepare_value(self, value):
if not value:
value = ""
if isinstance(value, (int, long)):
value = str(value)
if isinstance(value, basestring):
pks = self.parse_select2_value(value)
value = self.model.objects.filter(pk__in=pks)
filter_args = {}
if self.model == DocAlias:
filter_args["document__type"] = self.doc_type
else:
filter_args["type"] = self.doc_type
value = value.filter(**filter_args)
if isinstance(value, self.model):
value = [value]
self.widget.attrs["data-pre"] = select2_id_doc_name_json(value)
# doing this in the constructor is difficult because the URL
# patterns may not have been fully constructed there yet
self.widget.attrs["data-ajax-url"] = urlreverse("ajax_select2_search_docs", kwargs={
"doc_type": self.doc_type,
"model_name": self.model.__name__.lower()
})
return u",".join(unicode(o.pk) for o in value)
def clean(self, value):
value = super(SearchableDocumentsField, self).clean(value)
pks = self.parse_select2_value(value)
objs = self.model.objects.filter(pk__in=pks)
found_pks = [str(o.pk) for o in objs]
failed_pks = [x for x in pks if x not in found_pks]
if failed_pks:
raise forms.ValidationError(u"Could not recognize the following documents: {pks}. You can only input documents already registered in the Datatracker.".format(pks=", ".join(failed_pks)))
if self.max_entries != None and len(objs) > self.max_entries:
raise forms.ValidationError(u"You can select at most %s entries only." % self.max_entries)
return objs
class SearchableDocumentField(SearchableDocumentsField):
"""Specialized to only return one Document."""
def __init__(self, model=Document, *args, **kwargs):
kwargs["max_entries"] = 1
super(SearchableDocumentField, self).__init__(model=model, *args, **kwargs)
def clean(self, value):
return super(SearchableDocumentField, self).clean(value).first()
class SearchableDocAliasesField(SearchableDocumentsField):
def __init__(self, model=DocAlias, *args, **kwargs):
super(SearchableDocAliasesField, self).__init__(model=model, *args, **kwargs)
class SearchableDocAliasField(SearchableDocumentsField):
"""Specialized to only return one DocAlias."""
def __init__(self, model=DocAlias, *args, **kwargs):
kwargs["max_entries"] = 1
super(SearchableDocAliasField, self).__init__(model=model, *args, **kwargs)
def clean(self, value):
return super(SearchableDocAliasField, self).clean(value).first()

View file

@ -34,7 +34,7 @@ class AdForm(forms.Form):
self.fields['ad'].choices = list(choices) + [("", "-------"), (ad_pk, Person.objects.get(pk=ad_pk).plain_name())]
class NotifyForm(forms.Form):
notify = forms.CharField(max_length=255, help_text="List of email addresses to receive state notifications, separated by comma", label="Notification list", required=False)
notify = forms.CharField(max_length=255, help_text="List of email addresses to receive state notifications, separated by comma.", label="Notification list", required=False)
def clean_notify(self):
addrspecs = [x.strip() for x in self.cleaned_data["notify"].split(',')]

View file

@ -87,35 +87,32 @@ def ballot_icon(context, doc):
positions = list(doc.active_ballot().active_ad_positions().items())
positions.sort(key=sort_key)
edit_position_url = ""
if has_role(user, "Area Director"):
edit_position_url = urlreverse('ietf.doc.views_ballot.edit_position', kwargs=dict(name=doc.name, ballot_id=ballot.pk))
title = "IESG positions (click to show more%s)" % (", right-click to edit position" if edit_position_url else "")
res = ['<a href="%s" data-popup="%s" data-edit="%s" title="%s" class="ballot-icon"><table>' % (
urlreverse("doc_ballot", kwargs=dict(name=doc.name, ballot_id=ballot.pk)),
res = ['<a href="%s" data-toggle="modal" data-target="#modal-%d" title="IESG positions (click to show more)" class="ballot-icon"><table>' % (
urlreverse("ietf.doc.views_doc.ballot_popup", kwargs=dict(name=doc.name, ballot_id=ballot.pk)),
edit_position_url,
title
)]
ballot.pk)]
res.append("<tr>")
for i, (ad, pos) in enumerate(positions):
if i > 0 and i % 5 == 0:
res.append("</tr>")
res.append("<tr>")
res.append("</tr><tr>")
c = "position-%s" % (pos.pos.slug if pos else "norecord")
if user_is_person(user, ad):
c += " my"
res.append('<td class="%s" />' % c)
res.append('<td class="%s"></td>' % c)
res.append("</tr>")
res.append("</table></a>")
# add sufficient table calls to last row to avoid HTML validation warning
while (i + 1) % 5 != 0:
res.append('<td class="empty"></td>')
i = i + 1
res.append("</tr></table></a>")
# XXX FACELIFT: Loading via href will go away in bootstrap 4.
# See http://getbootstrap.com/javascript/#modals-usage
res.append('<div id="modal-%d" class="modal fade" tabindex="-1" role="dialog" aria-hidden="true"><div class="modal-dialog modal-lg"><div class="modal-content"></div></div></div>' % ballot.pk)
return "".join(res)
@ -156,7 +153,7 @@ def state_age_colored(doc):
except IndexError:
state_date = datetime.date(1990,1,1)
days = (datetime.date.today() - state_date).days
# loosely based on
# loosely based on
# http://trac.tools.ietf.org/group/iesg/trac/wiki/PublishPath
if iesg_state == "lc":
goal1 = 30
@ -180,16 +177,17 @@ def state_age_colored(doc):
goal1 = 14
goal2 = 28
if days > goal2:
class_name = "ietf-small ietf-highlight-r"
class_name = "label label-danger"
elif days > goal1:
class_name = "ietf-small ietf-highlight-y"
class_name = "label label-warning"
else:
class_name = "ietf-small"
if days > goal1:
title = ' title="Goal is &lt;%d days"' % (goal1,)
else:
title = ''
return mark_safe('<span class="%s"%s>(for&nbsp;%d&nbsp;day%s)</span>' % (
class_name, title, days, 's' if days != 1 else ''))
return mark_safe('<span class="%s"%s>for %d day%s</span>' % (
class_name, title, days,
's' if days != 1 else ''))
else:
return ""

View file

@ -8,9 +8,9 @@ from email.utils import parseaddr
from ietf.doc.models import ConsensusDocEvent
from django import template
from django.conf import settings
from django.utils.html import escape, fix_ampersands
from django.utils.text import wrap
from django.template.defaultfilters import truncatewords_html, linebreaksbr, stringfilter, urlize
from django.template.defaultfilters import truncatewords_html, linebreaksbr, stringfilter, striptags, urlize
from django.template import resolve_variable
from django.utils.safestring import mark_safe, SafeData
from django.utils.html import strip_tags
@ -52,12 +52,12 @@ def parse_email_list(value):
u'<a href="mailto:joe@example.org">joe@example.org</a>, <a href="mailto:fred@example.com">fred@example.com</a>'
Parsing a non-string should return the input value, rather than fail:
>>> parse_email_list(['joe@example.org', 'fred@example.com'])
['joe@example.org', 'fred@example.com']
Null input values should pass through silently:
>>> parse_email_list('')
''
@ -91,7 +91,7 @@ def fix_angle_quotes(value):
if "<" in value:
value = re.sub("<([\w\-\.]+@[\w\-\.]+)>", "&lt;\1&gt;", value)
return value
# there's an "ahref -> a href" in GEN_UTIL
# but let's wait until we understand what that's for.
@register.filter(name='make_one_per_line')
@ -103,7 +103,7 @@ def make_one_per_line(value):
'a\\nb\\nc'
Pass through non-strings:
>>> make_one_per_line([1, 2])
[1, 2]
@ -114,7 +114,7 @@ def make_one_per_line(value):
return re.sub(", ?", "\n", value)
else:
return value
@register.filter(name='timesum')
def timesum(value):
"""
@ -203,7 +203,7 @@ def rfcspace(string):
"""
string = str(string)
if string[:3].lower() == "rfc" and string[3] != " ":
return string[:3] + " " + string[3:]
return string[:3].upper() + " " + string[3:]
else:
return string
@ -226,7 +226,7 @@ def rfclink(string):
URL for that RFC.
"""
string = str(string);
return "http://tools.ietf.org/html/rfc" + string;
return "//tools.ietf.org/html/rfc" + string;
@register.filter(name='urlize_ietf_docs', is_safe=True, needs_autoescape=True)
def urlize_ietf_docs(string, autoescape=None):
@ -278,7 +278,7 @@ def truncate_ellipsis(text, arg):
return escape(text[:num-1])+"&hellip;"
else:
return escape(text)
@register.filter
def split(text, splitter=None):
return text.split(splitter)
@ -379,7 +379,7 @@ def linebreaks_lf(text):
@register.filter(name='clean_whitespace')
def clean_whitespace(text):
"""
Map all ASCII control characters (0x00-0x1F) to spaces, and
Map all ASCII control characters (0x00-0x1F) to spaces, and
remove unnecessary spaces.
"""
text = re.sub("[\000-\040]+", " ", text)
@ -388,7 +388,7 @@ def clean_whitespace(text):
@register.filter(name='unescape')
def unescape(text):
"""
Unescape &nbsp;/&gt;/&lt;
Unescape &nbsp;/&gt;/&lt;
"""
text = text.replace("&gt;", ">")
text = text.replace("&lt;", "<")
@ -429,7 +429,7 @@ def has_role(user, role_names):
@register.filter
def stable_dictsort(value, arg):
"""
Like dictsort, except it's stable (preserves the order of items
Like dictsort, except it's stable (preserves the order of items
whose sort key is the same). See also bug report
http://code.djangoproject.com/ticket/12110
"""
@ -461,21 +461,14 @@ def format_snippet(text, trunc_words=25):
full = mark_safe(keep_spacing(collapsebr(linebreaksbr(urlize(sanitize_html(text))))))
snippet = truncatewords_html(full, trunc_words)
if snippet != full:
return mark_safe(u'<div class="snippet">%s<span class="show-all">[show all]</span></div><div style="display:none" class="full">%s</div>' % (snippet, full))
return mark_safe(u'<div class="snippet">%s<button class="btn btn-xs btn-default show-all"><span class="fa fa-caret-down"></span></button></div><div class="hidden full">%s</div>' % (snippet, full))
return full
@register.filter
def format_editable_snippet(text,link):
full = mark_safe(keep_spacing(collapsebr(linebreaksbr(urlize(sanitize_html(text))))))
snippet = truncatewords_html(full, 25)
if snippet != full:
return mark_safe(u'<div class="snippet">%s<span class="show-all">[show all]</span></div><div style="display:none" class="full">%s' % (format_editable(snippet,link),format_editable(full,link)) )
else:
return format_editable(full,link)
@register.filter
def format_editable(text,link):
return mark_safe(u'<a class="editlink" href="%s">%s</a>' % (link,text))
@register.simple_tag
def doc_edit_button(url_name, *args, **kwargs):
"""Given URL name/args/kwargs, looks up the URL just like "url" tag and returns a properly formatted button for the document material tables."""
from django.core.urlresolvers import reverse as urlreverse
return mark_safe(u'<a class="btn btn-default btn-xs" href="%s">Edit</a>' % (urlreverse(url_name, args=args, kwargs=kwargs)))
@register.filter
def textify(text):
@ -535,33 +528,60 @@ def consensus(doc):
else:
return "Unknown"
# The function and class below provides a tag version of the builtin wordwrap filter
# https://djangosnippets.org/snippets/134/
@register.tag
def wordwrap(parser, token):
"""
This is a tag version of the Django builtin 'wordwrap' filter. This is useful
if you need to wrap a combination of fixed template text and template variables.
@register.filter
def pos_to_label(text):
"""Return a valid Bootstrap3 label type for a ballot position."""
return {
'Yes': 'success',
'No Objection': 'info',
'Abstain': 'warning',
'Discuss': 'danger',
'Block': 'danger',
'Recuse': 'default',
}.get(str(text), 'blank')
Usage:
{% wordwrap 80 %}
some really long text here, including template variable expansion, etc.
{% endwordwrap %}
@register.filter
def capfirst_allcaps(text):
"""Like capfirst, except it doesn't lowercase words in ALL CAPS."""
result = text
i = False
for token in re.split("(\W+)", striptags(text)):
if not re.match("^[A-Z]+$", token):
if not i:
result = result.replace(token, token.capitalize())
i = True
else:
result = result.replace(token, token.lower())
return result
@register.filter
def lower_allcaps(text):
"""Like lower, except it doesn't lowercase words in ALL CAPS."""
result = text
for token in re.split("(\W+)", striptags(text)):
if not re.match("^[A-Z]+$", token):
result = result.replace(token, token.lower())
return result
# See https://djangosnippets.org/snippets/2072/ and
# https://stackoverflow.com/questions/9939248/how-to-prevent-django-basic-inlines-from-autoescaping
@register.filter
def urlize_html(html, autoescape=False):
"""
Returns urls found in an (X)HTML text node element as urls via Django urlize filter.
"""
try:
tag_name, len = token.split_contents()
except ValueError:
raise template.TemplateSyntaxError, "The wordwrap tag requires exactly one argument: width."
nodelist = parser.parse(('endwordwrap',))
parser.delete_first_token()
return WordWrapNode(nodelist, len)
from BeautifulSoup import BeautifulSoup
except ImportError:
if settings.DEBUG:
raise template.TemplateSyntaxError, "Error in urlize_html The Python BeautifulSoup libraries aren't installed."
return html
else:
soup = BeautifulSoup(html)
class WordWrapNode(template.Node):
def __init__(self, nodelist, len):
self.nodelist = nodelist
self.len = len
def render(self, context):
return wrap(str(self.nodelist.render(context)), int(self.len))
textNodes = soup.findAll(text=True)
for textNode in textNodes:
urlizedText = urlize(textNode, autoescape=autoescape)
textNode.replaceWith(BeautifulSoup(urlizedText))
return str(soup)

View file

@ -44,23 +44,27 @@ area_short_names = {
}
@register.simple_tag
def wg_menu():
res = cache.get('base_left_wgmenu')
def wg_menu(flavor=""):
res = cache.get('wgmenu' + flavor)
if res:
return res
areas = Group.objects.filter(type="area", state="active").order_by('acronym')
groups = Group.objects.filter(type="wg", state="active", parent__in=areas).order_by("acronym")
wgs = Group.objects.filter(type="wg", state="active", parent__in=areas).order_by("acronym")
rgs = Group.objects.filter(type="rg", state="active").order_by("acronym")
for a in areas:
a.short_area_name = area_short_names.get(a.acronym) or a.name
if a.short_area_name.endswith(" Area"):
a.short_area_name = a.short_area_name[:-len(" Area")]
a.active_groups = [g for g in groups if g.parent_id == a.id]
a.active_groups = [g for g in wgs if g.parent_id == a.id]
areas = [a for a in areas if a.active_groups]
res = render_to_string('base/wg_menu.html', {'areas':areas})
cache.set('base_left_wgmenu', res, 30*60)
if flavor == "modal":
res = render_to_string('base/menu_wg_modal.html', {'areas':areas, 'rgs':rgs})
else:
res = render_to_string('base/menu_wg.html', {'areas':areas, 'rgs':rgs})
cache.set('wgmenu' + flavor, res, 30*60)
return res

View file

@ -1,4 +1,5 @@
import datetime
import json
import sys
if sys.version_info[0] == 2 and sys.version_info[1] < 7:
import unittest2 as unittest
@ -97,7 +98,7 @@ class SearchTestCase(TestCase):
make_test_data()
r = self.client.get("/")
self.assertEqual(r.status_code, 200)
self.assertTrue("Search Internet-Drafts" in r.content)
self.assertTrue("Search Documents" in r.content)
def test_drafts_pages(self):
draft = make_test_data()
@ -121,6 +122,32 @@ class SearchTestCase(TestCase):
r = self.client.get(urlreverse("index_active_drafts"))
self.assertEqual(r.status_code, 200)
self.assertTrue(draft.title in r.content)
def test_ajax_search_docs(self):
draft = make_test_data()
# Document
url = urlreverse("ajax_select2_search_docs", kwargs={
"model_name": "document",
"doc_type": "draft",
})
r = self.client.get(url, dict(q=draft.name))
self.assertEqual(r.status_code, 200)
data = json.loads(r.content)
self.assertEqual(data[0]["id"], draft.pk)
# DocAlias
doc_alias = draft.docalias_set.get()
url = urlreverse("ajax_select2_search_docs", kwargs={
"model_name": "docalias",
"doc_type": "draft",
})
r = self.client.get(url, dict(q=doc_alias.name))
self.assertEqual(r.status_code, 200)
data = json.loads(r.content)
self.assertEqual(data[0]["id"], doc_alias.pk)
class DocTestCase(TestCase):
@ -402,14 +429,12 @@ class DocTestCase(TestCase):
self.client.login(username='iab-chair', password='iab-chair+password')
r = self.client.get(urlreverse("doc_view", kwargs=dict(name=doc.name)))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertFalse(q('.actions'))
self.assertTrue("Request publication" not in r.content)
Document.objects.filter(pk=doc.pk).update(stream='iab')
r = self.client.get(urlreverse("doc_view", kwargs=dict(name=doc.name)))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue('IESG state' in q('.actions').html())
self.assertTrue("Request publication" in r.content)
class AddCommentTestCase(TestCase):

View file

@ -174,9 +174,9 @@ class BallotWriteupsTests(TestCase):
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('textarea[name=last_call_text]')), 1)
self.assertEqual(len(q('input[type=submit][value*="Save Last Call"]')), 1)
# we're secretariat, so we got The Link
self.assertEqual(len(q('a:contains("Make Last Call")')), 1)
self.assertTrue(q('[type=submit]:contains("Save")'))
# we're Secretariat, so we got The Link
self.assertEqual(len(q('a:contains("Issue last call")')), 1)
# subject error
r = self.client.post(url, dict(
@ -184,7 +184,7 @@ class BallotWriteupsTests(TestCase):
save_last_call_text="1"))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('ul.errorlist')) > 0)
self.assertTrue(len(q('form .has-error')) > 0)
# save
r = self.client.post(url, dict(
@ -243,7 +243,7 @@ class BallotWriteupsTests(TestCase):
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('textarea[name=ballot_writeup]')), 1)
self.assertEqual(len(q('input[type=submit][value*="Save Ballot Writeup"]')), 1)
self.assertTrue(q('[type=submit]:contains("Save")'))
self.assertTrue("IANA does not" in r.content)
# save
@ -317,7 +317,7 @@ class BallotWriteupsTests(TestCase):
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('textarea[name=approval_text]')), 1)
self.assertEqual(len(q('input[type=submit][value*="Save Approval"]')), 1)
self.assertTrue(q('[type=submit]:contains("Save")'))
# save
r = self.client.post(url, dict(
@ -365,8 +365,8 @@ class ApproveBallotTests(TestCase):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue("send out the announcement" in q('.actions input[type=submit]')[0].get('value').lower())
self.assertEqual(len(q('.announcement pre:contains("Subject: Protocol Action")')), 1)
self.assertTrue(q('[type=submit]:contains("send announcement")'))
self.assertEqual(len(q('form pre:contains("Subject: Protocol Action")')), 1)
# approve
mailbox_before = len(outbox)
@ -466,7 +466,7 @@ class DeferUndeferTestCase(TestCase):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('form.defer')),1)
self.assertEqual(len(q('[type=submit]:contains("Defer ballot")')),1)
# defer
mailbox_before = len(outbox)
@ -521,7 +521,7 @@ class DeferUndeferTestCase(TestCase):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('form.undefer')),1)
self.assertEqual(len(q('[type=submit]:contains("Undefer ballot")')),1)
# undefer
mailbox_before = len(outbox)

View file

@ -71,7 +71,7 @@ class EditCharterTests(TestCase):
r = self.client.post(url, dict(charter_state="-12345"))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
self.assertTrue(len(q('form .has-error')) > 0)
self.assertEqual(charter.get_state(), first_state)
# change state
@ -370,18 +370,16 @@ class EditCharterTests(TestCase):
desc="Has been copied",
due=due_date,
resolved="")
# m2 isn't used -- missing test?
m2 = GroupMilestone.objects.create(group=group, # pyflakes:ignore
state_id="active",
desc="To be deleted",
due=due_date,
resolved="")
# m3 isn't used -- missing test?
m3 = GroupMilestone.objects.create(group=group, # pyflakes:ignore
state_id="charter",
desc="Has been copied",
due=due_date,
resolved="")
GroupMilestone.objects.create(group=group,
state_id="active",
desc="To be deleted",
due=due_date,
resolved="")
GroupMilestone.objects.create(group=group,
state_id="charter",
desc="Has been copied",
due=due_date,
resolved="")
m4 = GroupMilestone.objects.create(group=group,
state_id="charter",
desc="New charter milestone",
@ -392,7 +390,7 @@ class EditCharterTests(TestCase):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue("Send out the announcement" in q('input[type=submit]')[0].get('value'))
self.assertTrue(q('[type=submit]:contains("Send announcement")'))
self.assertEqual(len(q('pre')), 1)
# approve

View file

@ -45,13 +45,13 @@ class ConflictReviewTests(TestCase):
r = self.client.post(url,dict(create_in_state=""))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
self.assertTrue(len(q('form .has-error')) > 0)
self.assertEqual(Document.objects.filter(name='conflict-review-imaginary-independent-submission').count() , 0)
r = self.client.post(url,dict(ad=""))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
self.assertTrue(len(q('form .has-error')) > 0)
self.assertEqual(Document.objects.filter(name='conflict-review-imaginary-independent-submission').count() , 0)
# successful review start
@ -139,7 +139,7 @@ class ConflictReviewTests(TestCase):
r = self.client.post(url,dict(review_state=""))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
self.assertTrue(len(q('form .has-error')) > 0)
# successful change to AD Review
adrev_pk = str(State.objects.get(used=True, slug='adrev',type__slug='conflrev').pk)
@ -274,7 +274,7 @@ class ConflictReviewTests(TestCase):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('form.approve')),1)
self.assertEqual(len(q('[type=submit]:contains("Send announcement")')), 1)
if approve_type == 'appr-noprob':
self.assertTrue( 'IESG has no problem' in ''.join(wrap(r.content,2**16)))
else:

View file

@ -33,7 +33,7 @@ class ChangeStateTests(TestCase):
login_testing_unauthorized(self, "secretary", url)
first_state = draft.get_state("draft-iesg")
next_states = first_state.next_states
next_states = first_state.next_states.all()
# normal get
r = self.client.get(url)
@ -42,14 +42,14 @@ class ChangeStateTests(TestCase):
self.assertEqual(len(q('form select[name=state]')), 1)
if next_states:
self.assertTrue(len(q('.next-states form input[type=hidden]')) > 0)
self.assertEqual(len(q('[type=submit][value="%s"]' % next_states[0].name)), 1)
# faulty post
r = self.client.post(url, dict(state=State.objects.get(used=True, type="draft", slug="active").pk))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
self.assertTrue(len(q('form .has-error')) > 0)
draft = Document.objects.get(name=draft.name)
self.assertEqual(draft.get_state("draft-iesg"), first_state)
@ -81,7 +81,7 @@ class ChangeStateTests(TestCase):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('.prev-state form input[name="state"]')), 1)
self.assertEqual(len(q('form [type=submit][value="%s"]' % first_state.name)), 1)
def test_pull_from_rfc_queue(self):
draft = make_test_data()
@ -127,7 +127,7 @@ class ChangeStateTests(TestCase):
r = self.client.post(url, dict(state="foobarbaz"))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
self.assertTrue(len(q('form .has-error')) > 0)
draft = Document.objects.get(name=draft.name)
self.assertEqual(draft.get_state("draft-iana-review"), first_state)
@ -149,7 +149,7 @@ class ChangeStateTests(TestCase):
self.assertTrue(not draft.latest_event(type="changed_ballot_writeup_text"))
r = self.client.post(url, dict(state=State.objects.get(used=True, type="draft-iesg", slug="lc-req").pk))
self.assertContains(r, "Your request to issue the Last Call")
self.assertTrue("Your request to issue" in r.content)
# last call text
e = draft.latest_event(WriteupDocEvent, type="changed_last_call_text")
@ -195,7 +195,7 @@ class EditInfoTests(TestCase):
r = self.client.post(url, dict(ad="123456789"))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
self.assertTrue(len(q('form .has-error')) > 0)
draft = Document.objects.get(name=draft.name)
self.assertEqual(draft.ad, prev_ad)
@ -689,7 +689,7 @@ class IndividualInfoFormsTests(TestCase):
r = self.client.get(url)
self.assertEqual(r.status_code,200)
q = PyQuery(r.content)
self.assertEqual(len(q('form.change-stream')),1)
self.assertEqual(len(q('[type=submit]:contains("Save")')), 1)
# shift to ISE stream
messages_before = len(outbox)
@ -742,13 +742,13 @@ class IndividualInfoFormsTests(TestCase):
r = self.client.get(url)
self.assertEqual(r.status_code,200)
q = PyQuery(r.content)
self.assertEqual(len(q('form.change-intended-status')),1)
self.assertEqual(len(q('[type=submit]:contains("Save")')), 1)
# don't allow status level to be cleared
r = self.client.post(url,dict(intended_std_level=""))
self.assertEqual(r.status_code,200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
self.assertTrue(len(q('form .has-error')) > 0)
# change intended status level
messages_before = len(outbox)
@ -768,7 +768,7 @@ class IndividualInfoFormsTests(TestCase):
r = self.client.get(url)
self.assertEqual(r.status_code,200)
q = PyQuery(r.content)
self.assertEqual(len(q('form.telechat-date')),1)
self.assertEqual(len(q('[type=submit]:contains("Save")')), 1)
# set a date
self.assertFalse(self.doc.latest_event(TelechatDocEvent, "scheduled_for_telechat"))
@ -791,7 +791,7 @@ class IndividualInfoFormsTests(TestCase):
r = self.client.get(url)
self.assertEqual(r.status_code,200)
q = PyQuery(r.content)
self.assertEqual(len(q('form.edit-iesg-note')),1)
self.assertEqual(len(q('[type=submit]:contains("Save")')),1)
# post
r = self.client.post(url,dict(note='ZpyQFGmA\r\nZpyQFGmA'))
@ -868,7 +868,7 @@ class IndividualInfoFormsTests(TestCase):
r = self.client.post(url, dict(shepherd=two_answers))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
self.assertTrue(len(q('form .has-error')) > 0)
def test_doc_change_shepherd_email(self):
self.doc.shepherd = None
@ -914,15 +914,16 @@ class IndividualInfoFormsTests(TestCase):
r = self.client.get(url)
self.assertEqual(r.status_code,200)
q = PyQuery(r.content)
self.assertEqual(len(q('span[id=doc_edit_shepherd_writeup]')),1)
self.assertEqual(len(q('.content-wrapper a:contains("Edit")')), 1)
# Try again when no longer a shepherd.
self.doc.shepherd = None
self.doc.save()
r = self.client.get(url)
self.assertEqual(r.status_code,200)
q = PyQuery(r.content)
self.assertEqual(len(q('span[id=doc_edit_shepherd_writeup]')),1)
self.assertEqual(len(q('.content-wrapper a:contains("Edit")')), 0)
def test_doc_change_shepherd_writeup(self):
url = urlreverse('doc_edit_shepherd_writeup',kwargs=dict(name=self.docname))
@ -1037,7 +1038,7 @@ class RequestPublicationTests(TestCase):
q = PyQuery(r.content)
subject = q('input#id_subject')[0].get("value")
self.assertTrue("Document Action" in subject)
body = q('.request-publication #id_body').text()
body = q('#id_body').text()
self.assertTrue("Informational" in body)
self.assertTrue("IAB" in body)
@ -1246,38 +1247,34 @@ class ChangeReplacesTests(TestCase):
# normal get
r = self.client.get(url)
self.assertEquals(r.status_code, 200)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEquals(len(q('form[class=change-replaces]')), 1)
self.assertEqual(len(q('[type=submit]:contains("Save")')), 1)
# Post that says replacea replaces base a
self.assertEquals(self.basea.get_state().slug,'active')
repljson='{"%d":"%s"}'%(DocAlias.objects.get(name=self.basea.name).id,self.basea.name)
r = self.client.post(url, dict(replaces=repljson))
self.assertEquals(r.status_code, 302)
self.assertEqual(self.basea.get_state().slug,'active')
r = self.client.post(url, dict(replaces=str(DocAlias.objects.get(name=self.basea.name).id)))
self.assertEqual(r.status_code, 302)
self.assertEqual(RelatedDocument.objects.filter(relationship__slug='replaces',source=self.replacea).count(),1)
self.assertEquals(Document.objects.get(name='draft-test-base-a').get_state().slug,'repl')
self.assertEqual(Document.objects.get(name='draft-test-base-a').get_state().slug,'repl')
# Post that says replaceboth replaces both base a and base b
url = urlreverse('doc_change_replaces', kwargs=dict(name=self.replaceboth.name))
self.assertEquals(self.baseb.get_state().slug,'expired')
repljson='{"%d":"%s","%d":"%s"}'%(DocAlias.objects.get(name=self.basea.name).id,self.basea.name,
DocAlias.objects.get(name=self.baseb.name).id,self.baseb.name)
r = self.client.post(url, dict(replaces=repljson))
self.assertEquals(r.status_code, 302)
self.assertEquals(Document.objects.get(name='draft-test-base-a').get_state().slug,'repl')
self.assertEquals(Document.objects.get(name='draft-test-base-b').get_state().slug,'repl')
self.assertEqual(self.baseb.get_state().slug,'expired')
r = self.client.post(url, dict(replaces=str(DocAlias.objects.get(name=self.basea.name).id) + "," + str(DocAlias.objects.get(name=self.baseb.name).id)))
self.assertEqual(r.status_code, 302)
self.assertEqual(Document.objects.get(name='draft-test-base-a').get_state().slug,'repl')
self.assertEqual(Document.objects.get(name='draft-test-base-b').get_state().slug,'repl')
# Post that undoes replaceboth
repljson='{}'
r = self.client.post(url, dict(replaces=repljson))
self.assertEquals(r.status_code, 302)
self.assertEquals(Document.objects.get(name='draft-test-base-a').get_state().slug,'repl') # Because A is still also replaced by replacea
self.assertEquals(Document.objects.get(name='draft-test-base-b').get_state().slug,'expired')
r = self.client.post(url, dict(replaces=""))
self.assertEqual(r.status_code, 302)
self.assertEqual(Document.objects.get(name='draft-test-base-a').get_state().slug,'repl') # Because A is still also replaced by replacea
self.assertEqual(Document.objects.get(name='draft-test-base-b').get_state().slug,'expired')
# Post that undoes replacea
url = urlreverse('doc_change_replaces', kwargs=dict(name=self.replacea.name))
r = self.client.post(url, dict(replaces=repljson))
self.assertEquals(r.status_code, 302)
self.assertEquals(Document.objects.get(name='draft-test-base-a').get_state().slug,'active')
r = self.client.post(url, dict(replaces=""))
self.assertEqual(r.status_code, 302)
self.assertEqual(Document.objects.get(name='draft-test-base-a').get_state().slug,'active')

View file

@ -60,7 +60,7 @@ class GroupMaterialTests(TestCase):
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
self.assertTrue(len(q('.has-error')) > 0)
test_file.seek(0)
@ -88,7 +88,7 @@ class GroupMaterialTests(TestCase):
state=State.objects.get(type="slides", slug="active").pk,
material=test_file))
self.assertEqual(r.status_code, 200)
self.assertTrue(len(q('form ul.errorlist')) > 0)
self.assertTrue(len(q('.has-error')) > 0)
def test_change_state(self):
doc = self.create_slides()

View file

@ -41,25 +41,25 @@ class StatusChangeTests(TestCase):
r = self.client.post(url,dict(document_name="bogus",title="Bogus Title",ad="",create_in_state=state_strpk,notify='ipu@ietf.org'))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
self.assertTrue(len(q('form .has-error')) > 0)
## Must set a name
r = self.client.post(url,dict(document_name="",title="Bogus Title",ad=ad_strpk,create_in_state=state_strpk,notify='ipu@ietf.org'))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
self.assertTrue(len(q('form .has-error')) > 0)
## Must not choose a document name that already exists
r = self.client.post(url,dict(document_name="imaginary-mid-review",title="Bogus Title",ad=ad_strpk,create_in_state=state_strpk,notify='ipu@ietf.org'))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
self.assertTrue(len(q('form .has-error')) > 0)
## Must set a title
r = self.client.post(url,dict(document_name="bogus",title="",ad=ad_strpk,create_in_state=state_strpk,notify='ipu@ietf.org'))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
self.assertTrue(len(q('form .has-error')) > 0)
# successful status change start
r = self.client.post(url,dict(document_name="imaginary-new",title="A new imaginary status change",ad=ad_strpk,
@ -90,7 +90,7 @@ class StatusChangeTests(TestCase):
r = self.client.post(url,dict(new_state=""))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
self.assertTrue(len(q('form .has-error')) > 0)
# successful change to AD Review
adrev_pk = str(State.objects.get(slug='adrev',type__slug='statchg').pk)
@ -283,7 +283,7 @@ class StatusChangeTests(TestCase):
messages_before = len(outbox)
r = self.client.post(url,dict(last_call_text='stuff',send_last_call_request='Save+and+Request+Last+Call'))
self.assertEqual(r.status_code,200)
self.assertTrue( 'Last Call Requested' in ''.join(wrap(r.content,2**16)))
self.assertTrue( 'Last call requested' in ''.join(wrap(r.content,2**16)))
self.assertEqual(len(outbox), messages_before + 1)
self.assertTrue('iesg-secretary' in outbox[-1]['To'])
self.assertTrue('Last Call:' in outbox[-1]['Subject'])
@ -307,7 +307,7 @@ class StatusChangeTests(TestCase):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('form.approve')),1)
self.assertEqual(len(q('[type=submit]:contains("Send announcement")')), 1)
# There should be two messages to edit
self.assertEqual(q('input#id_form-TOTAL_FORMS').val(),'2')
self.assertTrue( '(rfc9999) to Internet Standard' in ''.join(wrap(r.content,2**16)))
@ -345,30 +345,27 @@ class StatusChangeTests(TestCase):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('form.edit-status-change-rfcs')),1)
self.assertEqual(len(q('.content-wrapper [type=submit]:contains("Save")')),1)
# There should be three rows on the form
self.assertEqual(len(q('tr[id^=relation_row]')),3)
self.assertEqual(len(q('.content-wrapper .row')),3)
# Try to add a relation to an RFC that doesn't exist
r = self.client.post(url,dict(new_relation_row_blah="rfc9997",
statchg_relation_row_blah="tois",
Submit="Submit"))
statchg_relation_row_blah="tois"))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
# Try to add a relation leaving the relation type blank
r = self.client.post(url,dict(new_relation_row_blah="rfc9999",
statchg_relation_row_blah="",
Submit="Submit"))
statchg_relation_row_blah=""))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
# Try to add a relation with an unknown relationship type
r = self.client.post(url,dict(new_relation_row_blah="rfc9999",
statchg_relation_row_blah="badslug",
Submit="Submit"))
statchg_relation_row_blah="badslug"))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
@ -379,8 +376,7 @@ class StatusChangeTests(TestCase):
new_relation_row_foo="rfc9998",
statchg_relation_row_foo="tobcp",
new_relation_row_nob="rfc14",
statchg_relation_row_nob="tohist",
Submit="Submit"))
statchg_relation_row_nob="tohist"))
self.assertEqual(r.status_code, 302)
doc = Document.objects.get(name='status-change-imaginary-mid-review')
self.assertEqual(doc.relateddocument_set.count(),3)

View file

@ -49,6 +49,7 @@ urlpatterns = patterns('',
url(r'^all/$', views_search.index_all_drafts, name="index_all_drafts"),
url(r'^active/$', views_search.index_active_drafts, name="index_active_drafts"),
url(r'^select2search/(?P<model_name>(document|docalias))/(?P<doc_type>draft)/$', views_search.ajax_select2_search_docs, name="ajax_select2_search_docs"),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/(?:(?P<rev>[0-9-]+)/)?$', views_doc.document_main, name="doc_view"),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/history/$', views_doc.document_history, name="doc_history"),

View file

@ -519,3 +519,9 @@ def get_initial_notify(doc,extra=None):
receivers.extend(extra)
return ", ".join(set([x.strip() for x in receivers]))
def uppercase_std_abbreviated_name(name):
if re.match('(rfc|bcp|std|fyi) ?[0-9]+$', name):
return name.upper()
else:
return name

View file

@ -116,7 +116,7 @@ def edit_position(request, name, ballot_id):
if has_role(request.user, "Secretariat"):
ad_id = request.GET.get('ad')
if not ad_id:
raise Http404()
raise Http404
ad = get_object_or_404(Person, pk=ad_id)
old_pos = doc.latest_event(BallotPositionDocEvent, type="changed_ballot_position", ad=ad, ballot=ballot)
@ -251,12 +251,12 @@ def send_ballot_comment(request, name, ballot_id):
if not has_role(request.user, "Area Director"):
ad_id = request.GET.get('ad')
if not ad_id:
raise Http404()
raise Http404
ad = get_object_or_404(Person, pk=ad_id)
pos = doc.latest_event(BallotPositionDocEvent, type="changed_ballot_position", ad=ad, ballot=ballot)
if not pos:
raise Http404()
raise Http404
subj = []
d = ""
@ -330,11 +330,11 @@ def defer_ballot(request, name):
"""Signal post-pone of ballot, notifying relevant parties."""
doc = get_object_or_404(Document, docalias__name=name)
if doc.type_id not in ('draft','conflrev','statchg'):
raise Http404()
raise Http404
interesting_state = dict(draft='draft-iesg',conflrev='conflrev',statchg='statchg')
state = doc.get_state(interesting_state[doc.type_id])
if not state or state.slug=='defer' or not doc.telechat_date():
raise Http404()
raise Http404
login = request.user.person
telechat_date = TelechatDate.objects.active().order_by("date")[1].date
@ -380,13 +380,13 @@ def undefer_ballot(request, name):
"""undo deferral of ballot ballot."""
doc = get_object_or_404(Document, docalias__name=name)
if doc.type_id not in ('draft','conflrev','statchg'):
raise Http404()
raise Http404
if doc.type_id == 'draft' and not doc.get_state("draft-iesg"):
raise Http404()
raise Http404
interesting_state = dict(draft='draft-iesg',conflrev='conflrev',statchg='statchg')
state = doc.get_state(interesting_state[doc.type_id])
if not state or state.slug!='defer':
raise Http404()
raise Http404
telechat_date = TelechatDate.objects.active().order_by("date")[0].date
@ -417,7 +417,7 @@ def lastcalltext(request, name):
"""Editing of the last call text"""
doc = get_object_or_404(Document, docalias__name=name)
if not doc.get_state("draft-iesg"):
raise Http404()
raise Http404
login = request.user.person
@ -581,7 +581,7 @@ def ballot_approvaltext(request, name):
"""Editing of approval text"""
doc = get_object_or_404(Document, docalias__name=name)
if not doc.get_state("draft-iesg"):
raise Http404()
raise Http404
login = request.user.person
@ -629,7 +629,7 @@ def approve_ballot(request, name):
"""Approve ballot, sending out announcement, changing state."""
doc = get_object_or_404(Document, docalias__name=name)
if not doc.get_state("draft-iesg"):
raise Http404()
raise Http404
login = request.user.person

View file

@ -13,7 +13,7 @@ from django.contrib.auth.decorators import login_required
import debug # pyflakes:ignore
from ietf.doc.models import ( Document, DocHistory, State, DocEvent, BallotDocEvent,
BallotPositionDocEvent, InitialReviewDocEvent, NewRevisionDocEvent,
BallotPositionDocEvent, InitialReviewDocEvent, NewRevisionDocEvent,
WriteupDocEvent, save_document_in_history )
from ietf.doc.utils import ( add_state_change_event, close_open_ballots,
create_ballot_if_not_open, get_chartering_type )
@ -33,8 +33,8 @@ from ietf.group.mails import email_iesg_secretary_re_charter
class ChangeStateForm(forms.Form):
charter_state = forms.ModelChoiceField(State.objects.filter(used=True, type="charter"), label="Charter state", empty_label=None, required=False)
initial_time = forms.IntegerField(initial=0, label="Review time", help_text="(in weeks)", required=False)
message = forms.CharField(widget=forms.Textarea, help_text="Leave blank to change state without notifying the Secretariat", required=False, label=mark_safe("Message to<br> Secretariat"))
comment = forms.CharField(widget=forms.Textarea, help_text="Optional comment for the charter history", required=False)
message = forms.CharField(widget=forms.Textarea, help_text="Leave blank to change state without notifying the Secretariat.", required=False, label=mark_safe("Message to the Secretariat"))
comment = forms.CharField(widget=forms.Textarea, help_text="Optional comment for the charter history.", required=False)
def __init__(self, *args, **kwargs):
self.hide = kwargs.pop('hide', None)
group = kwargs.pop('group')
@ -47,7 +47,7 @@ class ChangeStateForm(forms.Form):
# hide requested fields
if self.hide:
for f in self.hide:
self.fields[f].widget = forms.HiddenInput
self.fields[f].widget = forms.HiddenInput()
@login_required
def change_state(request, name, option=None):
@ -101,7 +101,7 @@ def change_state(request, name, option=None):
e.state_id = group.state.slug
e.desc = "Group state changed to %s from %s" % (group.state, oldstate)
e.save()
else:
charter_state = State.objects.get(used=True, type="charter", slug="approved")
charter_rev = approved_revision(charter.rev)
@ -225,9 +225,9 @@ def change_state(request, name, option=None):
context_instance=RequestContext(request))
class ChangeTitleForm(forms.Form):
charter_title = forms.CharField(widget=forms.TextInput, label="Charter title", help_text="Enter new charter title", required=True)
message = forms.CharField(widget=forms.Textarea, help_text="Leave blank to change the title without notifying the Secretariat", required=False, label=mark_safe("Message to<br> Secretariat"))
comment = forms.CharField(widget=forms.Textarea, help_text="Optional comment for the charter history", required=False)
charter_title = forms.CharField(widget=forms.TextInput, label="Charter title", help_text="Enter new charter title.", required=True)
message = forms.CharField(widget=forms.Textarea, help_text="Leave blank to change the title without notifying the Secretariat.", required=False, label=mark_safe("Message to Secretariat"))
comment = forms.CharField(widget=forms.Textarea, help_text="Optional comment for the charter history.", required=False)
def __init__(self, *args, **kwargs):
charter = kwargs.pop('charter')
super(ChangeTitleForm, self).__init__(*args, **kwargs)
@ -328,8 +328,8 @@ def edit_ad(request, name):
class UploadForm(forms.Form):
content = forms.CharField(widget=forms.Textarea, label="Charter text", help_text="Edit the charter text", required=False)
txt = forms.FileField(label=".txt format", help_text="Or upload a .txt file", required=False)
content = forms.CharField(widget=forms.Textarea, label="Charter text", help_text="Edit the charter text.", required=False)
txt = forms.FileField(label=".txt format", help_text="Or upload a .txt file.", required=False)
def clean_content(self):
return self.cleaned_data["content"].replace("\r", "")
@ -382,7 +382,7 @@ def submit(request, name=None, option=None):
e.desc = "New version available: <b>%s-%s.txt</b>" % (charter.canonical_name(), charter.rev)
e.rev = charter.rev
e.save()
# Save file on disk
form.save(group, charter.rev)
@ -460,7 +460,7 @@ def announcement_text(request, name, ann):
e.desc = "%s %s text was changed" % (group.type.name, ann)
e.text = t
e.save()
charter.time = e.time
charter.save()
@ -495,7 +495,7 @@ class BallotWriteupForm(forms.Form):
def clean_ballot_writeup(self):
return self.cleaned_data["ballot_writeup"].replace("\r", "")
@role_required('Area Director','Secretariat')
def ballot_writeupnotes(request, name):
"""Editing of ballot write-up and notes"""
@ -508,13 +508,13 @@ def ballot_writeupnotes(request, name):
login = request.user.person
approval = charter.latest_event(WriteupDocEvent, type="changed_action_announcement")
existing = charter.latest_event(WriteupDocEvent, type="changed_ballot_writeup_text")
if not existing:
existing = generate_ballot_writeup(request, charter)
reissue = charter.latest_event(DocEvent, type="sent_ballot_announcement")
form = BallotWriteupForm(initial=dict(ballot_writeup=existing.text))
if request.method == 'POST' and ("save_ballot_writeup" in request.POST or "send_ballot" in request.POST):
@ -699,7 +699,7 @@ def approve(request, name):
send_mail_preformatted(request, announcement)
return HttpResponseRedirect(charter.get_absolute_url())
return render_to_response('doc/charter/approve.html',
dict(charter=charter,
announcement=announcement),

View file

@ -23,7 +23,7 @@ from ietf.utils.textupload import get_cleaned_text_file_content
class ChangeStateForm(forms.Form):
review_state = forms.ModelChoiceField(State.objects.filter(used=True, type="conflrev"), label="Conflict review state", empty_label=None, required=True)
comment = forms.CharField(widget=forms.Textarea, help_text="Optional comment for the review history", required=False)
comment = forms.CharField(widget=forms.Textarea, help_text="Optional comment for the review history.", required=False)
@role_required("Area Director", "Secretariat")
def change_state(request, name, option=None):
@ -114,8 +114,8 @@ def send_conflict_eval_email(request,review):
msg)
class UploadForm(forms.Form):
content = forms.CharField(widget=forms.Textarea, label="Conflict review response", help_text="Edit the conflict review response", required=False)
txt = forms.FileField(label=".txt format", help_text="Or upload a .txt file", required=False)
content = forms.CharField(widget=forms.Textarea, label="Conflict review response", help_text="Edit the conflict review response.", required=False)
txt = forms.FileField(label=".txt format", help_text="Or upload a .txt file.", required=False)
def clean_content(self):
return self.cleaned_data["content"].replace("\r", "")
@ -265,7 +265,7 @@ def default_approval_text(review):
class AnnouncementForm(forms.Form):
announcement_text = forms.CharField(widget=forms.Textarea, label="IETF Conflict Review Announcement", help_text="Edit the announcement message", required=True)
announcement_text = forms.CharField(widget=forms.Textarea, label="IETF Conflict Review Announcement", help_text="Edit the announcement message.", required=True)
@role_required("Secretariat")
def approve(request, name):
@ -324,13 +324,13 @@ def approve(request, name):
context_instance=RequestContext(request))
class SimpleStartReviewForm(forms.Form):
notify = forms.CharField(max_length=255, label="Notice emails", help_text="Separate email addresses with commas", required=False)
notify = forms.CharField(max_length=255, label="Notice emails", help_text="Separate email addresses with commas.", required=False)
class StartReviewForm(forms.Form):
ad = forms.ModelChoiceField(Person.objects.filter(role__name="ad", role__group__state="active",role__group__type='area').order_by('name'),
label="Shepherding AD", empty_label="(None)", required=True)
create_in_state = forms.ModelChoiceField(State.objects.filter(used=True, type="conflrev", slug__in=("needshep", "adrev")), empty_label=None, required=False)
notify = forms.CharField(max_length=255, label="Notice emails", help_text="Separate email addresses with commas", required=False)
notify = forms.CharField(max_length=255, label="Notice emails", help_text="Separate email addresses with commas.", required=False)
telechat_date = forms.TypedChoiceField(coerce=lambda x: datetime.datetime.strptime(x, '%Y-%m-%d').date(), empty_value=None, required=False, widget=forms.Select(attrs={'onchange':'make_bold()'}))
def __init__(self, *args, **kwargs):

View file

@ -33,7 +33,7 @@
import os, datetime, urllib, json, glob
from django.http import HttpResponse, Http404 , HttpResponseForbidden
from django.shortcuts import render_to_response, get_object_or_404, redirect
from django.shortcuts import render_to_response, get_object_or_404, redirect, render
from django.template import RequestContext
from django.template.loader import render_to_string
from django.core.exceptions import ObjectDoesNotExist
@ -869,20 +869,17 @@ def telechat_date(request, name):
e = doc.latest_event(TelechatDocEvent, type="scheduled_for_telechat")
initial_returning_item = bool(e and e.returning_item)
prompts = []
warnings = []
if e and e.telechat_date and doc.type.slug != 'charter':
if e.telechat_date==datetime.date.today():
prompts.append( "This document is currently scheduled for today's telechat. "
+"Please set the returning item bit carefully.")
warnings.append( "This document is currently scheduled for today's telechat. "
+"Please set the returning item bit carefully.")
elif e.telechat_date<datetime.date.today() and has_same_ballot(doc,e.telechat_date):
initial_returning_item = True
prompts.append( "This document appears to have been on a previous telechat with the same ballot, "
warnings.append( "This document appears to have been on a previous telechat with the same ballot, "
+"so the returning item bit has been set. Clear it if that is not appropriate.")
else:
pass
initial = dict(telechat_date=e.telechat_date if e else None,
returning_item = initial_returning_item,
)
@ -901,13 +898,12 @@ def telechat_date(request, name):
if doc.type.slug=='charter':
del form.fields['returning_item']
return render_to_response('doc/edit_telechat_date.html',
return render(request, 'doc/edit_telechat_date.html',
dict(doc=doc,
form=form,
user=request.user,
prompts=prompts,
login=login),
context_instance=RequestContext(request))
warnings=warnings,
login=login))
def edit_notify(request, name):
"""Change the set of email addresses document change notificaitions go to."""

View file

@ -1,6 +1,6 @@
# changing state and metadata on Internet Drafts
import datetime, json
import datetime
from django import forms
from django.http import HttpResponseRedirect, HttpResponseForbidden, Http404
@ -24,13 +24,14 @@ from ietf.doc.utils import ( add_state_change_event, can_adopt_draft,
get_tags_for_stream_id, nice_consensus,
update_reminder, update_telechat, make_notify_changed_event, get_initial_notify )
from ietf.doc.lastcall import request_last_call
from ietf.doc.fields import SearchableDocAliasesField
from ietf.group.models import Group, Role
from ietf.iesg.models import TelechatDate
from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream, user_is_person
from ietf.ietfauth.utils import role_required
from ietf.message.models import Message
from ietf.name.models import IntendedStdLevelName, DocTagName, StreamName
from ietf.person.fields import AutocompletedEmailField
from ietf.person.fields import SearchableEmailField
from ietf.person.models import Person, Email
from ietf.secr.lib.template import jsonapi
from ietf.utils.mail import send_mail, send_mail_message
@ -64,7 +65,7 @@ def change_state(request, name):
and logging the change as a comment."""
doc = get_object_or_404(Document, docalias__name=name)
if (not doc.latest_event(type="started_iesg_process")) or doc.get_state_slug() == "expired":
raise Http404()
raise Http404
login = request.user.person
@ -214,7 +215,7 @@ def change_stream(request, name):
and logging the change as a comment."""
doc = get_object_or_404(Document, docalias__name=name)
if not doc.type_id=='draft':
raise Http404()
raise Http404
if not (has_role(request.user, ("Area Director", "Secretariat")) or
(request.user.is_authenticated() and
@ -307,35 +308,21 @@ def collect_email_addresses(emails, doc):
return emails
class ReplacesForm(forms.Form):
replaces = forms.CharField(max_length=512,widget=forms.HiddenInput)
replaces = SearchableDocAliasesField(required=False)
comment = forms.CharField(widget=forms.Textarea, required=False)
def __init__(self, *args, **kwargs):
self.doc = kwargs.pop('doc')
super(ReplacesForm, self).__init__(*args, **kwargs)
drafts = {}
for d in self.doc.related_that_doc("replaces"):
drafts[d.id] = d.document.name
self.initial['replaces'] = json.dumps(drafts)
self.initial['replaces'] = self.doc.related_that_doc("replaces")
def clean_replaces(self):
data = self.cleaned_data['replaces'].strip()
if data:
ids = [int(x) for x in json.loads(data)]
else:
return []
objects = []
for id in ids:
try:
d = DocAlias.objects.get(pk=id)
except DocAlias.DoesNotExist:
raise forms.ValidationError("ERROR: %s not found for id %d" % DocAlias._meta.verbos_name, id)
for d in self.cleaned_data['replaces']:
if d.document == self.doc:
raise forms.ValidationError("ERROR: A draft can't replace itself")
raise forms.ValidationError("A draft can't replace itself")
if d.document.type_id == "draft" and d.document.get_state_slug() == "rfc":
raise forms.ValidationError("ERROR: A draft can't replace an RFC")
objects.append(d)
return objects
raise forms.ValidationError("A draft can't replace an RFC")
return self.cleaned_data['replaces']
def replaces(request, name):
"""Change 'replaces' set of a Document of type 'draft' , notifying parties
@ -472,7 +459,7 @@ class EditInfoForm(forms.Form):
area = forms.ModelChoiceField(Group.objects.filter(type="area", state="active"), empty_label="(None - individual submission)", required=False, label="Assigned to area")
ad = forms.ModelChoiceField(Person.objects.filter(role__name="ad", role__group__state="active",role__group__type='area').order_by('name'), label="Responsible AD", empty_label="(None)", required=True)
create_in_state = forms.ModelChoiceField(State.objects.filter(used=True, type="draft-iesg", slug__in=("pub-req", "watching")), empty_label=None, required=False)
notify = forms.CharField(max_length=255, label="Notice emails", help_text="Separate email addresses with commas", required=False)
notify = forms.CharField(max_length=255, label="Notice emails", help_text="Separate email addresses with commas.", required=False)
note = forms.CharField(widget=forms.Textarea, label="IESG note", required=False)
telechat_date = forms.TypedChoiceField(coerce=lambda x: datetime.datetime.strptime(x, '%Y-%m-%d').date(), empty_value=None, required=False, widget=forms.Select(attrs={'onchange':'make_bold()'}))
returning_item = forms.BooleanField(required=False)
@ -505,10 +492,10 @@ def to_iesg(request,name):
doc = get_object_or_404(Document, docalias__name=name, stream='ietf')
if doc.get_state_slug('draft') == "expired" or doc.get_state_slug('draft-iesg') == 'pub-req' :
raise Http404()
raise Http404
if not is_authorized_in_doc_stream(request.user, doc):
raise Http404()
raise Http404
target_state={
'iesg' : State.objects.get(type='draft-iesg',slug='pub-req'),
@ -614,7 +601,7 @@ def edit_info(request, name):
necessary and logging changes as document events."""
doc = get_object_or_404(Document, docalias__name=name)
if doc.get_state_slug() == "expired":
raise Http404()
raise Http404
login = request.user.person
@ -764,7 +751,7 @@ def request_resurrect(request, name):
"""Request resurrect of expired Internet Draft."""
doc = get_object_or_404(Document, docalias__name=name)
if doc.get_state_slug() != "expired":
raise Http404()
raise Http404
login = request.user.person
@ -788,7 +775,7 @@ def resurrect(request, name):
"""Resurrect expired Internet Draft."""
doc = get_object_or_404(Document, docalias__name=name)
if doc.get_state_slug() != "expired":
raise Http404()
raise Http404
login = request.user.person
@ -863,8 +850,8 @@ def edit_iesg_note(request, name):
context_instance=RequestContext(request))
class ShepherdWriteupUploadForm(forms.Form):
content = forms.CharField(widget=forms.Textarea, label="Shepherd writeup", help_text="Edit the shepherd writeup", required=False)
txt = forms.FileField(label=".txt format", help_text="Or upload a .txt file", required=False)
content = forms.CharField(widget=forms.Textarea, label="Shepherd writeup", help_text="Edit the shepherd writeup.", required=False)
txt = forms.FileField(label=".txt format", help_text="Or upload a .txt file.", required=False)
def clean_content(self):
return self.cleaned_data["content"].replace("\r", "")
@ -942,7 +929,7 @@ def edit_shepherd_writeup(request, name):
context_instance=RequestContext(request))
class ShepherdForm(forms.Form):
shepherd = AutocompletedEmailField(required=False, only_users=True)
shepherd = SearchableEmailField(required=False, only_users=True)
def edit_shepherd(request, name):
"""Change the shepherd for a Document"""
@ -1210,7 +1197,7 @@ def request_publication(request, name):
class AdoptDraftForm(forms.Form):
group = forms.ModelChoiceField(queryset=Group.objects.filter(type__in=["wg", "rg"], state="active").order_by("-type", "acronym"), required=True, empty_label=None)
newstate = forms.ModelChoiceField(queryset=State.objects.filter(type__in=['draft-stream-ietf','draft-stream-irtf'],slug__in=['wg-cand', 'c-adopt', 'adopt-wg', 'info', 'wg-doc', 'candidat','active']),required=True,label="State")
comment = forms.CharField(widget=forms.Textarea, required=False, label="Comment", help_text="Optional comment explaining the reasons for the adoption")
comment = forms.CharField(widget=forms.Textarea, required=False, label="Comment", help_text="Optional comment explaining the reasons for the adoption.")
weeks = forms.IntegerField(required=False, label="Expected weeks in adoption state")
def __init__(self, *args, **kwargs):
@ -1323,7 +1310,7 @@ def adopt_draft(request, name):
class ChangeStreamStateForm(forms.Form):
new_state = forms.ModelChoiceField(queryset=State.objects.filter(used=True), label='State', help_text=u"Only select 'Submitted to IESG for Publication' to correct errors. Use the document's main page to request publication.")
weeks = forms.IntegerField(label='Expected weeks in state',required=False)
comment = forms.CharField(widget=forms.Textarea, required=False, help_text="Optional comment for the document history")
comment = forms.CharField(widget=forms.Textarea, required=False, help_text="Optional comment for the document history.")
tags = forms.ModelMultipleChoiceField(queryset=DocTagName.objects.filter(used=True), widget=forms.CheckboxSelectMultiple, required=False)
def __init__(self, *args, **kwargs):

View file

@ -270,24 +270,23 @@ def edit_material_presentations(request, name, acronym=None, date=None, seq=None
if request.method == 'POST':
form = MaterialVersionForm(request.POST,choices=choices)
if form.is_valid():
if request.POST.get("action", "") == "Save":
new_selection = form.cleaned_data['version']
if initial['version'] != new_selection:
if initial['version'] == 'notpresented':
doc.sessionpresentation_set.create(session=session,rev=new_selection)
c = DocEvent(type="added_comment", doc=doc, by=request.user.person)
c.desc = "Added version %s to session: %s" % (new_selection,session)
c.save()
elif new_selection == 'notpresented':
doc.sessionpresentation_set.filter(session=session).delete()
c = DocEvent(type="added_comment", doc=doc, by=request.user.person)
c.desc = "Removed from session: %s" % (session)
c.save()
else:
doc.sessionpresentation_set.filter(session=session).update(rev=new_selection)
c = DocEvent(type="added_comment", doc=doc, by=request.user.person)
c.desc = "Revision for session %s changed to %s" % (session,new_selection)
c.save()
new_selection = form.cleaned_data['version']
if initial['version'] != new_selection:
if initial['version'] == 'notpresented':
doc.sessionpresentation_set.create(session=session,rev=new_selection)
c = DocEvent(type="added_comment", doc=doc, by=request.user.person)
c.desc = "Added version %s to session: %s" % (new_selection,session)
c.save()
elif new_selection == 'notpresented':
doc.sessionpresentation_set.filter(session=session).delete()
c = DocEvent(type="added_comment", doc=doc, by=request.user.person)
c.desc = "Removed from session: %s" % (session)
c.save()
else:
doc.sessionpresentation_set.filter(session=session).update(rev=new_selection)
c = DocEvent(type="added_comment", doc=doc, by=request.user.person)
c.desc = "Revision for session %s changed to %s" % (session,new_selection)
c.save()
return redirect('doc_view',name=doc.name)
else:
form = MaterialVersionForm(choices=choices,initial=initial)

View file

@ -37,7 +37,7 @@ from django.core.exceptions import ObjectDoesNotExist
from django.shortcuts import render_to_response
from django.db.models import Q
from django.template import RequestContext
from django.http import Http404, HttpResponseBadRequest
from django.http import Http404, HttpResponseBadRequest, HttpResponse
import debug # pyflakes:ignore
@ -45,6 +45,7 @@ from ietf.community.models import CommunityList
from ietf.doc.models import ( Document, DocAlias, State, RelatedDocument, DocEvent,
LastCallDocEvent, TelechatDocEvent, IESG_SUBSTATE_TAGS )
from ietf.doc.expire import expirable_draft
from ietf.doc.fields import select2_id_doc_name_json
from ietf.group.models import Group
from ietf.idindex.index import active_drafts_index_by_group
from ietf.ipr.models import IprDocAlias
@ -52,6 +53,7 @@ from ietf.name.models import DocTagName, DocTypeName, StreamName
from ietf.person.models import Person
from ietf.utils.draft_search import normalize_draftname
class SearchForm(forms.Form):
name = forms.CharField(required=False)
rfcs = forms.BooleanField(required=False, initial=True)
@ -148,7 +150,7 @@ def fill_in_search_attributes(docs):
for d in docs:
if isinstance(d,DocAlias):
d = d.document
rel_this_doc = d.all_related_that_doc(['replaces','obs'])
rel_this_doc = d.all_related_that_doc(['replaces','obs'])
for rel in rel_this_doc:
rel_id_camefrom.setdefault(rel.document.pk,[]).append(d.pk)
rel_docs += [x.document for x in rel_this_doc]
@ -240,7 +242,7 @@ def retrieve_search_results(form, all_types=False):
"""Takes a validated SearchForm and return the results."""
if not form.is_valid():
raise ValueError("SearchForm doesn't validate: %s" % form.errors)
query = form.cleaned_data
types=[];
@ -282,7 +284,7 @@ def retrieve_search_results(form, all_types=False):
if query["olddrafts"]:
allowed_draft_states.extend(['repl', 'expired', 'auth-rm', 'ietf-rm'])
docs = docs.filter(Q(states__slug__in=allowed_draft_states) |
docs = docs.filter(Q(states__slug__in=allowed_draft_states) |
~Q(type__slug='draft')).distinct()
# radio choices
@ -362,11 +364,10 @@ def retrieve_search_results(form, all_types=False):
meta['by'] = query['by']
meta['advanced'] = bool(query['by'] or len(meta['checked']))
meta['headers'] = [{'title': 'Add', 'key':'add'},
{'title': 'Document', 'key':'document'},
meta['headers'] = [{'title': 'Document', 'key':'document'},
{'title': 'Title', 'key':'title'},
{'title': 'Date', 'key':'date'},
{'title': 'Status', 'key':'status', 'colspan':'2'},
{'title': 'Status', 'key':'status'},
{'title': 'IPR', 'key':'ipr'},
{'title': 'AD / Shepherd', 'key':'ad'}]
@ -441,14 +442,14 @@ def ad_dashboard_group(doc):
return '%s Internet-Draft' % doc.get_state('draft').name
elif doc.type.slug=='conflrev':
if doc.get_state_slug('conflrev') in ('appr-reqnopub-sent','appr-noprob-sent'):
return 'Approved Conflict Review'
return 'Approved Conflict Review'
elif doc.get_state_slug('conflrev') in ('appr-reqnopub-pend','appr-noprob-pend','appr-reqnopub-pr','appr-noprob-pr'):
return "%s Conflict Review" % State.objects.get(type__slug='draft-iesg',slug='approved')
else:
return '%s Conflict Review' % doc.get_state('conflrev')
elif doc.type.slug=='statchg':
if doc.get_state_slug('statchg') in ('appr-sent',):
return 'Approved Status Change'
return 'Approved Status Change'
if doc.get_state_slug('statchg') in ('appr-pend','appr-pr'):
return '%s Status Change' % State.objects.get(type__slug='draft-iesg',slug='approved')
else:
@ -462,7 +463,7 @@ def ad_dashboard_group(doc):
return "Document"
def ad_dashboard_sort_key(doc):
if doc.type.slug=='draft' and doc.get_state_slug('draft') == 'rfc':
return "21%04d" % int(doc.rfc_number())
if doc.type.slug=='statchg' and doc.get_state_slug('statchg') == 'appr-sent':
@ -475,26 +476,26 @@ def ad_dashboard_sort_key(doc):
seed = ad_dashboard_group(doc)
if doc.type.slug=='conflrev' and doc.get_state_slug('conflrev') == 'adrev':
state = State.objects.get(type__slug='draft-iesg',slug='ad-eval')
state = State.objects.get(type__slug='draft-iesg',slug='ad-eval')
return "1%d%s" % (state.order,seed)
if doc.type.slug=='charter':
if doc.get_state_slug('charter') in ('notrev','infrev'):
return "100%s" % seed
elif doc.get_state_slug('charter') == 'intrev':
state = State.objects.get(type__slug='draft-iesg',slug='ad-eval')
state = State.objects.get(type__slug='draft-iesg',slug='ad-eval')
return "1%d%s" % (state.order,seed)
elif doc.get_state_slug('charter') == 'extrev':
state = State.objects.get(type__slug='draft-iesg',slug='lc')
state = State.objects.get(type__slug='draft-iesg',slug='lc')
return "1%d%s" % (state.order,seed)
elif doc.get_state_slug('charter') == 'iesgrev':
state = State.objects.get(type__slug='draft-iesg',slug='iesg-eva')
state = State.objects.get(type__slug='draft-iesg',slug='iesg-eva')
return "1%d%s" % (state.order,seed)
if doc.type.slug=='statchg' and doc.get_state_slug('statchg') == 'adrev':
state = State.objects.get(type__slug='draft-iesg',slug='ad-eval')
state = State.objects.get(type__slug='draft-iesg',slug='ad-eval')
return "1%d%s" % (state.order,seed)
if seed.startswith('Needs Shepherd'):
return "100%s" % seed
if seed.endswith(' Document'):
@ -627,3 +628,28 @@ def index_active_drafts(request):
groups = active_drafts_index_by_group()
return render_to_response("doc/index_active_drafts.html", { 'groups': groups }, context_instance=RequestContext(request))
def ajax_select2_search_docs(request, model_name, doc_type):
if model_name == "docalias":
model = DocAlias
else:
model = Document
q = [w.strip() for w in request.GET.get('q', '').split() if w.strip()]
if not q:
objs = model.objects.none()
else:
qs = model.objects.all()
if model == Document:
qs = qs.filter(type=doc_type)
elif model == DocAlias:
qs = qs.filter(document__type=doc_type)
for t in q:
qs = qs.filter(name__icontains=t)
objs = qs.distinct().order_by("name")[:20]
return HttpResponse(select2_id_doc_name_json(objs), content_type='application/json')

View file

@ -25,7 +25,7 @@ from ietf.utils.textupload import get_cleaned_text_file_content
class ChangeStateForm(forms.Form):
new_state = forms.ModelChoiceField(State.objects.filter(type="statchg", used=True), label="Status Change Evaluation State", empty_label=None, required=True)
comment = forms.CharField(widget=forms.Textarea, help_text="Optional comment for the review history", required=False)
comment = forms.CharField(widget=forms.Textarea, help_text="Optional comment for the review history.", required=False)
@role_required("Area Director", "Secretariat")
@ -109,8 +109,8 @@ def send_status_change_eval_email(request,doc):
send_mail_preformatted(request,msg)
class UploadForm(forms.Form):
content = forms.CharField(widget=forms.Textarea, label="Status change text", help_text="Edit the status change text", required=False)
txt = forms.FileField(label=".txt format", help_text="Or upload a .txt file", required=False)
content = forms.CharField(widget=forms.Textarea, label="Status change text", help_text="Edit the status change text.", required=False)
txt = forms.FileField(label=".txt format", help_text="Or upload a .txt file.", required=False)
def clean_content(self):
return self.cleaned_data["content"].replace("\r", "")
@ -306,7 +306,7 @@ def default_approval_text(status_change,relateddoc):
from django.forms.formsets import formset_factory
class AnnouncementForm(forms.Form):
announcement_text = forms.CharField(widget=forms.Textarea, label="Status Change Announcement", help_text="Edit the announcement message", required=True)
announcement_text = forms.CharField(widget=forms.Textarea, label="Status Change Announcement", help_text="Edit the announcement message.", required=True)
label = None
def __init__(self, *args, **kwargs):
@ -399,11 +399,11 @@ def clean_helper(form, formtype):
elif k.startswith('statchg_relation_row'):
status_fields[k[21:]]=v
for key in rfc_fields:
if rfc_fields[key]!="":
if key in status_fields:
new_relations[rfc_fields[key]]=status_fields[key]
else:
new_relations[rfc_fields[key]]=None
if rfc_fields[key]!="":
if key in status_fields:
new_relations[rfc_fields[key]]=status_fields[key]
else:
new_relations[rfc_fields[key]]=None
form.relations = new_relations
@ -436,12 +436,12 @@ class EditStatusChangeForm(forms.Form):
return clean_helper(self,EditStatusChangeForm)
class StartStatusChangeForm(forms.Form):
document_name = forms.CharField(max_length=255, label="Document name", help_text="A descriptive name such as status-change-md2-to-historic is better than status-change-rfc1319", required=True)
document_name = forms.CharField(max_length=255, label="Document name", help_text="A descriptive name such as status-change-md2-to-historic is better than status-change-rfc1319.", required=True)
title = forms.CharField(max_length=255, label="Title", required=True)
ad = forms.ModelChoiceField(Person.objects.filter(role__name="ad", role__group__state="active",role__group__type='area').order_by('name'),
label="Shepherding AD", empty_label="(None)", required=True)
create_in_state = forms.ModelChoiceField(State.objects.filter(type="statchg", slug__in=("needshep", "adrev")), empty_label=None, required=False)
notify = forms.CharField(max_length=255, label="Notice emails", help_text="Separate email addresses with commas", required=False)
notify = forms.CharField(max_length=255, label="Notice emails", help_text="Separate email addresses with commas.", required=False)
telechat_date = forms.TypedChoiceField(coerce=lambda x: datetime.datetime.strptime(x, '%Y-%m-%d').date(), empty_value=None, required=False, widget=forms.Select(attrs={'onchange':'make_bold()'}))
relations={}
@ -568,7 +568,7 @@ def edit_relations(request, name):
if request.method == 'POST':
form = EditStatusChangeForm(request.POST)
if 'Submit' in request.POST and form.is_valid():
if form.is_valid():
old_relations={}
for rel in status_change.relateddocument_set.filter(relationship__slug__in=STATUSCHANGE_RELATIONS):
@ -590,9 +590,6 @@ def edit_relations(request, name):
return HttpResponseRedirect(status_change.get_absolute_url())
elif 'Cancel' in request.POST:
return HttpResponseRedirect(status_change.get_absolute_url())
else:
relations={}
for rel in status_change.relateddocument_set.filter(relationship__slug__in=STATUSCHANGE_RELATIONS):

View file

@ -19,9 +19,10 @@ from ietf.group.models import ( Group, Role, GroupEvent, GroupHistory, GroupStat
from ietf.group.utils import save_group_in_history, can_manage_group_type
from ietf.group.utils import get_group_or_404
from ietf.ietfauth.utils import has_role
from ietf.person.fields import AutocompletedEmailsField
from ietf.person.fields import SearchableEmailsField
from ietf.person.models import Person, Email
from ietf.group.mails import email_iesg_secretary_re_charter, email_iesg_secretary_personnel_change
from ietf.utils.ordereddict import insert_after_in_ordered_dict
MAX_GROUP_DELEGATES = 3
@ -29,21 +30,20 @@ class GroupForm(forms.Form):
name = forms.CharField(max_length=255, label="Name", required=True)
acronym = forms.CharField(max_length=10, label="Acronym", required=True)
state = forms.ModelChoiceField(GroupStateName.objects.all(), label="State", required=True)
chairs = AutocompletedEmailsField(required=False, only_users=True)
secretaries = AutocompletedEmailsField(required=False, only_users=True)
techadv = AutocompletedEmailsField(label="Technical Advisors", required=False, only_users=True)
delegates = AutocompletedEmailsField(required=False, only_users=True, max_entries=MAX_GROUP_DELEGATES,
help_text=mark_safe("Chairs can delegate the authority to update the state of group documents - max %s persons at a given time" % MAX_GROUP_DELEGATES))
chairs = SearchableEmailsField(label="Chairs", required=False, only_users=True)
secretaries = SearchableEmailsField(label="Secretarias", required=False, only_users=True)
techadv = SearchableEmailsField(label="Technical Advisors", required=False, only_users=True)
delegates = SearchableEmailsField(label="Delegates", required=False, only_users=True, max_entries=MAX_GROUP_DELEGATES,
help_text=mark_safe("Chairs can delegate the authority to update the state of group documents - at most %s persons at a given time." % MAX_GROUP_DELEGATES))
ad = forms.ModelChoiceField(Person.objects.filter(role__name="ad", role__group__state="active", role__group__type='area').order_by('name'), label="Shepherding AD", empty_label="(None)", required=False)
parent = forms.ModelChoiceField(Group.objects.filter(state="active").order_by('name'), empty_label="(None)", required=False)
list_email = forms.CharField(max_length=64, required=False)
list_subscribe = forms.CharField(max_length=255, required=False)
list_archive = forms.CharField(max_length=255, required=False)
urls = forms.CharField(widget=forms.Textarea, label="Additional URLs", help_text="Format: http://site/path (Optional description). Separate multiple entries with newline.", required=False)
urls = forms.CharField(widget=forms.Textarea, label="Additional URLs", help_text="Format: https://site/path (Optional description). Separate multiple entries with newline. Prefer HTTPS URLs where possible.", required=False)
def __init__(self, *args, **kwargs):
self.group = kwargs.pop('group', None)
self.confirmed = kwargs.pop('confirmed', False)
self.group_type = kwargs.pop('group_type', False)
super(self.__class__, self).__init__(*args, **kwargs)
@ -57,10 +57,8 @@ class GroupForm(forms.Form):
if ad_pk and ad_pk not in [pk for pk, name in choices]:
self.fields['ad'].choices = list(choices) + [("", "-------"), (ad_pk, Person.objects.get(pk=ad_pk).plain_name())]
self.confirm_msg = ""
self.autoenable_confirm = False
if self.group:
self.fields['acronym'].widget.attrs['readonly'] = True
self.fields['acronym'].widget.attrs['readonly'] = ""
if self.group_type == "rg":
self.fields['ad'].widget = forms.HiddenInput()
@ -71,9 +69,6 @@ class GroupForm(forms.Form):
self.fields['parent'].label = "IETF Area"
def clean_acronym(self):
self.confirm_msg = ""
self.autoenable_confirm = False
# Changing the acronym of an already existing group will cause 404s all
# over the place, loose history, and generally muck up a lot of
# things, so we don't permit it
@ -90,27 +85,41 @@ class GroupForm(forms.Form):
if existing:
existing = existing[0]
if existing and existing.type_id == self.group_type:
if self.confirmed:
return acronym # take over confirmed
confirmed = self.data.get("confirm_acronym", False)
def insert_confirm_field(label, initial):
# set required to false, we don't need it since we do the
# validation of the field in here, and otherwise the
# browser and Django may barf
insert_after_in_ordered_dict(self.fields, "confirm_acronym", forms.BooleanField(label=label, required=False), after="acronym")
# we can't set initial, it's ignored since the form is bound, instead mutate the data
self.data = self.data.copy()
self.data["confirm_acronym"] = initial
if existing and existing.type_id == self.group_type:
if existing.state_id == "bof":
self.confirm_msg = "Turn BoF %s into proposed %s and start chartering it" % (existing.acronym, existing.type.name)
self.autoenable_confirm = True
raise forms.ValidationError("Warning: Acronym used for an existing BoF (%s)." % existing.name)
insert_confirm_field(label="Turn BoF %s into proposed %s and start chartering it" % (existing.acronym, existing.type.name), initial=True)
if confirmed:
return acronym
else:
raise forms.ValidationError("Warning: Acronym used for an existing BoF (%s)." % existing.name)
else:
self.confirm_msg = "Set state of %s %s to proposed and start chartering it" % (existing.acronym, existing.type.name)
self.autoenable_confirm = False
raise forms.ValidationError("Warning: Acronym used for an existing %s (%s, %s)." % (existing.type.name, existing.name, existing.state.name if existing.state else "unknown state"))
insert_confirm_field(label="Set state of %s %s to proposed and start chartering it" % (existing.acronym, existing.type.name), initial=False)
if confirmed:
return acronym
else:
raise forms.ValidationError("Warning: Acronym used for an existing %s (%s, %s)." % (existing.type.name, existing.name, existing.state.name if existing.state else "unknown state"))
if existing:
raise forms.ValidationError("Acronym used for an existing group (%s)." % existing.name)
old = GroupHistory.objects.filter(acronym__iexact=acronym, type__in=("wg", "rg"))
if old and not self.confirmed:
self.confirm_msg = "Confirm reusing acronym %s" % old[0].acronym
self.autoenable_confirm = False
raise forms.ValidationError("Warning: Acronym used for a historic group.")
if old:
insert_confirm_field(label="Confirm reusing acronym %s" % old[0].acronym, initial=False)
if confirmed:
return acronym
else:
raise forms.ValidationError("Warning: Acronym used for a historic group.")
return acronym
@ -149,7 +158,7 @@ def get_or_create_initial_charter(group, group_type):
)
charter.save()
charter.set_state(State.objects.get(used=True, type="charter", slug="notrev"))
# Create an alias as well
DocAlias.objects.create(name=charter.name, document=charter)
@ -190,7 +199,7 @@ def edit(request, group_type=None, acronym=None, action="edit"):
group_type = group.type_id
if request.method == 'POST':
form = GroupForm(request.POST, group=group, confirmed=request.POST.get("confirmed", False), group_type=group_type)
form = GroupForm(request.POST, group=group, group_type=group_type)
if form.is_valid():
clean = form.cleaned_data
if new_group:
@ -220,12 +229,12 @@ def edit(request, group_type=None, acronym=None, action="edit"):
group.charter = get_or_create_initial_charter(group, group_type)
changes = []
def desc(attr, new, old):
entry = "%(attr)s changed to <b>%(new)s</b> from %(old)s"
if new_group:
entry = "%(attr)s changed to <b>%(new)s</b>"
return entry % dict(attr=attr, new=new, old=old)
def diff(attr, name):

View file

@ -305,7 +305,7 @@ def construct_group_menu_context(request, group, selected, group_type, others):
if group.features.has_milestones:
if group.state_id != "proposed" and (is_chair or can_manage):
actions.append((u"Add or edit milestones", urlreverse("group_edit_milestones", kwargs=kwargs)))
actions.append((u"Edit milestones", urlreverse("group_edit_milestones", kwargs=kwargs)))
if group.features.has_materials and can_manage_materials(request.user, group):
actions.append((u"Upload material", urlreverse("ietf.doc.views_material.choose_material_type", kwargs=kwargs)))

View file

@ -2,102 +2,72 @@
import datetime
import calendar
import json
from django import forms
from django.http import HttpResponse, HttpResponseForbidden, HttpResponseBadRequest, HttpResponseRedirect, Http404
from django.http import HttpResponseForbidden, HttpResponseBadRequest, HttpResponseRedirect, Http404
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from ietf.doc.models import Document, DocEvent
from ietf.doc.models import DocEvent
from ietf.doc.utils import get_chartering_type
from ietf.doc.fields import SearchableDocumentsField
from ietf.group.models import GroupMilestone, MilestoneGroupEvent
from ietf.group.utils import (save_milestone_in_history, can_manage_group_type, milestone_reviewer_for_group_type,
get_group_or_404)
from ietf.name.models import GroupMilestoneStateName
from ietf.group.mails import email_milestones_changed
def json_doc_names(docs):
return json.dumps([{"id": doc.pk, "name": doc.name } for doc in docs])
def parse_doc_names(s):
return Document.objects.filter(pk__in=[x.strip() for x in s.split(",") if x.strip()], type="draft")
from ietf.utils.fields import DatepickerDateField
class MilestoneForm(forms.Form):
id = forms.IntegerField(required=True, widget=forms.HiddenInput)
desc = forms.CharField(max_length=500, label="Milestone:", required=True)
due_month = forms.TypedChoiceField(choices=(), required=True, coerce=int)
due_year = forms.TypedChoiceField(choices=(), required=True, coerce=int)
desc = forms.CharField(max_length=500, label="Milestone", required=True)
due = DatepickerDateField(date_format="MM yyyy", picker_settings={"min-view-mode": "months", "autoclose": "1", "view-mode": "years" }, required=True)
docs = SearchableDocumentsField(label="Drafts", required=False, help_text="Any drafts that the milestone concerns.")
resolved_checkbox = forms.BooleanField(required=False, label="Resolved")
resolved = forms.CharField(max_length=50, required=False)
resolved = forms.CharField(label="Resolved as", max_length=50, required=False)
delete = forms.BooleanField(required=False, initial=False)
docs = forms.CharField(max_length=10000, required=False)
accept = forms.ChoiceField(choices=(("accept", "Accept"), ("reject", "Reject and delete"), ("noaction", "No action")),
review = forms.ChoiceField(label="Review action", help_text="Choose whether to accept or reject the proposed changes.",
choices=(("accept", "Accept"), ("reject", "Reject and delete"), ("noaction", "No action")),
required=False, initial="noaction", widget=forms.RadioSelect)
def __init__(self, *args, **kwargs):
kwargs["label_suffix"] = ""
def __init__(self, needs_review, reviewer, *args, **kwargs):
m = self.milestone = kwargs.pop("instance", None)
self.needs_review = kwargs.pop("needs_review", False)
can_review = not self.needs_review
can_review = not needs_review
if m:
self.needs_review = m.state_id == "review"
needs_review = m.state_id == "review"
if not "initial" in kwargs:
kwargs["initial"] = {}
kwargs["initial"].update(dict(id=m.pk,
desc=m.desc,
due_month=m.due.month,
due_year=m.due.year,
due=m.due,
resolved_checkbox=bool(m.resolved),
resolved=m.resolved,
docs=",".join(m.docs.values_list("pk", flat=True)),
docs=m.docs.all(),
delete=False,
accept="noaction" if can_review and self.needs_review else None,
review="noaction" if can_review and needs_review else "",
))
kwargs["prefix"] = "m%s" % m.pk
super(MilestoneForm, self).__init__(*args, **kwargs)
# set choices for due date
this_year = datetime.date.today().year
self.fields["resolved"].widget.attrs["data-default"] = "Done"
self.fields["due_month"].choices = [(month, datetime.date(this_year, month, 1).strftime("%B")) for month in range(1, 13)]
if needs_review and self.milestone and self.milestone.state_id != "review":
self.fields["desc"].widget.attrs["readonly"] = True
years = [ y for y in range(this_year, this_year + 10)]
self.changed = False
initial = self.initial.get("due_year")
if initial and initial not in years:
years.insert(0, initial)
if not (needs_review and can_review):
self.fields["review"].widget = forms.HiddenInput()
self.fields["due_year"].choices = zip(years, map(str, years))
# figure out what to prepopulate many-to-many field with
pre = ""
if not self.is_bound:
pre = self.initial.get("docs", "")
else:
pre = self["docs"].data or ""
# this is ugly, but putting it on self["docs"] is buggy with a
# bound/unbound form in Django 1.2
self.docs_names = parse_doc_names(pre)
self.docs_prepopulate = json_doc_names(self.docs_names)
# calculate whether we've changed
self.changed = self.is_bound and (not self.milestone or any(unicode(self[f].data) != unicode(self.initial[f]) for f in self.fields.iterkeys()))
def clean_docs(self):
s = self.cleaned_data["docs"]
return Document.objects.filter(pk__in=[x.strip() for x in s.split(",") if x.strip()], type="draft")
self.needs_review = needs_review
def clean_resolved(self):
r = self.cleaned_data["resolved"].strip()
@ -137,14 +107,17 @@ def edit_milestones(request, acronym, group_type=None, milestone_set="current"):
title = "Edit charter milestones for %s %s" % (group.acronym, group.type.name)
milestones = group.groupmilestone_set.filter(state="charter")
reviewer = milestone_reviewer_for_group_type(group_type)
forms = []
milestones_dict = dict((str(m.id), m) for m in milestones)
def due_month_year_to_date(c):
y = c["due_year"]
m = c["due_month"]
return datetime.date(y, m, calendar.monthrange(y, m)[1])
y = c["due"].year
m = c["due"].month
first_day, last_day = calendar.monthrange(y, m)
return datetime.date(y, m, last_day)
def set_attributes_from_form(f, m):
c = f.cleaned_data
@ -156,10 +129,24 @@ def edit_milestones(request, acronym, group_type=None, milestone_set="current"):
m.state = GroupMilestoneStateName.objects.get(slug="active")
elif milestone_set == "charter":
m.state = GroupMilestoneStateName.objects.get(slug="charter")
m.desc = c["desc"]
m.due = due_month_year_to_date(c)
m.resolved = c["resolved"]
def milestone_changed(f, m):
# we assume that validation has run
if not m or not f.is_valid():
return True
c = f.cleaned_data
return (c["desc"] != m.desc or
due_month_year_to_date(c) != m.due or
c["resolved"] != m.resolved or
set(c["docs"]) != set(m.docs.all()) or
c.get("review") in ("accept", "reject")
)
def save_milestone_form(f):
c = f.cleaned_data
@ -183,14 +170,14 @@ def edit_milestones(request, acronym, group_type=None, milestone_set="current"):
changes = ['Changed %s' % named_milestone]
if m.state_id == "review" and not needs_review and c["accept"] != "noaction":
if m.state_id == "review" and not needs_review and c["review"] != "noaction":
if not history:
history = save_milestone_in_history(m)
if c["accept"] == "accept":
if c["review"] == "accept":
m.state_id = "active"
changes.append("set state to active from review, accepting new milestone")
elif c["accept"] == "reject":
elif c["review"] == "reject":
m.state_id = "deleted"
changes.append("set state to deleted from review, rejecting new milestone")
@ -260,8 +247,6 @@ def edit_milestones(request, acronym, group_type=None, milestone_set="current"):
elif m.state_id == "review":
return 'Added %s for review, due %s' % (named_milestone, m.due.strftime("%B %Y"))
finished_milestone_text = "Done"
form_errors = False
if request.method == 'POST':
@ -272,22 +257,23 @@ def edit_milestones(request, acronym, group_type=None, milestone_set="current"):
# new milestones have non-existing ids so instance end up as None
instance = milestones_dict.get(request.POST.get(prefix + "-id", ""), None)
f = MilestoneForm(request.POST, prefix=prefix, instance=instance,
needs_review=needs_review)
f = MilestoneForm(needs_review, reviewer, request.POST, prefix=prefix, instance=instance)
forms.append(f)
form_errors = form_errors or not f.is_valid()
f.changed = milestone_changed(f, f.milestone)
if f.is_valid() and f.cleaned_data.get("review") in ("accept", "reject"):
f.needs_review = False
action = request.POST.get("action", "review")
if action == "review":
for f in forms:
if not f.is_valid():
continue
# let's fill in the form milestone so we can output it in the template
if not f.milestone:
f.milestone = GroupMilestone()
set_attributes_from_form(f, f.milestone)
if f.is_valid():
# let's fill in the form milestone so we can output it in the template
if not f.milestone:
f.milestone = GroupMilestone()
set_attributes_from_form(f, f.milestone)
elif action == "save" and not form_errors:
changes = []
for f in forms:
@ -314,11 +300,11 @@ def edit_milestones(request, acronym, group_type=None, milestone_set="current"):
return HttpResponseRedirect(group.about_url())
else:
for m in milestones:
forms.append(MilestoneForm(instance=m, needs_review=needs_review))
forms.append(MilestoneForm(needs_review, reviewer, instance=m))
can_reset = milestone_set == "charter" and get_chartering_type(group.charter) == "rechartering"
empty_form = MilestoneForm(needs_review=needs_review)
empty_form = MilestoneForm(needs_review, reviewer)
forms.sort(key=lambda f: f.milestone.due if f.milestone else datetime.date.max)
@ -329,9 +315,8 @@ def edit_milestones(request, acronym, group_type=None, milestone_set="current"):
form_errors=form_errors,
empty_form=empty_form,
milestone_set=milestone_set,
finished_milestone_text=finished_milestone_text,
needs_review=needs_review,
reviewer=milestone_reviewer_for_group_type(group_type),
reviewer=reviewer,
can_reset=can_reset))
@login_required
@ -391,8 +376,3 @@ def reset_charter_milestones(request, group_type, acronym):
charter_milestones=charter_milestones,
current_milestones=current_milestones,
))
def ajax_search_docs(request, group_type, acronym):
docs = Document.objects.filter(name__icontains=request.GET.get('q',''), type="draft").order_by('name').distinct()[:20]
return HttpResponse(json_doc_names(docs), content_type='application/json')

View file

@ -24,8 +24,8 @@ class GroupInfo(models.Model):
list_archive = models.CharField(max_length=255, blank=True)
comments = models.TextField(blank=True)
unused_states = models.ManyToManyField('doc.State', help_text="Document states that have been disabled for the group", blank=True)
unused_tags = models.ManyToManyField(DocTagName, help_text="Document tags that have been disabled for the group", blank=True)
unused_states = models.ManyToManyField('doc.State', help_text="Document states that have been disabled for the group.", blank=True)
unused_tags = models.ManyToManyField(DocTagName, help_text="Document tags that have been disabled for the group.", blank=True)
def __unicode__(self):
return self.name
@ -170,7 +170,7 @@ class GroupMilestoneInfo(models.Model):
state = models.ForeignKey(GroupMilestoneStateName)
desc = models.CharField(verbose_name="Description", max_length=500)
due = models.DateField()
resolved = models.CharField(max_length=50, blank=True, help_text="Explanation of why milestone is resolved (usually \"Done\"), or empty if still due")
resolved = models.CharField(max_length=50, blank=True, help_text="Explanation of why milestone is resolved (usually \"Done\"), or empty if still due.")
docs = models.ManyToManyField('doc.Document', blank=True)
@ -230,7 +230,7 @@ class Role(models.Model):
name = models.ForeignKey(RoleName)
group = models.ForeignKey(Group)
person = models.ForeignKey(Person)
email = models.ForeignKey(Email, help_text="Email address used by person for this role")
email = models.ForeignKey(Email, help_text="Email address used by person for this role.")
def __unicode__(self):
return u"%s is %s in %s" % (self.person.plain_name(), self.name.name, self.group.acronym or self.group.name)
@ -245,7 +245,7 @@ class RoleHistory(models.Model):
name = models.ForeignKey(RoleName)
group = models.ForeignKey(GroupHistory)
person = models.ForeignKey(Person)
email = models.ForeignKey(Email, help_text="Email address used by person for this role")
email = models.ForeignKey(Email, help_text="Email address used by person for this role.")
def __unicode__(self):
return u"%s is %s in %s" % (self.person.plain_name(), self.name.name, self.group.acronym)

View file

@ -1,7 +1,6 @@
import os
import shutil
import calendar
import json
import datetime
from pyquery import PyQuery
@ -99,7 +98,7 @@ class GroupPagesTests(TestCase):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('table.ietf-doctable td.acronym a:contains("%s")' % group.acronym)), 1)
self.assertEqual(len(q('.content-wrapper a:contains("%s")' % group.acronym)), 1)
def test_concluded_groups(self):
draft = make_test_data()
@ -111,7 +110,7 @@ class GroupPagesTests(TestCase):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('table.concluded-groups a:contains("%s")' % group.acronym)), 1)
self.assertEqual(len(q('.content-wrapper a:contains("%s")' % group.acronym)), 1)
def test_bofs(self):
draft = make_test_data()
@ -123,7 +122,7 @@ class GroupPagesTests(TestCase):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('table.ietf-doctable td.acronym a:contains("%s")' % group.acronym)), 1)
self.assertEqual(len(q('.content-wrapper a:contains("%s")' % group.acronym)), 1)
def test_group_documents(self):
draft = make_test_data()
@ -302,7 +301,7 @@ class GroupEditTests(TestCase):
r = self.client.post(url, dict(acronym="foobarbaz")) # No name
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
self.assertTrue(len(q('form .has-error')) > 0)
self.assertEqual(len(Group.objects.filter(type="wg")), num_wgs)
# acronym contains non-alphanumeric
@ -330,7 +329,7 @@ class GroupEditTests(TestCase):
self.assertEqual(group.charter.name, "charter-ietf-testwg")
self.assertEqual(group.charter.rev, "00-00")
def test_create_based_on_existing(self):
def test_create_based_on_existing_bof(self):
make_test_data()
url = urlreverse('group_create', kwargs=dict(group_type="wg"))
@ -342,8 +341,8 @@ class GroupEditTests(TestCase):
r = self.client.post(url, dict(name="Test", acronym=group.parent.acronym))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
self.assertEqual(len(q('form input[name="confirmed"]')), 0) # can't confirm us out of this
self.assertTrue(len(q('form .has-error')) > 0)
self.assertEqual(len(q('form input[name="confirm_acronym"]')), 0) # can't confirm us out of this
# try elevating BoF to WG
group.state_id = "bof"
@ -352,14 +351,14 @@ class GroupEditTests(TestCase):
r = self.client.post(url, dict(name="Test", acronym=group.acronym))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
self.assertEqual(len(q('form input[name="confirmed"]')), 1)
self.assertTrue(len(q('form .has-error')) > 0)
self.assertEqual(len(q('form input[name="confirm_acronym"]')), 1)
self.assertEqual(Group.objects.get(acronym=group.acronym).state_id, "bof")
# confirm elevation
state = GroupStateName.objects.get(slug="proposed")
r = self.client.post(url, dict(name="Test", acronym=group.acronym, confirmed="1",state=state.pk))
r = self.client.post(url, dict(name="Test", acronym=group.acronym, confirm_acronym="1", state=state.pk))
self.assertEqual(r.status_code, 302)
self.assertEqual(Group.objects.get(acronym=group.acronym).state_id, "proposed")
self.assertEqual(Group.objects.get(acronym=group.acronym).name, "Test")
@ -383,7 +382,7 @@ class GroupEditTests(TestCase):
r = self.client.post(url, dict(acronym="collide"))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
self.assertTrue(len(q('form .has-error')) > 0)
# create old acronym
group.acronym = "oldmars"
@ -396,7 +395,7 @@ class GroupEditTests(TestCase):
r = self.client.post(url, dict(acronym="oldmars"))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
self.assertTrue(len(q('form .has-error')) > 0)
# edit info
with open(os.path.join(self.charter_dir, "%s-%s.txt" % (group.charter.canonical_name(), group.charter.rev)), "w") as f:
@ -453,7 +452,7 @@ class GroupEditTests(TestCase):
r = self.client.post(url, dict(instructions="")) # No instructions
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
self.assertTrue(len(q('form .has-error')) > 0)
# request conclusion
mailbox_before = len(outbox)
@ -530,15 +529,14 @@ class MilestoneTests(TestCase):
r = self.client.post(url, { 'prefix': "m-1",
'm-1-id': "-1",
'm-1-desc': "", # no description
'm-1-due_month': str(due.month),
'm-1-due_year': str(due.year),
'm-1-due': due.strftime("%B %Y"),
'm-1-resolved': "",
'm-1-docs': ",".join(docs),
'action': "save",
})
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
self.assertTrue(len(q('form .has-error')) > 0)
self.assertEqual(GroupMilestone.objects.count(), milestones_before)
# add
@ -546,8 +544,7 @@ class MilestoneTests(TestCase):
r = self.client.post(url, { 'prefix': "m-1",
'm-1-id': "-1",
'm-1-desc': "Test 3",
'm-1-due_month': str(due.month),
'm-1-due_year': str(due.year),
'm-1-due': due.strftime("%B %Y"),
'm-1-resolved': "",
'm-1-docs': ",".join(docs),
'action': "save",
@ -584,8 +581,7 @@ class MilestoneTests(TestCase):
r = self.client.post(url, { 'prefix': "m-1",
'm-1-id': -1,
'm-1-desc': "Test 3",
'm-1-due_month': str(due.month),
'm-1-due_year': str(due.year),
'm-1-due': due.strftime("%B %Y"),
'm-1-resolved': "",
'm-1-docs': "",
'action': "save",
@ -619,11 +615,10 @@ class MilestoneTests(TestCase):
r = self.client.post(url, { 'prefix': "m1",
'm1-id': m1.id,
'm1-desc': m1.desc,
'm1-due_month': str(m1.due.month),
'm1-due_year': str(m1.due.year),
'm1-due': m1.due.strftime("%B %Y"),
'm1-resolved': m1.resolved,
'm1-docs': ",".join(m1.docs.values_list("name", flat=True)),
'm1-accept': "accept",
'm1-review': "accept",
'action': "save",
})
self.assertEqual(r.status_code, 302)
@ -646,8 +641,7 @@ class MilestoneTests(TestCase):
r = self.client.post(url, { 'prefix': "m1",
'm1-id': m1.id,
'm1-desc': m1.desc,
'm1-due_month': str(m1.due.month),
'm1-due_year': str(m1.due.year),
'm1-due': m1.due.strftime("%B %Y"),
'm1-resolved': "",
'm1-docs': ",".join(m1.docs.values_list("name", flat=True)),
'm1-delete': "checked",
@ -677,15 +671,14 @@ class MilestoneTests(TestCase):
r = self.client.post(url, { 'prefix': "m1",
'm1-id': m1.id,
'm1-desc': "", # no description
'm1-due_month': str(due.month),
'm1-due_year': str(due.year),
'm1-due': due.strftime("%B %Y"),
'm1-resolved': "",
'm1-docs': ",".join(docs),
'action': "save",
})
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
self.assertTrue(len(q('form .has-error')) > 0)
m = GroupMilestone.objects.get(pk=m1.pk)
self.assertEqual(GroupMilestone.objects.count(), milestones_before)
self.assertEqual(m.due, m1.due)
@ -695,8 +688,7 @@ class MilestoneTests(TestCase):
r = self.client.post(url, { 'prefix': "m1",
'm1-id': m1.id,
'm1-desc': "Test 2 - changed",
'm1-due_month': str(due.month),
'm1-due_year': str(due.year),
'm1-due': due.strftime("%B %Y"),
'm1-resolved': "Done",
'm1-resolved_checkbox': "checked",
'm1-docs': ",".join(docs),
@ -873,15 +865,6 @@ class MilestoneTests(TestCase):
self.assertTrue(m1.desc in unicode(outbox[-1]))
self.assertTrue(m2.desc in unicode(outbox[-1]))
def test_ajax_search_docs(self):
draft = make_test_data()
r = self.client.get(urlreverse("group_ajax_search_docs", kwargs=dict(group_type=draft.group.type_id, acronym=draft.group.acronym)),
dict(q=draft.name))
self.assertEqual(r.status_code, 200)
data = json.loads(r.content)
self.assertTrue(data[0]["id"], draft.name)
class CustomizeWorkflowTests(TestCase):
def test_customize_workflow(self):
make_test_data()

View file

@ -22,7 +22,6 @@ urlpatterns = patterns('',
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/milestones/$', 'ietf.group.milestones.edit_milestones', {'milestone_set': "current"}, "group_edit_milestones"),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/milestones/charter/$', 'ietf.group.milestones.edit_milestones', {'milestone_set': "charter"}, "group_edit_charter_milestones"),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/milestones/charter/reset/$', 'ietf.group.milestones.reset_charter_milestones', None, "group_reset_charter_milestones"),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/ajax/searchdocs/$', 'ietf.group.milestones.ajax_search_docs', None, "group_ajax_search_docs"),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/workflow/$', 'ietf.group.edit.customize_workflow'),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/about/(?P<group_type>.)?$', 'ietf.group.info.group_about', None, 'group_about'),

View file

@ -31,6 +31,5 @@ urlpatterns = patterns('',
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/milestones/$', milestones.edit_milestones, {'milestone_set': "current"}, "group_edit_milestones"),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/milestones/charter/$', milestones.edit_milestones, {'milestone_set': "charter"}, "group_edit_charter_milestones"),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/milestones/charter/reset/$', milestones.reset_charter_milestones, None, "group_reset_charter_milestones"),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/ajax/searchdocs/$', milestones.ajax_search_docs, None, "group_ajax_search_docs"),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/workflow/$', edit.customize_workflow),
)

View file

@ -10,7 +10,7 @@ from ietf.group.models import Group, GroupEvent, Role
from ietf.group.utils import save_group_in_history
from ietf.ietfauth.utils import has_role
from ietf.name.models import StreamName
from ietf.person.fields import AutocompletedEmailsField
from ietf.person.fields import SearchableEmailsField
from ietf.person.models import Email
import debug # pyflakes:ignore
@ -24,14 +24,16 @@ def stream_documents(request, acronym):
streams = [ s.slug for s in StreamName.objects.all().exclude(slug__in=['ietf', 'legacy']) ]
if not acronym in streams:
raise Http404("No such stream: %s" % acronym)
group = get_object_or_404(Group, acronym=acronym)
editable = has_role(request.user, "Secretariat") or group.has_role(request.user, "chair")
stream = StreamName.objects.get(slug=acronym)
form = SearchForm({'by':'stream', 'stream':acronym,
'rfcs':'on', 'activedrafts':'on'})
docs, meta = retrieve_search_results(form)
return render_to_response('group/stream_documents.html', {'stream':stream, 'docs':docs, 'meta':meta }, context_instance=RequestContext(request))
return render_to_response('group/stream_documents.html', {'stream':stream, 'docs':docs, 'meta':meta, 'editable':editable }, context_instance=RequestContext(request))
class StreamEditForm(forms.Form):
delegates = AutocompletedEmailsField(required=False, only_users=True)
delegates = SearchableEmailsField(required=False, only_users=True)
def stream_edit(request, acronym):
group = get_object_or_404(Group, acronym=acronym)
@ -62,7 +64,7 @@ def stream_edit(request, acronym):
for e in new:
Role.objects.get_or_create(name_id=slug, email=e, group=group, person=e.person)
return redirect("ietf.group.views.streams")
return redirect("ietf.group.views_stream.streams")
else:
form = StreamEditForm(initial=dict(delegates=Email.objects.filter(role__group=group, role__name="delegate")))
@ -72,4 +74,4 @@ def stream_edit(request, acronym):
'form': form,
},
context_instance=RequestContext(request))

View file

@ -85,50 +85,50 @@ def get_doc_section(doc):
def agenda_sections():
return OrderedDict([
('1', {'title':"Administrivia"}),
('1.1', {'title':"Roll Call"}),
('1.2', {'title':"Bash the Agenda"}),
('1.3', {'title':"Approval of the Minutes of Past Telechats"}),
('1.4', {'title':"List of Remaining Action Items from Last Telechat"}),
('2', {'title':"Protocol Actions"}),
('2.1', {'title':"WG Submissions"}),
('2.1.1', {'title':"New Items", 'docs': []}),
('2.1.2', {'title':"Returning Items", 'docs':[]}),
('2.1.3', {'title':"For Action", 'docs':[]}),
('2.2', {'title':"Individual Submissions"}),
('2.2.1', {'title':"New Items", 'docs':[]}),
('2.2.2', {'title':"Returning Items", 'docs':[]}),
('2.2.3', {'title':"For Action", 'docs':[]}),
('2.3', {'title':"Status Changes"}),
('2.3.1', {'title':"New Items", 'docs':[]}),
('2.3.2', {'title':"Returning Items", 'docs':[]}),
('2.3.3', {'title':"For Action", 'docs':[]}),
('3', {'title':"Document Actions"}),
('3.1', {'title':"WG Submissions"}),
('3.1.1', {'title':"New Items", 'docs':[]}),
('3.1.2', {'title':"Returning Items", 'docs':[]}),
('3.1.3', {'title':"For Action", 'docs':[]}),
('3.2', {'title':"Individual Submissions Via AD"}),
('3.2.1', {'title':"New Items", 'docs':[]}),
('3.2.2', {'title':"Returning Items", 'docs':[]}),
('3.2.3', {'title':"For Action", 'docs':[]}),
('3.3', {'title':"Status Changes"}),
('3.3.1', {'title':"New Items", 'docs':[]}),
('3.3.2', {'title':"Returning Items", 'docs':[]}),
('3.3.3', {'title':"For Action", 'docs':[]}),
('3.4', {'title':"IRTF and Independent Submission Stream Documents"}),
('3.4.1', {'title':"New Items", 'docs':[]}),
('3.4.2', {'title':"Returning Items", 'docs':[]}),
('3.4.3', {'title':"For Action", 'docs':[]}),
('4', {'title':"Working Group Actions"}),
('4.1', {'title':"WG Creation"}),
('4.1.1', {'title':"Proposed for IETF Review", 'docs':[]}),
('4.1.2', {'title':"Proposed for Approval", 'docs':[]}),
('4.2', {'title':"WG Rechartering"}),
('4.2.1', {'title':"Under Evaluation for IETF Review", 'docs':[]}),
('4.2.2', {'title':"Proposed for Approval", 'docs':[]}),
('5', {'title':"IAB News We Can Use"}),
('6', {'title':"Management Issues"}),
('7', {'title':"Working Group News"}),
('1.1', {'title':"Roll call"}),
('1.2', {'title':"Bash the agenda"}),
('1.3', {'title':"Approval of the minutes of past telechats"}),
('1.4', {'title':"List of remaining action items from last telechat"}),
('2', {'title':"Protocol actions"}),
('2.1', {'title':"WG submissions"}),
('2.1.1', {'title':"New items", 'docs': []}),
('2.1.2', {'title':"Returning items", 'docs':[]}),
('2.1.3', {'title':"For action", 'docs':[]}),
('2.2', {'title':"Individual submissions"}),
('2.2.1', {'title':"New items", 'docs':[]}),
('2.2.2', {'title':"Returning items", 'docs':[]}),
('2.2.3', {'title':"For action", 'docs':[]}),
('2.3', {'title':"Status changes"}),
('2.3.1', {'title':"New items", 'docs':[]}),
('2.3.2', {'title':"Returning items", 'docs':[]}),
('2.3.3', {'title':"For action", 'docs':[]}),
('3', {'title':"Document actions"}),
('3.1', {'title':"WG submissions"}),
('3.1.1', {'title':"New items", 'docs':[]}),
('3.1.2', {'title':"Returning items", 'docs':[]}),
('3.1.3', {'title':"For action", 'docs':[]}),
('3.2', {'title':"Individual submissions via AD"}),
('3.2.1', {'title':"New items", 'docs':[]}),
('3.2.2', {'title':"Returning items", 'docs':[]}),
('3.2.3', {'title':"For action", 'docs':[]}),
('3.3', {'title':"Status changes"}),
('3.3.1', {'title':"New items", 'docs':[]}),
('3.3.2', {'title':"Returning items", 'docs':[]}),
('3.3.3', {'title':"For action", 'docs':[]}),
('3.4', {'title':"IRTF and Independent Submission stream documents"}),
('3.4.1', {'title':"New items", 'docs':[]}),
('3.4.2', {'title':"Returning items", 'docs':[]}),
('3.4.3', {'title':"For action", 'docs':[]}),
('4', {'title':"Working Group actions"}),
('4.1', {'title':"WG creation"}),
('4.1.1', {'title':"Proposed for IETF review", 'docs':[]}),
('4.1.2', {'title':"Proposed for approval", 'docs':[]}),
('4.2', {'title':"WG rechartering"}),
('4.2.1', {'title':"Under evaluation for IETF review", 'docs':[]}),
('4.2.2', {'title':"Proposed for approval", 'docs':[]}),
('5', {'title':"IAB news we can use"}),
('6', {'title':"Management issues"}),
('7', {'title':"Working Group news"}),
])
def fill_in_agenda_administrivia(date, sections):
@ -185,7 +185,7 @@ def fill_in_agenda_docs(date, sections, matches=None):
# prune empty "For action" sections
empty_for_action = [n for n, section in sections.iteritems()
if section["title"] == "For Action" and not section["docs"]]
if section["title"] == "For action" and not section["docs"]]
for num in empty_for_action:
del sections[num]

View file

@ -2,24 +2,24 @@
# Portion Copyright (C) 2008-2009 Nokia Corporation and/or its subsidiary(-ies).
# All rights reserved. Contact: Pasi Eronen <pasi.eronen@nokia.com>
#
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# modification, are permitted provided that the following conditions
# are met:
#
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
#
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
#
#
# * Neither the name of the Nokia Corporation and/or its
# subsidiary(-ies) nor the names of its contributors may be used
# to endorse or promote products derived from this software
# without specific prior written permission.
#
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
@ -57,6 +57,7 @@ from ietf.iesg.agenda import agenda_data, agenda_sections, fill_in_agenda_docs,
from ietf.iesg.models import TelechatDate
from ietf.ietfauth.utils import has_role, role_required, user_is_person
from ietf.person.models import Person
from ietf.doc.views_search import fill_in_search_attributes
def review_decisions(request, year=None):
events = DocEvent.objects.filter(type__in=("iesg_disapproved", "iesg_approved"))
@ -176,8 +177,8 @@ def agenda(request, date=None):
data = agenda_data(date)
if has_role(request.user, ["Area Director", "IAB Chair", "Secretariat"]):
data["sections"]["1.1"]["title"] = data["sections"]["1.1"]["title"].replace("Roll Call", '<a href="https://www.ietf.org/iesg/internal/rollcall.txt">Roll Call</a>')
data["sections"]["1.3"]["title"] = data["sections"]["1.3"]["title"].replace("Minutes", '<a href="https://www.ietf.org/iesg/internal/minutes.txt">Minutes</a>')
data["sections"]["1.1"]["title"] = data["sections"]["1.1"]["title"].replace("Roll call", '<a href="https://www.ietf.org/iesg/internal/rollcall.txt">Roll Call</a>')
data["sections"]["1.3"]["title"] = data["sections"]["1.3"]["title"].replace("minutes", '<a href="https://www.ietf.org/iesg/internal/minutes.txt">Minutes</a>')
request.session['ballot_edit_return_point'] = request.path_info
return render_to_response("iesg/agenda.html", {
@ -303,7 +304,7 @@ class RescheduleForm(forms.Form):
def __init__(self, *args, **kwargs):
dates = kwargs.pop('telechat_dates')
super(self.__class__, self).__init__(*args, **kwargs)
# telechat choices
@ -360,6 +361,9 @@ def agenda_documents(request):
telechats = []
for date in dates:
sections = agenda_sections()
# augment the docs with the search attributes, since we're using
# the search_result_row view to display them (which expects them)
fill_in_search_attributes(docs_by_date[date])
fill_in_agenda_docs(date, sections, docs_by_date[date])
telechats.append({

View file

@ -217,7 +217,7 @@ class PersonForm(ModelForm):
# Make sure the alias table contains any new and/or old names.
old_names = set([x.name for x in Alias.objects.filter(person=self.instance)])
curr_names = set([x for x in [self.instance.name,
curr_names = set([x for x in [self.instance.name,
self.instance.ascii,
self.instance.ascii_short,
self.data['name'],

View file

@ -4,73 +4,58 @@ from django.utils.html import escape
from django import forms
from django.core.urlresolvers import reverse as urlreverse
import debug # pyflakes:ignore
from ietf.doc.models import DocAlias
from ietf.ipr.models import IprDisclosureBase
def tokeninput_id_name_json(objs):
"""Returns objects as JSON string.
NOTE: double quotes in the object name are replaced with single quotes to avoid
problems with representation of the JSON string in the HTML widget attribute"""
def format_ipr(x):
text = x.title.replace('"',"'")
return escape(u"%s <%s>" % (text, x.time.date().isoformat()))
def format_doc(x):
return escape(x.name)
def select2_id_ipr_title_json(value):
return json.dumps([{ "id": o.pk, "text": escape(u"%s <%s>" % (o.title, o.time.date().isoformat())) } for o in value])
formatter = format_ipr if objs and isinstance(objs[0], IprDisclosureBase) else format_doc
return json.dumps([{ "id": o.pk, "name": formatter(o) } for o in objs])
class AutocompletedIprDisclosuresField(forms.CharField):
"""Tokenizing autocompleted multi-select field for choosing
IPR disclosures using jquery.tokeninput.js.
class SearchableIprDisclosuresField(forms.CharField):
"""Server-based multi-select field for choosing documents using
select2.js.
The field uses a comma-separated list of primary keys in a
CharField element as its API, the tokeninput Javascript adds some
selection magic on top of this so we have to pass it a JSON
representation of ids and user-understandable labels."""
CharField element as its API with some extra attributes used by
the Javascript part."""
def __init__(self,
max_entries=None, # max number of selected objs
model=IprDisclosureBase,
hint_text="Type in term(s) to search disclosure title",
hint_text="Type in terms to search disclosure title",
*args, **kwargs):
kwargs["max_length"] = 1000
self.max_entries = max_entries
self.model = model
super(AutocompletedIprDisclosuresField, self).__init__(*args, **kwargs)
super(SearchableIprDisclosuresField, self).__init__(*args, **kwargs)
self.widget.attrs["class"] = "tokenized-field"
self.widget.attrs["data-hint-text"] = hint_text
self.widget.attrs["class"] = "select2-field form-control"
self.widget.attrs["data-placeholder"] = hint_text
if self.max_entries != None:
self.widget.attrs["data-max-entries"] = self.max_entries
def parse_tokenized_value(self, value):
def parse_select2_value(self, value):
return [x.strip() for x in value.split(",") if x.strip()]
def prepare_value(self, value):
if not value:
value = ""
if isinstance(value, basestring):
pks = self.parse_tokenized_value(value)
pks = self.parse_select2_value(value)
value = self.model.objects.filter(pk__in=pks)
if isinstance(value, self.model):
value = [value]
self.widget.attrs["data-pre"] = tokeninput_id_name_json(value)
self.widget.attrs["data-pre"] = select2_id_ipr_title_json(value)
# doing this in the constructor is difficult because the URL
# patterns may not have been fully constructed there yet
self.widget.attrs["data-ajax-url"] = urlreverse("ipr_ajax_search")
return ",".join(str(e.pk) for e in value)
return u",".join(unicode(e.pk) for e in value)
def clean(self, value):
value = super(AutocompletedIprDisclosuresField, self).clean(value)
pks = self.parse_tokenized_value(value)
value = super(SearchableIprDisclosuresField, self).clean(value)
pks = self.parse_select2_value(value)
objs = self.model.objects.filter(pk__in=pks)
@ -83,75 +68,3 @@ class AutocompletedIprDisclosuresField(forms.CharField):
raise forms.ValidationError(u"You can select at most %s entries only." % self.max_entries)
return objs
class AutocompletedDraftsField(AutocompletedIprDisclosuresField):
"""Version of AutocompletedPersonsField with the defaults right for Drafts."""
def __init__(self, model=DocAlias, hint_text="Type in name to search draft name",
*args, **kwargs):
super(AutocompletedDraftsField, self).__init__(model=model, hint_text=hint_text, *args, **kwargs)
def prepare_value(self, value):
if not value:
value = ""
if isinstance(value, basestring):
pks = self.parse_tokenized_value(value)
value = self.model.objects.filter(pk__in=pks)
if isinstance(value, self.model):
value = [value]
if isinstance(value, long):
value = self.model.objects.filter(pk=value)
self.widget.attrs["data-pre"] = tokeninput_id_name_json(value)
# doing this in the constructor is difficult because the URL
# patterns may not have been fully constructed there yet
self.widget.attrs["data-ajax-url"] = urlreverse("ipr_ajax_draft_search")
return ",".join(str(e.pk) for e in value)
class AutocompletedDraftField(AutocompletedDraftsField):
"""Version of AutocompletedEmailsField specialized to a single object."""
def __init__(self, *args, **kwargs):
kwargs["max_entries"] = 1
super(AutocompletedDraftField, self).__init__(*args, **kwargs)
def clean(self, value):
return super(AutocompletedDraftField, self).clean(value).first()
class AutocompletedRfcsField(AutocompletedIprDisclosuresField):
"""Version of AutocompletedPersonsField with the defaults right for Drafts."""
def __init__(self, model=DocAlias, hint_text="Type in the RFC number",
*args, **kwargs):
super(AutocompletedRfcsField, self).__init__(model=model, hint_text=hint_text, *args, **kwargs)
def prepare_value(self, value):
if not value:
value = ""
if isinstance(value, basestring):
pks = self.parse_tokenized_value(value)
value = self.model.objects.filter(pk__in=pks)
if isinstance(value, self.model):
value = [value]
if isinstance(value, long):
value = self.model.objects.filter(pk=value)
self.widget.attrs["data-pre"] = tokeninput_id_name_json(value)
# doing this in the constructor is difficult because the URL
# patterns may not have been fully constructed there yet
self.widget.attrs["data-ajax-url"] = urlreverse("ipr_ajax_rfc_search")
return ",".join(str(e.pk) for e in value)
class AutocompletedRfcField(AutocompletedRfcsField):
"""Version of AutocompletedEmailsField specialized to a single object."""
def __init__(self, *args, **kwargs):
kwargs["max_entries"] = 1
super(AutocompletedRfcField, self).__init__(*args, **kwargs)
def clean(self, value):
return super(AutocompletedRfcField, self).clean(value).first()

View file

@ -1,16 +1,18 @@
import datetime
import email
from django.utils.safestring import mark_safe
from django import forms
from ietf.group.models import Group
from ietf.doc.fields import SearchableDocAliasField
from ietf.ipr.mail import utc_from_string
from ietf.ipr.fields import (AutocompletedIprDisclosuresField, AutocompletedDraftField,
AutocompletedRfcField)
from ietf.ipr.fields import SearchableIprDisclosuresField
from ietf.ipr.models import (IprDocRel, IprDisclosureBase, HolderIprDisclosure,
GenericIprDisclosure, ThirdPartyIprDisclosure, NonDocSpecificIprDisclosure,
IprLicenseTypeName, IprDisclosureStateName)
from ietf.message.models import Message
from ietf.utils.fields import DatepickerDateField
# ----------------------------------------------------------------
# Globals
@ -44,8 +46,8 @@ class MessageModelChoiceField(forms.ModelChoiceField):
# Forms
# ----------------------------------------------------------------
class AddCommentForm(forms.Form):
private = forms.BooleanField(required=False,help_text="If this box is checked the comment will not appear in the disclosure's public history view.")
comment = forms.CharField(required=True, widget=forms.Textarea)
private = forms.BooleanField(label="Private comment", required=False,help_text="If this box is checked the comment will not appear in the disclosure's public history view.")
class AddEmailForm(forms.Form):
direction = forms.ChoiceField(choices=(("incoming", "Incoming"), ("outgoing", "Outgoing")),
@ -89,7 +91,7 @@ class AddEmailForm(forms.Form):
return self.cleaned_data
class DraftForm(forms.ModelForm):
document = AutocompletedDraftField(required=False)
document = SearchableDocAliasField(label="I-D name/RFC number", required=False, doc_type="draft")
class Meta:
model = IprDocRel
@ -103,20 +105,20 @@ class GenericDisclosureForm(forms.Form):
"""Custom ModelForm-like form to use for new Generic or NonDocSpecific Iprs.
If patent_info is submitted create a NonDocSpecificIprDisclosure object
otherwise create a GenericIprDisclosure object."""
compliant = forms.BooleanField(required=False)
compliant = forms.BooleanField(label="This disclosure complies with RFC 3979", required=False)
holder_legal_name = forms.CharField(max_length=255)
notes = forms.CharField(max_length=255,widget=forms.Textarea,required=False)
other_designations = forms.CharField(max_length=255,required=False)
holder_contact_name = forms.CharField(max_length=255)
holder_contact_email = forms.EmailField()
holder_contact_info = forms.CharField(max_length=255,widget=forms.Textarea,required=False)
notes = forms.CharField(label="Additional notes", max_length=255,widget=forms.Textarea,required=False)
other_designations = forms.CharField(label="Designations for other contributions", max_length=255,required=False)
holder_contact_name = forms.CharField(label="Name", max_length=255)
holder_contact_email = forms.EmailField(label="Email")
holder_contact_info = forms.CharField(label="Other Info (address, phone, etc.)", max_length=255,widget=forms.Textarea,required=False)
submitter_name = forms.CharField(max_length=255,required=False)
submitter_email = forms.EmailField(required=False)
patent_info = forms.CharField(max_length=255,widget=forms.Textarea,required=False)
patent_info = forms.CharField(max_length=255,widget=forms.Textarea, required=False, help_text="Patent, Serial, Publication, Registration, or Application/File number(s), Date(s) granted or applied for, Country, and any additional notes.")
has_patent_pending = forms.BooleanField(required=False)
statement = forms.CharField(max_length=255,widget=forms.Textarea,required=False)
updates = AutocompletedIprDisclosuresField(required=False)
same_as_ii_above = forms.BooleanField(required=False)
updates = SearchableIprDisclosuresField(required=False, help_text="If this disclosure <strong>updates</strong> other disclosures identify here which ones. Leave this field blank if this disclosure does not update any prior disclosures. <strong>Note</strong>: Updates to IPR disclosures must only be made by authorized representatives of the original submitters. Updates will automatically be forwarded to the current Patent Holder's Contact and to the Submitter of the original IPR disclosure.")
same_as_ii_above = forms.BooleanField(label="Same as in section II above", required=False)
def __init__(self,*args,**kwargs):
super(GenericDisclosureForm, self).__init__(*args,**kwargs)
@ -156,7 +158,7 @@ class GenericDisclosureForm(forms.Form):
class IprDisclosureFormBase(forms.ModelForm):
"""Base form for Holder and ThirdParty disclosures"""
updates = AutocompletedIprDisclosuresField(required=False)
updates = SearchableIprDisclosuresField(required=False, help_text=mark_safe("If this disclosure <strong>updates</strong> other disclosures identify here which ones. Leave this field blank if this disclosure does not update any prior disclosures. Note: Updates to IPR disclosures must only be made by authorized representatives of the original submitters. Updates will automatically be forwarded to the current Patent Holder's Contact and to the Submitter of the original IPR disclosure."))
same_as_ii_above = forms.BooleanField(required=False)
def __init__(self,*args,**kwargs):
@ -164,6 +166,22 @@ class IprDisclosureFormBase(forms.ModelForm):
self.fields['submitter_name'].required = False
self.fields['submitter_email'].required = False
self.fields['compliant'].initial = True
self.fields['compliant'].label = "This disclosure complies with RFC 3979"
if "ietfer_name" in self.fields:
self.fields["ietfer_name"].label = "Name"
if "ietfer_contact_email" in self.fields:
self.fields["ietfer_contact_email"].label = "Email"
if "ietfer_contact_info" in self.fields:
self.fields["ietfer_contact_info"].label = "Other info"
self.fields["ietfer_contact_info"].help_text = "Address, phone, etc."
if "patent_info" in self.fields:
self.fields["patent_info"].help_text = "Patent, Serial, Publication, Registration, or Application/File number(s), Date(s) granted or applied for, Country, and any additional notes"
if "licensing" in self.fields:
self.fields["licensing_comments"].label = "Licensing information, comments, notes, or URL for further information"
if "submitter_claims_all_terms_disclosed" in self.fields:
self.fields["submitter_claims_all_terms_disclosed"].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"
if "same_as_ii_above" in self.fields:
self.fields["same_as_ii_above"].label = "Same as in section II above"
class Meta:
"""This will be overridden"""
@ -204,7 +222,7 @@ class HolderIprDisclosureForm(IprDisclosureFormBase):
def clean(self):
super(HolderIprDisclosureForm, self).clean()
cleaned_data = self.cleaned_data
if not self.data.get('draft-0-document') and not self.data.get('rfc-0-document') and not cleaned_data.get('other_designations'):
if not self.data.get('iprdocrel_set-0-document') and not cleaned_data.get('other_designations'):
raise forms.ValidationError('You need to specify a contribution in Section IV')
return cleaned_data
@ -224,7 +242,7 @@ class GenericIprDisclosureForm(IprDisclosureFormBase):
exclude = [ 'by','docs','state','rel' ]
class MessageModelForm(forms.ModelForm):
response_due = forms.DateField(required=False,help_text='The date which a response is due')
response_due = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1" }, required=False, help_text='The date which a response is due.')
class Meta:
model = Message
@ -246,12 +264,6 @@ class NotifyForm(forms.Form):
type = forms.CharField(widget=forms.HiddenInput)
text = forms.CharField(widget=forms.Textarea)
class RfcForm(DraftForm):
document = AutocompletedRfcField(required=False)
class Meta(DraftForm.Meta):
exclude = ('revisions',)
class ThirdPartyIprDisclosureForm(IprDisclosureFormBase):
class Meta:
model = ThirdPartyIprDisclosure
@ -260,7 +272,7 @@ class ThirdPartyIprDisclosureForm(IprDisclosureFormBase):
def clean(self):
super(ThirdPartyIprDisclosureForm, self).clean()
cleaned_data = self.cleaned_data
if not self.data.get('draft-0-document') and not self.data.get('rfc-0-document') and not cleaned_data.get('other_designations'):
if not self.data.get('iprdocrel_set-0-document') and not cleaned_data.get('other_designations'):
raise forms.ValidationError('You need to specify a contribution in Section III')
return cleaned_data
@ -276,15 +288,15 @@ class ThirdPartyIprDisclosureForm(IprDisclosureFormBase):
class SearchForm(forms.Form):
state = forms.MultipleChoiceField(choices=STATE_CHOICES,widget=forms.CheckboxSelectMultiple,required=False)
draft = forms.CharField(max_length=128,required=False)
rfc = forms.IntegerField(required=False)
holder = forms.CharField(max_length=128,required=False)
patent = forms.CharField(max_length=128,required=False)
group = GroupModelChoiceField(label="Working group name",queryset=Group.objects.filter(type='wg').order_by('acronym'),required=False)
doctitle = forms.CharField(max_length=128,required=False)
iprtitle = forms.CharField(max_length=128,required=False)
draft = forms.CharField(label="Draft name", max_length=128, required=False)
rfc = forms.IntegerField(label="RFC number", required=False)
holder = forms.CharField(label="Name of patent owner/applicant", max_length=128,required=False)
patent = forms.CharField(label="Text in patent information", max_length=128,required=False)
group = GroupModelChoiceField(label="Working group",queryset=Group.objects.filter(type='wg').order_by('acronym'),required=False, empty_label="(Select WG)")
doctitle = forms.CharField(label="Words in document title", max_length=128,required=False)
iprtitle = forms.CharField(label="Words in IPR disclosure title", max_length=128,required=False)
class StateForm(forms.Form):
state = forms.ModelChoiceField(queryset=IprDisclosureStateName.objects,label="New State",empty_label=None)
private = forms.BooleanField(required=False,help_text="If this box is checked the comment will not appear in the disclosure's public history view.")
comment = forms.CharField(required=False, widget=forms.Textarea)
comment = forms.CharField(required=False, widget=forms.Textarea, help_text="You may add a comment to be included in the disclosure history.")
private = forms.BooleanField(label="Private comment", required=False, help_text="If this box is checked the comment will not appear in the disclosure's public history view.")

View file

@ -12,8 +12,8 @@ LICENSE_CHOICES = (
(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).'),
' 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.'),
)
@ -27,10 +27,10 @@ SELECT_CHOICES = (
(2, 'NO'),
)
STATUS_CHOICES = (
( 0, "Waiting for approval" ),
( 1, "Approved and Posted" ),
( 2, "Rejected by Administrator" ),
( 3, "Removed by Request" ),
( 0, "Waiting for approval" ),
( 1, "Approved and Posted" ),
( 2, "Rejected by Administrator" ),
( 3, "Removed by Request" ),
)
class IprDetail(models.Model):
@ -45,35 +45,35 @@ class IprDetail(models.Model):
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)
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)
other_designations = models.CharField("Designations for other contributions", 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)
patents = models.TextField("Patent, serial, publication, registration, or application/file number(s)", db_column="p_applications", max_length=255)
date_applied = models.CharField("Date(s) granted or applied for", max_length=255)
country = models.CharField(max_length=255)
notes = models.TextField("Additional notes", db_column="p_notes", blank=True)
is_pending = models.IntegerField("Unpublished Pending Patent Application", blank=True, null=True, choices=SELECT_CHOICES, db_column="selecttype")
applies_to_all = models.IntegerField("Applies to all IPR owned by Submitter", blank=True, null=True, choices=SELECT_CHOICES, db_column="selectowned")
is_pending = models.IntegerField("Unpublished pending patent application", blank=True, null=True, choices=SELECT_CHOICES, db_column="selecttype")
applies_to_all = models.IntegerField("Applies to all IPR owned by submitter", blank=True, null=True, choices=SELECT_CHOICES, db_column="selectowned")
# Licensing Declaration fieldset
licensing_option = models.IntegerField(null=True, blank=True, choices=LICENSE_CHOICES)
lic_opt_a_sub = models.IntegerField(null=True, editable=False, choices=STDONLY_CHOICES)
lic_opt_b_sub = models.IntegerField(null=True, editable=False, choices=STDONLY_CHOICES)
lic_opt_c_sub = models.IntegerField(null=True, editable=False, choices=STDONLY_CHOICES)
comments = models.TextField("Licensing Comments", blank=True)
lic_checkbox = models.BooleanField("All terms and conditions has been disclosed", default=False)
comments = models.TextField("Licensing comments", blank=True)
lic_checkbox = models.BooleanField("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.", default=False)
# Other notes fieldset
@ -119,8 +119,8 @@ class IprContact(models.Model):
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)
address1 = models.CharField("Address", blank=True, max_length=255)
address2 = models.CharField("Address (continued)", blank=True, max_length=255)
telephone = models.CharField(blank=True, max_length=25)
fax = models.CharField(blank=True, max_length=25)
email = models.EmailField(max_length=255)
@ -182,11 +182,11 @@ from ietf.message.models import Message
class IprDisclosureBase(models.Model):
by = models.ForeignKey(Person) # who was logged in, or System if nobody was logged in
compliant = models.BooleanField(default=True) # complies to RFC3979
compliant = models.BooleanField("Complies to RFC3979", default=True)
docs = models.ManyToManyField(DocAlias, through='IprDocRel')
holder_legal_name = models.CharField(max_length=255)
notes = models.TextField(blank=True)
other_designations = models.CharField(blank=True, max_length=255)
notes = models.TextField("Additional notes", blank=True)
other_designations = models.CharField("Designations for other contributions", blank=True, max_length=255)
rel = models.ManyToManyField('self', through='RelatedIpr', symmetrical=False)
state = models.ForeignKey(IprDisclosureStateName)
submitter_name = models.CharField(max_length=255)
@ -269,7 +269,7 @@ class HolderIprDisclosure(IprDisclosureBase):
has_patent_pending = models.BooleanField(default=False)
holder_contact_email = models.EmailField()
holder_contact_name = models.CharField(max_length=255)
holder_contact_info = models.TextField(blank=True)
holder_contact_info = models.TextField(blank=True, help_text="Address, phone, etc.")
licensing = models.ForeignKey(IprLicenseTypeName)
licensing_comments = models.TextField(blank=True)
submitter_claims_all_terms_disclosed = models.BooleanField(default=False)
@ -277,7 +277,7 @@ class HolderIprDisclosure(IprDisclosureBase):
class ThirdPartyIprDisclosure(IprDisclosureBase):
ietfer_name = models.CharField(max_length=255) # "Whose Personal Belief Triggered..."
ietfer_contact_email = models.EmailField()
ietfer_contact_info = models.TextField(blank=True)
ietfer_contact_info = models.TextField(blank=True, help_text="Address, phone, etc.")
patent_info = models.TextField()
has_patent_pending = models.BooleanField(default=False)
@ -285,7 +285,7 @@ class NonDocSpecificIprDisclosure(IprDisclosureBase):
'''A Generic IPR Disclosure w/ patent information'''
holder_contact_name = models.CharField(max_length=255)
holder_contact_email = models.EmailField()
holder_contact_info = models.TextField(blank=True)
holder_contact_info = models.TextField(blank=True, help_text="Address, phone, etc.")
patent_info = models.TextField()
has_patent_pending = models.BooleanField(default=False)
statement = models.TextField() # includes licensing info
@ -293,7 +293,7 @@ class NonDocSpecificIprDisclosure(IprDisclosureBase):
class GenericIprDisclosure(IprDisclosureBase):
holder_contact_name = models.CharField(max_length=255)
holder_contact_email = models.EmailField()
holder_contact_info = models.TextField(blank=True)
holder_contact_info = models.TextField(blank=True, help_text="Address, phone, etc.")
statement = models.TextField() # includes licensing info
class IprDocRel(models.Model):

View file

@ -13,7 +13,7 @@ from ietf.ipr.models import (IprDisclosureBase,GenericIprDisclosure,HolderIprDis
ThirdPartyIprDisclosure,RelatedIpr)
from ietf.ipr.utils import get_genitive, get_ipr_summary
from ietf.message.models import Message
from ietf.utils.test_utils import TestCase
from ietf.utils.test_utils import TestCase, login_testing_unauthorized
from ietf.utils.test_data import make_test_data
@ -247,7 +247,7 @@ class IprTests(TestCase):
})
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q("ul.errorlist")) > 0)
self.assertTrue(len(q("form .has-error")) > 0)
# successful post
r = self.client.post(url, {
@ -283,13 +283,11 @@ class IprTests(TestCase):
"holder_contact_info": "555-555-0100",
"ietfer_name": "Test Participant",
"ietfer_contact_info": "555-555-0101",
"rfc-TOTAL_FORMS": 1,
"rfc-INITIAL_FORMS": 0,
"rfc-0-document": DocAlias.objects.filter(name__startswith="rfc").first().pk,
"draft-TOTAL_FORMS": 1,
"draft-INITIAL_FORMS": 0,
"draft-0-document": "%s" % draft.docalias_set.first().pk,
"draft-0-revisions": '00',
"iprdocrel_set-TOTAL_FORMS": 2,
"iprdocrel_set-INITIAL_FORMS": 0,
"iprdocrel_set-0-document": "%s" % draft.docalias_set.first().pk,
"iprdocrel_set-0-revisions": '00',
"iprdocrel_set-1-document": DocAlias.objects.filter(name__startswith="rfc").first().pk,
"patent_info": "none",
"has_patent_pending": False,
"licensing": "royalty-free",
@ -319,13 +317,11 @@ class IprTests(TestCase):
"ietfer_name": "Test Participant",
"ietfer_contact_email": "test@ietfer.com",
"ietfer_contact_info": "555-555-0101",
"rfc-TOTAL_FORMS": 1,
"rfc-INITIAL_FORMS": 0,
"rfc-0-document": DocAlias.objects.filter(name__startswith="rfc").first().pk,
"draft-TOTAL_FORMS": 1,
"draft-INITIAL_FORMS": 0,
"draft-0-document": "%s" % draft.docalias_set.first().pk,
"draft-0-revisions": '00',
"iprdocrel_set-TOTAL_FORMS": 2,
"iprdocrel_set-INITIAL_FORMS": 0,
"iprdocrel_set-0-document": "%s" % draft.docalias_set.first().pk,
"iprdocrel_set-0-revisions": '00',
"iprdocrel_set-1-document": DocAlias.objects.filter(name__startswith="rfc").first().pk,
"patent_info": "none",
"has_patent_pending": False,
"licensing": "royalty-free",
@ -333,7 +329,6 @@ class IprTests(TestCase):
"submitter_email": "test@holder.com",
})
self.assertEqual(r.status_code, 200)
# print r.content
self.assertTrue("Your IPR disclosure has been submitted" in r.content)
iprs = IprDisclosureBase.objects.filter(title__icontains="belonging to Test Legal")
@ -357,13 +352,11 @@ class IprTests(TestCase):
"holder_contact_info": "555-555-0100",
"ietfer_name": "Test Participant",
"ietfer_contact_info": "555-555-0101",
"rfc-TOTAL_FORMS": 1,
"rfc-INITIAL_FORMS": 0,
"rfc-0-document": DocAlias.objects.filter(name__startswith="rfc").first().pk,
"draft-TOTAL_FORMS": 1,
"draft-INITIAL_FORMS": 0,
"draft-0-document": "%s" % draft.docalias_set.first().pk,
"draft-0-revisions": '00',
"iprdocrel_set-TOTAL_FORMS": 2,
"iprdocrel_set-INITIAL_FORMS": 0,
"iprdocrel_set-0-document": "%s" % draft.docalias_set.first().pk,
"iprdocrel_set-0-revisions": '00',
"iprdocrel_set-1-document": DocAlias.objects.filter(name__startswith="rfc").first().pk,
"patent_info": "none",
"has_patent_pending": False,
"licensing": "royalty-free",
@ -442,7 +435,7 @@ I would like to revoke this declaration.
r = self.client.get(url)
self.assertEqual(r.status_code,200)
q = PyQuery(r.content)
x = len(q('table#pending-iprs tr')) - 1 # minus header
x = len(q('table.ipr-table tbody tr'))
self.assertEqual(num,x)
def test_admin_removed(self):
@ -459,7 +452,7 @@ I would like to revoke this declaration.
r = self.client.get(url)
self.assertEqual(r.status_code,200)
q = PyQuery(r.content)
x = len(q('table#removed-iprs tr')) - 1 # minus header
x = len(q('table.ipr-table tbody tr'))
self.assertEqual(num,x)
def test_admin_parked(self):
@ -468,14 +461,11 @@ I would like to revoke this declaration.
def test_post(self):
make_test_data()
ipr = IprDisclosureBase.objects.get(title='Statement regarding rights')
url = urlreverse("ipr_post",kwargs={ "id": ipr.id })
# fail if not logged in
url = urlreverse("ipr_post", kwargs={ "id": ipr.id })
login_testing_unauthorized(self, "secretary", url)
r = self.client.get(url,follow=True)
self.assertTrue("Sign In" in r.content)
# successful post
self.client.login(username="secretary", password="secretary+password")
r = self.client.get(url,follow=True)
self.assertEqual(r.status_code,200)
self.assertEqual(r.status_code, 200)
ipr = IprDisclosureBase.objects.get(title='Statement regarding rights')
self.assertEqual(ipr.state.slug,'posted')
@ -518,4 +508,4 @@ Subject: test
""".format(data['reply_to'],datetime.datetime.now().ctime())
result = process_response_email(message_string)
self.assertIsInstance(result,Message)
self.assertFalse(event.response_past_due())
self.assertFalse(event.response_past_due())

View file

@ -10,8 +10,6 @@ urlpatterns = patterns('ietf.ipr.views',
url(r'^admin/$', RedirectView.as_view(url=reverse_lazy('ipr_admin',kwargs={'state':'pending'})),name="ipr_admin_main"),
url(r'^admin/(?P<state>pending|removed|parked)/$', 'admin', name='ipr_admin'),
url(r'^ajax/search/$', 'ajax_search', name='ipr_ajax_search'),
url(r'^ajax/draft-search/$', 'ajax_draft_search', name='ipr_ajax_draft_search'),
url(r'^ajax/rfc-search/$', 'ajax_rfc_search', name='ipr_ajax_rfc_search'),
(r'^by-draft/$', 'iprs_for_drafts_txt'),
url(r'^(?P<id>\d+)/$', 'show', name='ipr_show'),
url(r'^(?P<id>\d+)/addcomment/$', 'add_comment', name='ipr_add_comment'),

View file

@ -10,8 +10,7 @@ from django.db.models import Q
from django.forms.models import inlineformset_factory
from django.forms.formsets import formset_factory
from django.http import HttpResponse, Http404, HttpResponseRedirect
from django.shortcuts import render_to_response as render, get_object_or_404, redirect
from django.template import RequestContext
from django.shortcuts import render, get_object_or_404, redirect
from django.template.loader import render_to_string
from ietf.doc.models import DocAlias
@ -19,9 +18,9 @@ from ietf.group.models import Role, Group
from ietf.ietfauth.utils import role_required, has_role
from ietf.ipr.mail import (message_from_message, get_reply_to, get_update_submitter_emails,
get_update_cc_addrs)
from ietf.ipr.fields import tokeninput_id_name_json
from ietf.ipr.fields import select2_id_ipr_title_json
from ietf.ipr.forms import (HolderIprDisclosureForm, GenericDisclosureForm,
ThirdPartyIprDisclosureForm, DraftForm, RfcForm, SearchForm, MessageModelForm,
ThirdPartyIprDisclosureForm, DraftForm, SearchForm, MessageModelForm,
AddCommentForm, AddEmailForm, NotifyForm, StateForm, NonDocSpecificIprDisclosureForm,
GenericIprDisclosureForm)
from ietf.ipr.models import (IprDisclosureStateName, IprDisclosureBase,
@ -174,45 +173,13 @@ def ajax_search(request):
objs = objs.distinct()[:10]
return HttpResponse(tokeninput_id_name_json(objs), content_type='application/json')
def ajax_draft_search(request):
q = [w.strip() for w in request.GET.get('q', '').split() if w.strip()]
return HttpResponse(select2_id_ipr_title_json(objs), content_type='application/json')
if not q:
objs = DocAlias.objects.none()
else:
query = Q()
for t in q:
query &= Q(name__icontains=t)
objs = DocAlias.objects.filter(name__startswith='draft').filter(query)
objs = objs.distinct()[:10]
return HttpResponse(tokeninput_id_name_json(objs), content_type='application/json')
def ajax_rfc_search(request):
# expects one numeric term
q = [w.strip() for w in request.GET.get('q', '').split() if w.strip()]
if not q:
objs = DocAlias.objects.none()
else:
query = Q()
query &= Q(name__startswith='rfc%s' % q[0])
objs = DocAlias.objects.filter(query)
objs = objs.distinct()[:10]
return HttpResponse(tokeninput_id_name_json(objs), content_type='application/json')
# ----------------------------------------------------------------
# Views
# ----------------------------------------------------------------
def about(request):
return render("ipr/disclosure.html", {}, context_instance=RequestContext(request))
return render(request, "ipr/disclosure.html", {})
@role_required('Secretariat',)
def add_comment(request, id):
@ -239,8 +206,7 @@ def add_comment(request, id):
else:
form = AddCommentForm()
return render('ipr/add_comment.html',dict(ipr=ipr,form=form),
context_instance=RequestContext(request))
return render(request, 'ipr/add_comment.html',dict(ipr=ipr,form=form))
@role_required('Secretariat',)
def add_email(request, id):
@ -276,29 +242,31 @@ def add_email(request, id):
else:
form = AddEmailForm(ipr=ipr)
return render('ipr/add_email.html',dict(ipr=ipr,form=form),
context_instance=RequestContext(request))
return render(request, 'ipr/add_email.html',dict(ipr=ipr,form=form))
@role_required('Secretariat',)
def admin(request,state):
def admin(request, state):
"""Administrative disclosure listing. For non-posted disclosures"""
if state == 'removed':
states = ('removed','rejected')
else:
states = [state]
states = IprDisclosureStateName.objects.filter(slug__in=[state, "rejected"] if state == "removed" else [state])
if not states:
raise Http404
iprs = IprDisclosureBase.objects.filter(state__in=states).order_by('-time')
tabs = [('Pending','pending',urlreverse('ipr_admin',kwargs={'state':'pending'}),True),
('Removed','removed',urlreverse('ipr_admin',kwargs={'state':'removed'}),True),
('Parked','parked',urlreverse('ipr_admin',kwargs={'state':'parked'}),True)]
template = 'ipr/admin_' + state + '.html'
return render(template, {
tabs = [
t + (t[0].lower() == state.lower(),)
for t in [
('Pending', urlreverse('ipr_admin', kwargs={'state':'pending'})),
('Removed', urlreverse('ipr_admin', kwargs={'state':'removed'})),
('Parked', urlreverse('ipr_admin', kwargs={'state':'parked'})),
]]
return render(request, 'ipr/admin_list.html', {
'iprs': iprs,
'tabs': tabs,
'selected': state},
context_instance=RequestContext(request)
)
'states': states,
'administrative_list': state,
})
@role_required('Secretariat',)
def edit(request, id, updates=None):
@ -306,35 +274,23 @@ def edit(request, id, updates=None):
ipr = get_object_or_404(IprDisclosureBase, id=id).get_child()
type = class_to_type[ipr.__class__.__name__]
# only include extra when initial formset is empty
if ipr.iprdocrel_set.filter(document__name__startswith='draft'):
draft_extra = 0
else:
draft_extra = 1
if ipr.iprdocrel_set.filter(document__name__startswith='rfc'):
rfc_extra = 0
else:
rfc_extra = 1
DraftFormset = inlineformset_factory(IprDisclosureBase, IprDocRel, form=DraftForm, can_delete=True, extra=draft_extra)
RfcFormset = inlineformset_factory(IprDisclosureBase, IprDocRel, form=RfcForm, can_delete=True, extra=rfc_extra)
DraftFormset = inlineformset_factory(IprDisclosureBase, IprDocRel, form=DraftForm, can_delete=True, extra=1)
if request.method == 'POST':
form = ipr_form_mapping[ipr.__class__.__name__](request.POST,instance=ipr)
if not type == 'generic':
draft_formset = DraftFormset(request.POST, instance=ipr, prefix='draft')
rfc_formset = RfcFormset(request.POST, instance=ipr, prefix='rfc')
if type != 'generic':
draft_formset = DraftFormset(request.POST, instance=ipr)
else:
draft_formset = None
rfc_formset = None
if request.user.is_anonymous():
person = Person.objects.get(name="(System)")
else:
person = request.user.person
# check formset validity
if not type == 'generic':
valid_formsets = draft_formset.is_valid() and rfc_formset.is_valid()
if type != 'generic':
valid_formsets = draft_formset.is_valid()
else:
valid_formsets = True
@ -343,13 +299,9 @@ def edit(request, id, updates=None):
disclosure = form.save(commit=False)
disclosure.save()
if not type == 'generic':
# clear and recreate IprDocRels
# IprDocRel.objects.filter(disclosure=ipr).delete()
draft_formset = DraftFormset(request.POST, instance=disclosure, prefix='draft')
if type != 'generic':
draft_formset = DraftFormset(request.POST, instance=disclosure)
draft_formset.save()
rfc_formset = RfcFormset(request.POST, instance=disclosure, prefix='rfc')
rfc_formset.save()
set_disclosure_title(disclosure)
disclosure.save()
@ -369,28 +321,20 @@ def edit(request, id, updates=None):
messages.success(request,'Disclosure modified')
return redirect("ipr_show", id=ipr.id)
else:
# assert False, form.errors
pass
else:
if ipr.updates:
form = ipr_form_mapping[ipr.__class__.__name__](instance=ipr,initial={'updates':[ x.target for x in ipr.updates ]})
else:
form = ipr_form_mapping[ipr.__class__.__name__](instance=ipr)
#disclosure = IprDisclosureBase() # dummy disclosure for inlineformset
dqs=IprDocRel.objects.filter(document__name__startswith='draft')
rqs=IprDocRel.objects.filter(document__name__startswith='rfc')
draft_formset = DraftFormset(instance=ipr, prefix='draft',queryset=dqs)
rfc_formset = RfcFormset(instance=ipr, prefix='rfc',queryset=rqs)
return render("ipr/details_edit.html", {
draft_formset = DraftFormset(instance=ipr, queryset=IprDocRel.objects.all())
return render(request, "ipr/details_edit.html", {
'form': form,
'draft_formset':draft_formset,
'rfc_formset':rfc_formset,
'type':type},
context_instance=RequestContext(request)
)
'type':type
})
@role_required('Secretariat',)
def email(request, id):
@ -441,11 +385,10 @@ def email(request, id):
}
form = MessageModelForm(initial=initial)
return render("ipr/email.html", {
return render(request, "ipr/email.html", {
'ipr': ipr,
'form':form},
context_instance=RequestContext(request)
)
'form':form
})
def history(request, id):
"""Show the history for a specific IPR disclosure"""
@ -454,16 +397,12 @@ def history(request, id):
if not has_role(request.user, "Secretariat"):
events = events.exclude(type='private_comment')
tabs = [('Disclosure','disclosure',urlreverse('ipr_show',kwargs={'id':id}),True),
('History','history',urlreverse('ipr_history',kwargs={'id':id}),True)]
return render("ipr/details_history.html", {
return render(request, "ipr/details_history.html", {
'events':events,
'ipr': ipr,
'tabs':tabs,
'selected':'history'},
context_instance=RequestContext(request)
)
'tabs': get_details_tabs(ipr, 'History'),
'selected_tab_entry':'history'
})
def iprs_for_drafts_txt(request):
docipr = {}
@ -487,27 +426,25 @@ def iprs_for_drafts_txt(request):
def new(request, type, updates=None):
"""Submit a new IPR Disclosure. If the updates field != None, this disclosure
updates one or more other disclosures."""
DraftFormset = inlineformset_factory(IprDisclosureBase, IprDocRel, form=DraftForm, can_delete=False, extra=1)
RfcFormset = inlineformset_factory(IprDisclosureBase, IprDocRel, form=RfcForm, can_delete=False, extra=1)
# 1 to show initially + the template
DraftFormset = inlineformset_factory(IprDisclosureBase, IprDocRel, form=DraftForm, can_delete=False, extra=1 + 1)
if request.method == 'POST':
form = ipr_form_mapping[type](request.POST)
if not type == 'generic':
draft_formset = DraftFormset(request.POST, instance=IprDisclosureBase(), prefix='draft')
rfc_formset = RfcFormset(request.POST, instance=IprDisclosureBase(), prefix='rfc')
if type != 'generic':
draft_formset = DraftFormset(request.POST, instance=IprDisclosureBase())
else:
draft_formset = None
rfc_formset = None
if request.user.is_anonymous():
person = Person.objects.get(name="(System)")
else:
person = request.user.person
# check formset validity
if not type == 'generic':
valid_formsets = draft_formset.is_valid() and rfc_formset.is_valid()
if type != 'generic':
valid_formsets = draft_formset.is_valid()
else:
valid_formsets = True
@ -518,11 +455,9 @@ def new(request, type, updates=None):
disclosure.state = IprDisclosureStateName.objects.get(slug='pending')
disclosure.save()
if not type == 'generic':
draft_formset = DraftFormset(request.POST, instance=disclosure, prefix='draft')
if type != 'generic':
draft_formset = DraftFormset(request.POST, instance=disclosure)
draft_formset.save()
rfc_formset = RfcFormset(request.POST, instance=disclosure, prefix='rfc')
rfc_formset.save()
set_disclosure_title(disclosure)
disclosure.save()
@ -544,7 +479,7 @@ def new(request, type, updates=None):
"ipr/new_update_email.txt",
{"ipr": disclosure,})
return render("ipr/submitted.html", context_instance=RequestContext(request))
return render(request, "ipr/submitted.html")
else:
if updates:
@ -552,16 +487,13 @@ def new(request, type, updates=None):
else:
form = ipr_form_mapping[type]()
disclosure = IprDisclosureBase() # dummy disclosure for inlineformset
draft_formset = DraftFormset(instance=disclosure, prefix='draft')
rfc_formset = RfcFormset(instance=disclosure, prefix='rfc')
return render("ipr/details_edit.html", {
draft_formset = DraftFormset(instance=disclosure)
return render(request, "ipr/details_edit.html", {
'form': form,
'draft_formset':draft_formset,
'rfc_formset':rfc_formset,
'type':type},
context_instance=RequestContext(request)
)
'type':type,
})
@role_required('Secretariat',)
def notify(request, id, type):
@ -597,11 +529,10 @@ def notify(request, id, type):
initial = [ {'type':'msgout','text':m} for m in get_posted_emails(ipr) ]
formset = NotifyFormset(initial=initial)
return render("ipr/notify.html", {
return render(request, "ipr/notify.html", {
'formset': formset,
'ipr': ipr},
context_instance=RequestContext(request)
)
'ipr': ipr,
})
@role_required('Secretariat',)
def post(request, id):
@ -731,44 +662,47 @@ def search(request):
iprs = sorted(iprs, key=lambda x: x.state.order)
else:
iprs = sorted(iprs, key=lambda x: (x.time, x.id), reverse=True)
return render(template, {
return render(request, template, {
"q": q,
"iprs": iprs,
"docs": docs,
"doc": doc,
"form":form,
"states":states},
context_instance=RequestContext(request)
)
"states":states
})
return HttpResponseRedirect(request.path)
else:
form = SearchForm(initial={'state':['all']})
return render("ipr/search.html", {"form":form }, context_instance=RequestContext(request))
return render(request, "ipr/search.html", {"form":form })
def get_details_tabs(ipr, selected):
return [
t + (t[0].lower() == selected.lower(),)
for t in [
('Disclosure', urlreverse('ipr_show', kwargs={ 'id': ipr.pk })),
('History', urlreverse('ipr_history', kwargs={ 'id': ipr.pk }))
]]
def show(request, id):
"""View of individual declaration"""
ipr = get_object_or_404(IprDisclosureBase, id=id).get_child()
if not has_role(request.user, 'Secretariat'):
if ipr.state.slug == 'removed':
return render("ipr/removed.html", {
'ipr': ipr},
context_instance=RequestContext(request)
)
return render(request, "ipr/removed.html", {
'ipr': ipr
})
elif ipr.state.slug != 'posted':
raise Http404
tabs = [('Disclosure','disclosure',urlreverse('ipr_show',kwargs={'id':id}),True),
('History','history',urlreverse('ipr_history',kwargs={'id':id}),True)]
return render("ipr/details_view.html", {
return render(request, "ipr/details_view.html", {
'ipr': ipr,
'tabs':tabs,
'selected':'disclosure'},
context_instance=RequestContext(request)
)
'tabs': get_details_tabs(ipr, 'Disclosure'),
'updates_iprs': ipr.relatedipr_source_set.all(),
'updated_by_iprs': ipr.relatedipr_target_set.filter(source__state="posted")
})
def showlist(request):
"""List all disclosures by type, posted only"""
@ -781,12 +715,11 @@ def showlist(request):
generic = itertools.chain(generic,nondocspecific)
generic = sorted(generic, key=lambda x: x.time,reverse=True)
return render("ipr/list.html", {
return render(request, "ipr/list.html", {
'generic_disclosures' : generic,
'specific_disclosures': specific,
'thirdpty_disclosures': thirdpty},
context_instance=RequestContext(request)
)
'thirdpty_disclosures': thirdpty,
})
@role_required('Secretariat',)
def state(request, id):
@ -822,8 +755,7 @@ def state(request, id):
else:
form = StateForm(initial={'state':ipr.state.pk,'private':True})
return render('ipr/state.html',dict(ipr=ipr,form=form),
context_instance=RequestContext(request))
return render(request, 'ipr/state.html', dict(ipr=ipr, form=form))
# use for link to update specific IPR
def update(request, id):
@ -832,4 +764,4 @@ def update(request, id):
ipr = get_object_or_404(IprDisclosureBase,id=id)
child = ipr.get_child()
type = class_to_type[child.__class__.__name__]
return new(request, type, updates=id)
return new(request, type, updates=id)

50
ietf/liaisons/fields.py Normal file
View file

@ -0,0 +1,50 @@
import json
from django.utils.html import escape
from django import forms
from django.core.urlresolvers import reverse as urlreverse
from ietf.liaisons.models import LiaisonStatement
def select2_id_liaison_json(objs):
return json.dumps([{ "id": o.pk, "text": escape(o.title) } for o in objs])
class SearchableLiaisonStatementField(forms.IntegerField):
"""Server-based multi-select field for choosing liaison statements using
select2.js."""
def __init__(self, hint_text="Type in title to search for document", *args, **kwargs):
super(SearchableLiaisonStatementField, self).__init__(*args, **kwargs)
self.widget.attrs["class"] = "select2-field"
self.widget.attrs["data-placeholder"] = hint_text
self.widget.attrs["data-max-entries"] = 1
def prepare_value(self, value):
if not value:
value = None
elif isinstance(value, LiaisonStatement):
value = value
else:
value = LiaisonStatement.objects.exclude(approved=None).filter(pk=value).first()
self.widget.attrs["data-pre"] = select2_id_liaison_json([value] if value else [])
# doing this in the constructor is difficult because the URL
# patterns may not have been fully constructed there yet
self.widget.attrs["data-ajax-url"] = urlreverse("ajax_select2_search_liaison_statements")
return value
def clean(self, value):
value = super(SearchableLiaisonStatementField, self).clean(value)
if value == None:
return None
obj = LiaisonStatement.objects.filter(pk=value).first()
if not obj and self.required:
raise forms.ValidationError(u"You must select a value.")
return obj

View file

@ -13,11 +13,13 @@ from ietf.liaisons.accounts import (can_add_outgoing_liaison, can_add_incoming_l
get_person_for_user, is_secretariat, is_sdo_liaison_manager)
from ietf.liaisons.utils import IETFHM
from ietf.liaisons.widgets import (FromWidget, ReadOnlyWidget, ButtonWidget,
ShowAttachmentsWidget, RelatedLiaisonWidget)
ShowAttachmentsWidget)
from ietf.liaisons.models import LiaisonStatement, LiaisonStatementPurposeName
from ietf.liaisons.fields import SearchableLiaisonStatementField
from ietf.group.models import Group, Role
from ietf.person.models import Person, Email
from ietf.doc.models import Document
from ietf.utils.fields import DatepickerDateField
class LiaisonForm(forms.Form):
@ -28,10 +30,11 @@ class LiaisonForm(forms.Form):
to_poc = forms.CharField(widget=ReadOnlyWidget, label="POC", required=False)
response_contact = forms.CharField(required=False, max_length=255)
technical_contact = forms.CharField(required=False, max_length=255)
cc1 = forms.CharField(widget=forms.Textarea, label="CC", required=False, help_text='Please insert one email address per line')
cc1 = forms.CharField(widget=forms.Textarea, label="CC", required=False, help_text='Please insert one email address per line.')
purpose = forms.ChoiceField()
deadline_date = forms.DateField(label='Deadline')
submitted_date = forms.DateField(label='Submission date', initial=datetime.date.today())
related_to = SearchableLiaisonStatementField(label=u'Related Liaison Statement', required=False)
deadline_date = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1" }, label='Deadline', required=True)
submitted_date = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1" }, label='Submission date', required=True, initial=datetime.date.today())
title = forms.CharField(label=u'Title')
body = forms.CharField(widget=forms.Textarea, required=False)
attachments = forms.CharField(label='Attachments', widget=ShowAttachmentsWidget, required=False)
@ -42,13 +45,12 @@ class LiaisonForm(forms.Form):
require=['id_attach_title', 'id_attach_file'],
required_label='title and file'),
required=False)
related_to = forms.ModelChoiceField(LiaisonStatement.objects.all(), label=u'Related Liaison', widget=RelatedLiaisonWidget, required=False)
fieldsets = [('From', ('from_field', 'replyto')),
('To', ('organization', 'to_poc')),
('Other email addresses', ('response_contact', 'technical_contact', 'cc1')),
('Purpose', ('purpose', 'deadline_date')),
('References', ('related_to', )),
('Reference', ('related_to', )),
('Liaison Statement', ('title', 'submitted_date', 'body', 'attachments')),
('Add attachment', ('attach_title', 'attach_file', 'attach_button')),
]
@ -82,7 +84,7 @@ class LiaisonForm(forms.Form):
self.initial["title"] = self.instance.title
self.initial["body"] = self.instance.body
self.initial["attachments"] = self.instance.attachments.all()
self.initial["related_to"] = self.instance.related_to_id
self.initial["related_to"] = self.instance.related_to
if "approved" in self.fields:
self.initial["approved"] = bool(self.instance.approved)

View file

@ -0,0 +1,264 @@
# -*- coding: utf-8 -*-
from south.v2 import SchemaMigration
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'LiaisonStatement'
# db.create_table(u'liaisons_liaisonstatement', (
# (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
# ('title', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)),
# ('purpose', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['name.LiaisonStatementPurposeName'])),
# ('body', self.gf('django.db.models.fields.TextField')(blank=True)),
# ('deadline', self.gf('django.db.models.fields.DateField')(null=True, blank=True)),
# ('related_to', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['liaisons.LiaisonStatement'], null=True, blank=True)),
# ('from_group', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='liaisonstatement_from_set', null=True, to=orm['group.Group'])),
# ('from_name', self.gf('django.db.models.fields.CharField')(max_length=255)),
# ('from_contact', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['person.Email'], null=True, blank=True)),
# ('to_group', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='liaisonstatement_to_set', null=True, to=orm['group.Group'])),
# ('to_name', self.gf('django.db.models.fields.CharField')(max_length=255)),
# ('to_contact', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)),
# ('reply_to', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)),
# ('response_contact', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)),
# ('technical_contact', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)),
# ('cc', self.gf('django.db.models.fields.TextField')(blank=True)),
# ('submitted', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)),
# ('modified', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)),
# ('approved', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)),
# ('action_taken', self.gf('django.db.models.fields.BooleanField')(default=False)),
# ))
# db.send_create_signal(u'liaisons', ['LiaisonStatement'])
# # Adding M2M table for field attachments on 'LiaisonStatement'
# m2m_table_name = db.shorten_name(u'liaisons_liaisonstatement_attachments')
# db.create_table(m2m_table_name, (
# ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
# ('liaisonstatement', models.ForeignKey(orm[u'liaisons.liaisonstatement'], null=False)),
# ('document', models.ForeignKey(orm[u'doc.document'], null=False))
# ))
# db.create_unique(m2m_table_name, ['liaisonstatement_id', 'document_id'])
pass
def backwards(self, orm):
# # Deleting model 'LiaisonStatement'
# db.delete_table(u'liaisons_liaisonstatement')
# # Removing M2M table for field attachments on 'LiaisonStatement'
# db.delete_table(db.shorten_name(u'liaisons_liaisonstatement_attachments'))
pass
models = {
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'})
},
u'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
u'doc.document': {
'Meta': {'object_name': 'Document'},
'abstract': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'ad': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'ad_document_set'", 'null': 'True', 'to': u"orm['person.Person']"}),
'authors': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['person.Email']", 'symmetrical': 'False', 'through': u"orm['doc.DocumentAuthor']", 'blank': 'True'}),
'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'external_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
'group': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['group.Group']", 'null': 'True', 'blank': 'True'}),
'intended_std_level': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.IntendedStdLevelName']", 'null': 'True', 'blank': 'True'}),
'internal_comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
'note': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'notify': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '1', 'blank': 'True'}),
'pages': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
'rev': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}),
'shepherd': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'shepherd_document_set'", 'null': 'True', 'to': u"orm['person.Email']"}),
'states': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['doc.State']", 'symmetrical': 'False', 'blank': 'True'}),
'std_level': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.StdLevelName']", 'null': 'True', 'blank': 'True'}),
'stream': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.StreamName']", 'null': 'True', 'blank': 'True'}),
'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['name.DocTagName']", 'null': 'True', 'blank': 'True'}),
'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.DocTypeName']", 'null': 'True', 'blank': 'True'})
},
u'doc.documentauthor': {
'Meta': {'ordering': "['document', 'order']", 'object_name': 'DocumentAuthor'},
'author': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['person.Email']"}),
'document': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['doc.Document']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '1'})
},
u'doc.state': {
'Meta': {'ordering': "['type', 'order']", 'object_name': 'State'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'next_states': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'previous_states'", 'blank': 'True', 'to': u"orm['doc.State']"}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}),
'type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['doc.StateType']"}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'doc.statetype': {
'Meta': {'object_name': 'StateType'},
'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '30', 'primary_key': 'True'})
},
u'group.group': {
'Meta': {'object_name': 'Group'},
'acronym': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '40'}),
'ad': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['person.Person']", 'null': 'True', 'blank': 'True'}),
'charter': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'chartered_group'", 'unique': 'True', 'null': 'True', 'to': u"orm['doc.Document']"}),
'comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'list_archive': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'list_email': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
'list_subscribe': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}),
'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['group.Group']", 'null': 'True', 'blank': 'True'}),
'state': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.GroupStateName']", 'null': 'True'}),
'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.GroupTypeName']", 'null': 'True'}),
'unused_states': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['doc.State']", 'symmetrical': 'False', 'blank': 'True'}),
'unused_tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['name.DocTagName']", 'symmetrical': 'False', 'blank': 'True'})
},
u'liaisons.liaisonstatement': {
'Meta': {'object_name': 'LiaisonStatement'},
'action_taken': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'approved': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'attachments': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['doc.Document']", 'symmetrical': 'False', 'blank': 'True'}),
'body': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'cc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'deadline': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
'from_contact': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['person.Email']", 'null': 'True', 'blank': 'True'}),
'from_group': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'liaisonstatement_from_set'", 'null': 'True', 'to': u"orm['group.Group']"}),
'from_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'purpose': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.LiaisonStatementPurposeName']"}),
'related_to': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['liaisons.LiaisonStatement']", 'null': 'True', 'blank': 'True'}),
'reply_to': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'response_contact': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'submitted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'technical_contact': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'to_contact': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'to_group': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'liaisonstatement_to_set'", 'null': 'True', 'to': u"orm['group.Group']"}),
'to_name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
u'name.doctagname': {
'Meta': {'ordering': "['order']", 'object_name': 'DocTagName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '32', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.doctypename': {
'Meta': {'ordering': "['order']", 'object_name': 'DocTypeName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '32', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.groupstatename': {
'Meta': {'ordering': "['order']", 'object_name': 'GroupStateName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '32', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.grouptypename': {
'Meta': {'ordering': "['order']", 'object_name': 'GroupTypeName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '32', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.intendedstdlevelname': {
'Meta': {'ordering': "['order']", 'object_name': 'IntendedStdLevelName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '32', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.liaisonstatementpurposename': {
'Meta': {'ordering': "['order']", 'object_name': 'LiaisonStatementPurposeName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '32', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.stdlevelname': {
'Meta': {'ordering': "['order']", 'object_name': 'StdLevelName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '32', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.streamname': {
'Meta': {'ordering': "['order']", 'object_name': 'StreamName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '32', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'person.email': {
'Meta': {'object_name': 'Email'},
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'address': ('django.db.models.fields.CharField', [], {'max_length': '64', 'primary_key': 'True'}),
'person': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['person.Person']", 'null': 'True'}),
'time': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
},
u'person.person': {
'Meta': {'object_name': 'Person'},
'address': ('django.db.models.fields.TextField', [], {'max_length': '255', 'blank': 'True'}),
'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'ascii': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'ascii_short': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True', 'null': 'True', 'blank': 'True'})
}
}
complete_apps = ['liaisons']

View file

@ -0,0 +1,229 @@
# -*- coding: utf-8 -*-
from south.v2 import DataMigration
class Migration(DataMigration):
def forwards(self, orm):
for l in orm.LiaisonStatement.objects.filter(title=""):
a = l.attachments.all().first()
if a:
l.title = a.title
l.save()
def backwards(self, orm):
pass
models = {
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'})
},
u'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
u'doc.document': {
'Meta': {'object_name': 'Document'},
'abstract': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'ad': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'ad_document_set'", 'null': 'True', 'to': u"orm['person.Person']"}),
'authors': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['person.Email']", 'symmetrical': 'False', 'through': u"orm['doc.DocumentAuthor']", 'blank': 'True'}),
'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'external_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
'group': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['group.Group']", 'null': 'True', 'blank': 'True'}),
'intended_std_level': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.IntendedStdLevelName']", 'null': 'True', 'blank': 'True'}),
'internal_comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}),
'note': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'notify': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '1', 'blank': 'True'}),
'pages': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
'rev': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}),
'shepherd': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'shepherd_document_set'", 'null': 'True', 'to': u"orm['person.Email']"}),
'states': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['doc.State']", 'symmetrical': 'False', 'blank': 'True'}),
'std_level': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.StdLevelName']", 'null': 'True', 'blank': 'True'}),
'stream': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.StreamName']", 'null': 'True', 'blank': 'True'}),
'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['name.DocTagName']", 'null': 'True', 'blank': 'True'}),
'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.DocTypeName']", 'null': 'True', 'blank': 'True'})
},
u'doc.documentauthor': {
'Meta': {'ordering': "['document', 'order']", 'object_name': 'DocumentAuthor'},
'author': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['person.Email']"}),
'document': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['doc.Document']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '1'})
},
u'doc.state': {
'Meta': {'ordering': "['type', 'order']", 'object_name': 'State'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'next_states': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'previous_states'", 'blank': 'True', 'to': u"orm['doc.State']"}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}),
'type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['doc.StateType']"}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'doc.statetype': {
'Meta': {'object_name': 'StateType'},
'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '30', 'primary_key': 'True'})
},
u'group.group': {
'Meta': {'object_name': 'Group'},
'acronym': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '40'}),
'ad': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['person.Person']", 'null': 'True', 'blank': 'True'}),
'charter': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'chartered_group'", 'unique': 'True', 'null': 'True', 'to': u"orm['doc.Document']"}),
'comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'list_archive': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'list_email': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
'list_subscribe': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}),
'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['group.Group']", 'null': 'True', 'blank': 'True'}),
'state': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.GroupStateName']", 'null': 'True'}),
'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.GroupTypeName']", 'null': 'True'}),
'unused_states': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['doc.State']", 'symmetrical': 'False', 'blank': 'True'}),
'unused_tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['name.DocTagName']", 'symmetrical': 'False', 'blank': 'True'})
},
u'liaisons.liaisonstatement': {
'Meta': {'object_name': 'LiaisonStatement'},
'action_taken': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'approved': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'attachments': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['doc.Document']", 'symmetrical': 'False', 'blank': 'True'}),
'body': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'cc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'deadline': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
'from_contact': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['person.Email']", 'null': 'True', 'blank': 'True'}),
'from_group': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'liaisonstatement_from_set'", 'null': 'True', 'to': u"orm['group.Group']"}),
'from_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'purpose': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.LiaisonStatementPurposeName']"}),
'related_to': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['liaisons.LiaisonStatement']", 'null': 'True', 'blank': 'True'}),
'reply_to': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'response_contact': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'submitted': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'technical_contact': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'to_contact': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'to_group': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'liaisonstatement_to_set'", 'null': 'True', 'to': u"orm['group.Group']"}),
'to_name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
u'name.doctagname': {
'Meta': {'ordering': "['order']", 'object_name': 'DocTagName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '32', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.doctypename': {
'Meta': {'ordering': "['order']", 'object_name': 'DocTypeName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '32', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.groupstatename': {
'Meta': {'ordering': "['order']", 'object_name': 'GroupStateName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '32', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.grouptypename': {
'Meta': {'ordering': "['order']", 'object_name': 'GroupTypeName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '32', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.intendedstdlevelname': {
'Meta': {'ordering': "['order']", 'object_name': 'IntendedStdLevelName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '32', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.liaisonstatementpurposename': {
'Meta': {'ordering': "['order']", 'object_name': 'LiaisonStatementPurposeName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '32', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.stdlevelname': {
'Meta': {'ordering': "['order']", 'object_name': 'StdLevelName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '32', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'name.streamname': {
'Meta': {'ordering': "['order']", 'object_name': 'StreamName'},
'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'slug': ('django.db.models.fields.CharField', [], {'max_length': '32', 'primary_key': 'True'}),
'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
u'person.email': {
'Meta': {'object_name': 'Email'},
'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'address': ('django.db.models.fields.CharField', [], {'max_length': '64', 'primary_key': 'True'}),
'person': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['person.Person']", 'null': 'True'}),
'time': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
},
u'person.person': {
'Meta': {'object_name': 'Person'},
'address': ('django.db.models.fields.TextField', [], {'max_length': '255', 'blank': 'True'}),
'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'ascii': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'ascii_short': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True', 'null': 'True', 'blank': 'True'})
}
}
complete_apps = ['liaisons']
symmetrical = True

View file

View file

@ -16,12 +16,12 @@ class LiaisonStatement(models.Model):
related_to = models.ForeignKey('LiaisonStatement', blank=True, null=True)
from_group = models.ForeignKey(Group, related_name="liaisonstatement_from_set", null=True, blank=True, help_text="Sender group, if it exists")
from_name = models.CharField(max_length=255, help_text="Name of the sender body")
from_group = models.ForeignKey(Group, related_name="liaisonstatement_from_set", null=True, blank=True, help_text="Sender group, if it exists.")
from_name = models.CharField(max_length=255, help_text="Name of the sender body.")
from_contact = models.ForeignKey(Email, blank=True, null=True)
to_group = models.ForeignKey(Group, related_name="liaisonstatement_to_set", null=True, blank=True, help_text="Recipient group, if it exists")
to_name = models.CharField(max_length=255, help_text="Name of the recipient body")
to_contact = models.CharField(blank=True, max_length=255, help_text="Contacts at recipient body")
to_group = models.ForeignKey(Group, related_name="liaisonstatement_to_set", null=True, blank=True, help_text="Recipient group, if it exists.")
to_name = models.CharField(max_length=255, help_text="Name of the recipient body.")
to_contact = models.CharField(blank=True, max_length=255, help_text="Contacts at recipient body.")
reply_to = models.CharField(blank=True, max_length=255)
@ -49,4 +49,4 @@ class LiaisonStatement(models.Model):
return slugify("liaison" + " " + self.submitted.strftime("%Y-%m-%d") + " " + frm[:50] + " " + to[:50] + " " + self.title[:115])
def __unicode__(self):
return self.title or u"<no title>"
return self.title

View file

@ -5,7 +5,7 @@ from django.views.generic import RedirectView, TemplateView
urlpatterns = patterns('',
(r'^help/$', TemplateView.as_view(template_name='liaisons/help.html')),
(r'^help/fields/$', TemplateView.as_view(template_name='liaisons/field_help.html')),
url(r'^help/fields/$', TemplateView.as_view(template_name='liaisons/field_help.html'), name="liaisons_field_help"),
(r'^help/from_ietf/$', TemplateView.as_view(template_name='liaisons/guide_from_ietf.html')),
(r'^help/to_ietf/$', TemplateView.as_view(template_name='liaisons/guide_to_ietf.html')),
(r'^managers/$', RedirectView.as_view(url='http://www.ietf.org/liaison/managers.html')),
@ -18,6 +18,6 @@ urlpatterns += patterns('ietf.liaisons.views',
url(r'^for_approval/$', 'liaison_approval_list', name='liaison_approval_list'),
url(r'^for_approval/(?P<object_id>\d+)/$', 'liaison_approval_detail', name='liaison_approval_detail'),
url(r'^add/$', 'add_liaison', name='add_liaison'),
url(r'^ajax/get_info/$', 'get_info'),
url(r'^ajax/liaison_list/$', 'ajax_liaison_list', name='ajax_liaison_list'),
url(r'^ajax/get_info/$', 'ajax_get_liaison_info'),
url(r'^ajax/select2search/$', 'ajax_select2_search_liaison_statements', name='ajax_select2_search_liaison_statements'),
)

View file

@ -16,6 +16,7 @@ from ietf.liaisons.accounts import (get_person_for_user, can_add_outgoing_liaiso
from ietf.liaisons.forms import liaison_form_factory
from ietf.liaisons.utils import IETFHM, can_submit_liaison_required, approvable_liaison_statements
from ietf.liaisons.mails import notify_pending_by_email, send_liaison_by_email
from ietf.liaisons.fields import select2_id_liaison_json
@ -44,7 +45,7 @@ def add_liaison(request, liaison=None):
@can_submit_liaison_required
def get_info(request):
def ajax_get_liaison_info(request):
person = get_person_for_user(request.user)
to_entity_id = request.GET.get('to_entity_id', None)
@ -110,14 +111,20 @@ def liaison_list(request):
"sort": sort,
}, context_instance=RequestContext(request))
def ajax_liaison_list(request):
sort, order_by = normalize_sort(request)
liaisons = LiaisonStatement.objects.exclude(approved=None).order_by(order_by)
def ajax_select2_search_liaison_statements(request):
q = [w.strip() for w in request.GET.get('q', '').split() if w.strip()]
return render_to_response('liaisons/liaison_table.html', {
"liaisons": liaisons,
"sort": sort,
}, context_instance=RequestContext(request))
if not q:
objs = LiaisonStatement.objects.none()
else:
qs = LiaisonStatement.objects.exclude(approved=None).all()
for t in q:
qs = qs.filter(title__icontains=t)
objs = qs.distinct().order_by("-id")[:20]
return HttpResponse(select2_id_liaison_json(objs), content_type='application/json')
@can_submit_liaison_required
def liaison_approval_list(request):

View file

@ -1,12 +1,9 @@
from django.conf import settings
from django.core.urlresolvers import reverse as urlreverse
from django.db.models.query import QuerySet
from django.forms.widgets import Select, Widget, TextInput
from django.forms.widgets import Select, Widget
from django.utils.safestring import mark_safe
from django.utils.html import conditional_escape
from ietf.liaisons.models import LiaisonStatement
class FromWidget(Select):
@ -28,7 +25,7 @@ class FromWidget(Select):
value = option[0]
text = option[1]
base = u'<input type="hidden" value="%s" id="id_%s" name="%s" />%s' % (conditional_escape(value), conditional_escape(name), conditional_escape(name), conditional_escape(text))
base += u' (<a class="from_mailto" href="">' + conditional_escape(self.submitter) + u'</a>)'
base += u' <a class="from_mailto form-control" href="">' + conditional_escape(self.submitter) + u'</a>'
if self.full_power_on:
base += '<div style="display: none;" class="reducedToOptions">'
for from_code in self.full_power_on:
@ -40,9 +37,8 @@ class FromWidget(Select):
class ReadOnlyWidget(Widget):
def render(self, name, value, attrs=None):
html = u'<div id="id_%s">%s</div>' % (conditional_escape(name), conditional_escape(value or ''))
html = u'<div id="id_%s" class="form-control" style="height: auto; min-height: 34px;">%s</div>' % (conditional_escape(name), conditional_escape(value or ''))
return mark_safe(html)
@ -63,7 +59,7 @@ class ButtonWidget(Widget):
html += u'<span style="display: none" class="attachRequiredField">%s</span>' % conditional_escape(i)
required_str = u'Please fill in %s to attach a new file' % conditional_escape(self.required_label)
html += u'<span style="display: none" class="attachDisabledLabel">%s</span>' % conditional_escape(required_str)
html += u'<input type="button" class="addAttachmentWidget" value="%s" />' % conditional_escape(self.label)
html += u'<input type="button" class="addAttachmentWidget btn btn-primary btn-sm" value="%s" />' % conditional_escape(self.label)
return mark_safe(html)
@ -71,8 +67,8 @@ class ShowAttachmentsWidget(Widget):
def render(self, name, value, attrs=None):
html = u'<div id="id_%s">' % name
html += u'<span style="display: none" class="showAttachmentsEmpty">No files attached</span>'
html += u'<div class="attachedFiles">'
html += u'<span style="display: none" class="showAttachmentsEmpty form-control" style="height: auto; min-height: 34px;">No files attached</span>'
html += u'<div class="attachedFiles form-control" style="height: auto; min-height: 34px;">'
if value and isinstance(value, QuerySet):
for attachment in value:
html += u'<a class="initialAttach" href="%s%s">%s</a><br />' % (settings.LIAISON_ATTACH_URL, conditional_escape(attachment.external_url), conditional_escape(attachment.title))
@ -80,32 +76,3 @@ class ShowAttachmentsWidget(Widget):
html += u'No files attached'
html += u'</div></div>'
return mark_safe(html)
class RelatedLiaisonWidget(TextInput):
def render(self, name, value, attrs=None):
if not value:
value = ''
title = ''
noliaison = 'inline'
deselect = 'none'
else:
liaison = LiaisonStatement.objects.get(pk=value)
title = liaison.title
if not title:
attachments = liaison.attachments.all()
if attachments:
title = attachments[0].title
else:
title = 'Liaison #%s' % liaison.pk
noliaison = 'none'
deselect = 'inline'
html = u'<span class="noRelated" style="display: %s;">No liaison selected</span>' % conditional_escape(noliaison)
html += u'<span class="relatedLiaisonWidgetTitle">%s</span>' % conditional_escape(title)
html += u'<input type="hidden" name="%s" class="relatedLiaisonWidgetValue" value="%s" /> ' % (conditional_escape(name), conditional_escape(value))
html += u'<span style="display: none;" class="listURL">%s</span> ' % urlreverse('ajax_liaison_list')
html += u'<div style="display: none;" class="relatedLiaisonWidgetDialog" id="related-dialog" title="Select a liaison"></div> '
html += '<input type="button" id="id_%s" value="Select liaison" /> ' % conditional_escape(name)
html += '<input type="button" style="display: %s;" id="id_no_%s" value="Deselect liaison" />' % (conditional_escape(deselect), conditional_escape(name))
return mark_safe(html)

View file

@ -18,13 +18,13 @@ class MailingListTests(TestCase):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q(".group-archives a:contains(\"%s\")" % group.acronym)), 0)
self.assertEqual(len(q(".content-wrapper a:contains(\"%s\")" % group.acronym)), 0)
# successful get
group.list_archive = "https://example.com/foo"
group.save()
r = self.client.get(url)
q = PyQuery(r.content)
self.assertEqual(len(q(".group-archives a:contains(\"%s\")" % group.acronym)), 1)
self.assertEqual(len(q(".content-wrapper a:contains(\"%s\")" % group.acronym)), 1)

View file

@ -59,7 +59,7 @@ class Meeting(models.Model):
venue_addr = models.TextField(blank=True)
break_area = models.CharField(blank=True, max_length=255)
reg_area = models.CharField(blank=True, max_length=255)
agenda_note = models.TextField(blank=True, help_text="Text in this field will be placed at the top of the html agenda page for the meeting. HTML can be used, but will not validated.")
agenda_note = models.TextField(blank=True, help_text="Text in this field will be placed at the top of the html agenda page for the meeting. HTML can be used, but will not be validated.")
agenda = models.ForeignKey('Schedule',null=True,blank=True, related_name='+')
session_request_lock_message = models.CharField(blank=True,max_length=255) # locked if not empty
@ -298,8 +298,8 @@ class TimeSlot(models.Model):
time = models.DateTimeField()
duration = TimedeltaField()
location = models.ForeignKey(Room, blank=True, null=True)
show_location = models.BooleanField(default=True, help_text="Show location in agenda")
sessions = models.ManyToManyField('Session', related_name='slots', through='ScheduledSession', null=True, blank=True, help_text=u"Scheduled session, if any")
show_location = models.BooleanField(default=True, help_text="Show location in agenda.")
sessions = models.ManyToManyField('Session', related_name='slots', through='ScheduledSession', null=True, blank=True, help_text=u"Scheduled session, if any.")
modified = models.DateTimeField(default=datetime.datetime.now)
#
@ -461,8 +461,8 @@ class Schedule(models.Model):
meeting = models.ForeignKey(Meeting, null=True)
name = models.CharField(max_length=16, blank=False)
owner = models.ForeignKey(Person)
visible = models.BooleanField(default=True, help_text=u"Make this agenda available to those who know about it")
public = models.BooleanField(default=True, help_text=u"Make this agenda publically available")
visible = models.BooleanField(default=True, help_text=u"Make this agenda available to those who know about it.")
public = models.BooleanField(default=True, help_text=u"Make this agenda publically available.")
badness = models.IntegerField(null=True, blank=True)
# considering copiedFrom = models.ForeignKey('Schedule', blank=True, null=True)
@ -618,14 +618,14 @@ class ScheduledSession(models.Model):
Each relationship is attached to the named agenda, which is owned by
a specific person/user.
"""
timeslot = models.ForeignKey('TimeSlot', null=False, blank=False, help_text=u"")
session = models.ForeignKey('Session', null=True, default=None, help_text=u"Scheduled session")
timeslot = models.ForeignKey('TimeSlot', null=False, blank=False)
session = models.ForeignKey('Session', null=True, default=None, help_text=u"Scheduled session.")
schedule = models.ForeignKey('Schedule', null=False, blank=False, related_name='assignments')
extendedfrom = models.ForeignKey('ScheduledSession', null=True, default=None, help_text=u"Timeslot this session is an extension of")
extendedfrom = models.ForeignKey('ScheduledSession', null=True, default=None, help_text=u"Timeslot this session is an extension of.")
modified = models.DateTimeField(default=datetime.datetime.now)
notes = models.TextField(blank=True)
badness = models.IntegerField(default=0, blank=True, null=True)
pinned = models.BooleanField(default=False, help_text="Do not move session during automatic placement")
pinned = models.BooleanField(default=False, help_text="Do not move session during automatic placement.")
class Meta:
ordering = ["timeslot__time", "session__group__parent__name", "session__group__acronym", "session__name", ]
@ -810,8 +810,8 @@ class Session(models.Model):
Training sessions and similar are modeled by filling in a
responsible group (e.g. Edu team) and filling in the name."""
meeting = models.ForeignKey(Meeting)
name = models.CharField(blank=True, max_length=255, help_text="Name of session, in case the session has a purpose rather than just being a group meeting")
short = models.CharField(blank=True, max_length=32, help_text="Short version of 'name' above, for use in filenames")
name = models.CharField(blank=True, max_length=255, help_text="Name of session, in case the session has a purpose rather than just being a group meeting.")
short = models.CharField(blank=True, max_length=32, help_text="Short version of 'name' above, for use in filenames.")
group = models.ForeignKey(Group) # The group type determines the session type. BOFs also need to be added as a group.
attendees = models.IntegerField(null=True, blank=True)
agenda_note = models.CharField(blank=True, max_length=255)

View file

@ -731,9 +731,9 @@ class CurrentScheduleState:
#
if False:
class AutomaticScheduleStep(models.Model):
schedule = models.ForeignKey('Schedule', null=False, blank=False, help_text=u"Who made this agenda")
session = models.ForeignKey('Session', null=True, default=None, help_text=u"Scheduled session involved")
moved_from = models.ForeignKey('ScheduledSession', related_name="+", null=True, default=None, help_text=u"Where session was")
moved_to = models.ForeignKey('ScheduledSession', related_name="+", null=True, default=None, help_text=u"Where session went")
schedule = models.ForeignKey('Schedule', null=False, blank=False, help_text=u"Who made this agenda.")
session = models.ForeignKey('Session', null=True, default=None, help_text=u"Scheduled session involved.")
moved_from = models.ForeignKey('ScheduledSession', related_name="+", null=True, default=None, help_text=u"Where session was.")
moved_to = models.ForeignKey('ScheduledSession', related_name="+", null=True, default=None, help_text=u"Where session went.")
stepnum = models.IntegerField(default=0, blank=True, null=True)

View file

@ -48,7 +48,7 @@ class ScheduleEditTests(LiveServerTestCase):
self.driver.get(url)
self.driver.find_element_by_name('username').send_keys('plain')
self.driver.find_element_by_name('password').send_keys('plain+password')
self.driver.find_element_by_xpath('//input[@value="Sign in"]').click()
self.driver.find_element_by_xpath('//button[@type="submit"]').click()
def testUnschedule(self):

View file

@ -39,29 +39,21 @@ class MeetingTests(TestCase):
session = Session.objects.filter(meeting=meeting, group__acronym="mars").first()
slot = TimeSlot.objects.get(scheduledsession__session=session)
time_interval = "%s-%s" % (slot.time.strftime("%H%M"), (slot.time + slot.duration).strftime("%H%M"))
time_interval = "%s-%s" % (slot.time.strftime("%H:%M").lstrip("0"), (slot.time + slot.duration).strftime("%H:%M").lstrip("0"))
# plain
r = self.client.get(urlreverse("ietf.meeting.views.agenda", kwargs=dict(num=meeting.number)))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
agenda_content = q("#agenda").html()
agenda_content = q(".content-wrapper").html()
self.assertTrue(session.group.acronym in agenda_content)
self.assertTrue(session.group.name in agenda_content)
self.assertTrue(session.group.parent.acronym.upper() in agenda_content)
self.assertTrue(slot.location.name in agenda_content)
self.assertTrue(time_interval in agenda_content)
# mobile
r = self.client.get(urlreverse("ietf.meeting.views.agenda", kwargs=dict(num=meeting.number)),
{ '_testiphone': "1" })
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
agenda_content = q("#agenda").html()
self.assertTrue(session.group.acronym in agenda_content)
self.assertTrue(session.group.name[:10] in agenda_content)
self.assertTrue(slot.location.name in agenda_content)
self.assertTrue(time_interval in agenda_content)
# the rest of the results don't have as nicely formatted times
time_interval = time_interval.replace(":", "")
# text
r = self.client.get(urlreverse("ietf.meeting.views.agenda", kwargs=dict(num=meeting.number, ext=".txt")))
@ -125,12 +117,11 @@ class MeetingTests(TestCase):
r = self.client.get(urlreverse("ietf.meeting.views.materials", kwargs=dict(meeting_num=meeting.number)))
self.assertEqual(r.status_code, 200)
#debug.show('r.content')
q = PyQuery(r.content)
row = q('.ietf-materials b:contains("%s")' % str(session.group.acronym.upper())).closest("tr")
self.assertTrue(row.find("a:contains(\"Agenda\")"))
self.assertTrue(row.find("a:contains(\"Minutes\")"))
self.assertTrue(row.find("a:contains(\"Slideshow\")"))
row = q('.content-wrapper td:contains("%s")' % str(session.group.acronym)).closest("tr")
self.assertTrue(row.find('a:contains("Agenda")'))
self.assertTrue(row.find('a:contains("Minutes")'))
self.assertTrue(row.find('a:contains("Slideshow")'))
# FIXME: missing tests of .pdf/.tar generation (some code can
# probably be lifted from similar tests in iesg/tests.py)

View file

@ -9,8 +9,7 @@ from ietf.meeting import ajax
urlpatterns = patterns('',
(r'^(?P<meeting_num>\d+)/materials.html$', views.materials),
(r'^agenda/$', views.agenda),
(r'^(?P<base>agenda-utc)(?P<ext>.html)?$', views.agenda),
(r'^agenda(?P<ext>.html)?$', views.agenda),
(r'^agenda(-utc)?(?P<ext>.html)?$', views.agenda),
(r'^agenda(?P<ext>.txt)$', views.agenda),
(r'^agenda(?P<ext>.csv)$', views.agenda),
(r'^agenda/edit$', views.edit_agenda),
@ -28,8 +27,7 @@ urlpatterns = patterns('',
(r'^(?P<num>\d+)/agenda/(?P<owner>[A-Za-z0-9-.+_]+@[A-Za-z0-9._]+)/(?P<name>[A-Za-z0-9-:_]+)/sessions.json$', ajax.scheduledsessions_json),
(r'^(?P<num>\d+)/agenda/(?P<owner>[A-Za-z0-9-.+_]+@[A-Za-z0-9._]+)/(?P<name>[A-Za-z0-9-:_]+).json$', ajax.agenda_infourl),
(r'^(?P<num>\d+)/agenda/edit$', views.edit_agenda),
(r'^(?P<num>\d+)/agenda(?P<ext>.html)?/?$', views.agenda),
(r'^(?P<num>\d+)/(?P<base>agenda-utc)(?P<ext>.html)?/?$', views.agenda),
(r'^(?P<num>\d+)/agenda(-utc)?(?P<ext>.html)?/?$', views.agenda),
(r'^(?P<num>\d+)/requests.html$', RedirectView.as_view(url='/meeting/%(num)s/requests', permanent=True)),
(r'^(?P<num>\d+)/requests$', views.meeting_requests),
(r'^(?P<num>\d+)/agenda(?P<ext>.txt)$', views.agenda),

View file

@ -187,7 +187,7 @@ def edit_timeslots(request, num=None):
roomsurl = reverse(timeslot_roomsurl, args=[meeting.number])
adddayurl = reverse(timeslot_slotsurl, args=[meeting.number])
return HttpResponse(render_to_string("meeting/timeslot_edit.html",
return render(request, "meeting/timeslot_edit.html",
{"timeslots": timeslots,
"meeting_base_url": meeting_base_url,
"site_base_url": site_base_url,
@ -199,8 +199,9 @@ def edit_timeslots(request, num=None):
"time_slices":time_slices,
"slot_slices": slots,
"date_slices":date_slices,
"meeting":meeting},
RequestContext(request)), content_type="text/html")
"meeting":meeting,
"hide_menu": True,
})
class RoomForm(ModelForm):
class Meta:
@ -227,12 +228,13 @@ def edit_roomurl(request, num, roomid):
roomform = RoomForm(instance=room)
meeting_base_url = request.build_absolute_uri(meeting.base_url())
site_base_url = request.build_absolute_uri('/')[:-1] # skip the trailing slash
return HttpResponse(render_to_string("meeting/room_edit.html",
return render(request, "meeting/room_edit.html",
{"meeting_base_url": meeting_base_url,
"site_base_url": site_base_url,
"editroom": roomform,
"meeting":meeting},
RequestContext(request)), content_type="text/html")
"meeting":meeting,
"hide_menu": True,
})
##############################################################################
#@role_required('Area Director','Secretariat')
@ -268,7 +270,8 @@ def edit_agenda(request, num=None, owner=None, name=None):
return HttpResponse(render_to_string("meeting/private_agenda.html",
{"schedule":schedule,
"meeting": meeting,
"meeting_base_url":meeting_base_url},
"meeting_base_url":meeting_base_url,
"hide_menu": True},
RequestContext(request)), status=403, content_type="text/html")
scheduledsessions = get_all_scheduledsessions_from_schedule(schedule)
@ -287,7 +290,7 @@ def edit_agenda(request, num=None, owner=None, name=None):
time_slices,date_slices = build_all_agenda_slices(meeting)
return HttpResponse(render_to_string("meeting/landscape_edit.html",
return render(request, "meeting/landscape_edit.html",
{"schedule":schedule,
"saveas": saveas,
"saveasurl": saveasurl,
@ -302,8 +305,9 @@ def edit_agenda(request, num=None, owner=None, name=None):
"area_directors" : ads,
"wg_list": wg_list ,
"scheduledsessions": scheduledsessions,
"show_inline": set(["txt","htm","html"]) },
RequestContext(request)), content_type="text/html")
"show_inline": set(["txt","htm","html"]),
"hide_menu": True,
})
##############################################################################
# show the properties associated with an agenda (visible, public)
@ -326,11 +330,12 @@ def edit_agenda_properties(request, num=None, owner=None, name=None):
if not (canedit or has_role(request.user,'Secretariat')):
return HttpResponseForbidden("You may not edit this agenda")
else:
return HttpResponse(render_to_string("meeting/properties_edit.html",
return render(request, "meeting/properties_edit.html",
{"schedule":schedule,
"form":form,
"meeting":meeting},
RequestContext(request)), content_type="text/html")
"meeting":meeting,
"hide_menu": True,
})
##############################################################################
# show list of agendas.
@ -352,29 +357,25 @@ def edit_agendas(request, num=None, order=None):
schedules = schedules.order_by('owner', 'name')
return HttpResponse(render_to_string("meeting/agenda_list.html",
return render(request, "meeting/agenda_list.html",
{"meeting": meeting,
"schedules": schedules.all()
},
RequestContext(request)),
content_type="text/html")
"schedules": schedules.all(),
"hide_menu": True,
})
@ensure_csrf_cookie
def agenda(request, num=None, name=None, base=None, ext=None):
base = base if base else 'agenda'
ext = ext if ext else '.html'
if 'iPhone' in get_user_agent(request) and ext == ".html":
base = 'm_agenda'
mimetype = {".html":"text/html", ".txt": "text/plain", ".ics":"text/calendar", ".csv":"text/csv"}
meeting = get_meeting(num)
schedule = get_schedule(meeting, name)
if schedule == None:
return HttpResponse(render_to_string("meeting/no-"+base+ext,
{'meeting':meeting }, RequestContext(request)), content_type=mimetype[ext])
base = base.replace("-utc", "")
return render(request, "meeting/no-"+base+ext, {'meeting':meeting }, content_type=mimetype[ext])
updated = meeting_updated(meeting)
return HttpResponse(render_to_string("meeting/"+base+ext,
{"schedule":schedule, "updated": updated}, RequestContext(request)), content_type=mimetype[ext])
return render(request, "meeting/"+base+ext, {"schedule":schedule, "updated": updated}, content_type=mimetype[ext])
def read_agenda_file(num, doc):
# XXXX FIXME: the path fragment in the code below should be moved to

View file

@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
def fix_non_selected_choice(apps, schema_editor):
apps.get_model("name", "IprLicenseTypeName").objects.filter(slug="none-selected").update(desc="[None selected]")
class Migration(migrations.Migration):
dependencies = [
('name', '0002_auto_20150208_1008'),
]
operations = [
migrations.RunPython(fix_non_selected_choice)
]

View file

@ -11,13 +11,13 @@ from ietf.dbtemplate.forms import DBTemplateForm
from ietf.group.models import Group, Role
from ietf.ietfauth.utils import role_required
from ietf.name.models import RoleName, FeedbackTypeName, NomineePositionStateName
from ietf.nomcom.models import ( NomCom, Nomination, Nominee, NomineePosition,
from ietf.nomcom.models import ( NomCom, Nomination, Nominee, NomineePosition,
Position, Feedback, ReminderDates )
from ietf.nomcom.utils import (NOMINATION_RECEIPT_TEMPLATE, FEEDBACK_RECEIPT_TEMPLATE,
get_user_email, validate_private_key, validate_public_key,
get_or_create_nominee, create_feedback_email)
from ietf.person.models import Email
from ietf.person.fields import AutocompletedEmailField
from ietf.person.fields import SearchableEmailField
from ietf.utils.fields import MultiEmailField
from ietf.utils.mail import send_mail
@ -192,62 +192,6 @@ class EditMembersFormPreview(FormPreview):
return redirect('nomcom_edit_members', year=self.year)
class EditChairForm(BaseNomcomForm, forms.Form):
chair = forms.EmailField(label="Chair email", required=False,
widget=forms.TextInput(attrs={'size': '40'}))
fieldsets = [('Chair info', ('chair',))]
class EditChairFormPreview(FormPreview):
form_template = 'nomcom/edit_chair.html'
preview_template = 'nomcom/edit_chair_preview.html'
@method_decorator(role_required("Secretariat"))
def __call__(self, request, *args, **kwargs):
year = kwargs['year']
group = get_nomcom_group_or_404(year)
self.state['group'] = group
self.state['rolodex_url'] = ROLODEX_URL
self.group = group
self.year = year
return super(EditChairFormPreview, self).__call__(request, *args, **kwargs)
def get_initial(self, request):
chair = self.group.get_chair()
if chair:
return { "chair": chair.email.address }
return {}
def process_preview(self, request, form, context):
chair_email = form.cleaned_data['chair']
try:
chair_email_obj = Email.objects.get(address=chair_email)
chair_person = chair_email_obj.person
except Email.DoesNotExist:
chair_person = None
chair_email_obj = None
chair_info = {'email': chair_email,
'email_obj': chair_email_obj,
'person': chair_person}
self.state.update({'chair_info': chair_info})
def done(self, request, cleaned_data):
chair_info = self.state['chair_info']
chair_exclude = self.group.role_set.filter(name__slug='chair').exclude(email__address=chair_info['email'])
chair_exclude.delete()
if chair_info['email_obj'] and chair_info['person']:
Role.objects.get_or_create(name=RoleName.objects.get(slug="chair"),
group=self.group,
person=chair_info['person'],
email=chair_info['email_obj'])
return redirect('nomcom_edit_chair', year=self.year)
class EditNomcomForm(BaseNomcomForm, forms.ModelForm):
fieldsets = [('Edit nomcom settings', ('public_key', 'initial_text',
@ -281,7 +225,7 @@ class EditNomcomForm(BaseNomcomForm, forms.ModelForm):
class MergeForm(BaseNomcomForm, forms.Form):
secondary_emails = MultiEmailField(label="Secondary email addresses",
help_text="Provide a comma separated list of email addresses. Nominations already received with any of these email address will be moved to show under the primary address", widget=forms.Textarea)
help_text="Provide a comma separated list of email addresses. Nominations already received with any of these email address will be moved to show under the primary address.", widget=forms.Textarea)
primary_email = forms.EmailField(label="Primary email address",
widget=forms.TextInput(attrs={'size': '40'}))
@ -363,11 +307,10 @@ class MergeForm(BaseNomcomForm, forms.Form):
class NominateForm(BaseNomcomForm, forms.ModelForm):
comments = forms.CharField(label="Candidate's Qualifications for the Position:",
comments = forms.CharField(label="Candidate's qualifications for the position",
widget=forms.Textarea())
confirmation = forms.BooleanField(label='Email comments back to me as confirmation',
help_text="If you want to get a confirmation mail containing your feedback in cleartext, \
please check the 'email comments back to me as confirmation'",
help_text="If you want to get a confirmation mail containing your feedback in cleartext, please check the 'email comments back to me as confirmation'.",
required=False)
fieldsets = [('Candidate Nomination', ('position', 'candidate_name',
@ -385,6 +328,7 @@ class NominateForm(BaseNomcomForm, forms.ModelForm):
'candidate_email', 'candidate_phone',
'comments']
self.fields['nominator_email'].label = 'Nominator email'
if self.nomcom:
self.fields['position'].queryset = Position.objects.get_by_nomcom(self.nomcom).opened()
self.fields['comments'].help_text = self.nomcom.initial_text
@ -446,7 +390,7 @@ class NominateForm(BaseNomcomForm, forms.ModelForm):
# send receipt email to nominator
if confirmation:
if author:
subject = 'Nomination Receipt'
subject = 'Nomination receipt'
from_email = settings.NOMCOM_FROM_EMAIL
to_email = author.address
context = {'nominee': nominee.email.person.name,
@ -464,19 +408,18 @@ class NominateForm(BaseNomcomForm, forms.ModelForm):
class FeedbackForm(BaseNomcomForm, forms.ModelForm):
position_name = forms.CharField(label='position',
position_name = forms.CharField(label='Position',
widget=forms.TextInput(attrs={'size': '40'}))
nominee_name = forms.CharField(label='nominee name',
nominee_name = forms.CharField(label='Nominee name',
widget=forms.TextInput(attrs={'size': '40'}))
nominee_email = forms.CharField(label='nominee email',
nominee_email = forms.CharField(label='Nominee email',
widget=forms.TextInput(attrs={'size': '40'}))
nominator_email = forms.CharField(label='commenter email')
nominator_email = forms.CharField(label='Commenter email')
comments = forms.CharField(label='Comments on this candidate',
comments = forms.CharField(label='Comments on this nominee',
widget=forms.Textarea())
confirmation = forms.BooleanField(label='Email comments back to me as confirmation',
help_text="If you want to get a confirmation mail containing your feedback in cleartext, \
please check the 'email comments back to me as confirmation'",
help_text="If you want to get a confirmation mail containing your feedback in cleartext, please check the 'email comments back to me as confirmation'.",
required=False)
def __init__(self, *args, **kwargs):
@ -641,7 +584,7 @@ class PositionForm(BaseNomcomForm, forms.ModelForm):
fieldsets = [('Position', ('name', 'description',
'is_open', 'incumbent'))]
incumbent = AutocompletedEmailField(required=False)
incumbent = SearchableEmailField(required=False)
class Meta:
model = Position

View file

@ -1,4 +1,4 @@
# -*- coding: utf-8-No-BOM -*-
# -*- coding: utf-8 -*-
import os
from django.db import models
@ -38,11 +38,11 @@ class NomCom(models.Model):
upload_to=upload_path_handler, blank=True, null=True)
group = models.ForeignKey(Group)
send_questionnaire = models.BooleanField(verbose_name='Send questionnaires automatically"', default=False,
help_text='If you check this box, questionnaires are sent automatically after nominations')
send_questionnaire = models.BooleanField(verbose_name='Send questionnaires automatically', default=False,
help_text='If you check this box, questionnaires are sent automatically after nominations.')
reminder_interval = models.PositiveIntegerField(help_text='If the nomcom user sets the interval field then a cron command will \
send reminders to the nominees who have not responded using \
the following formula: (today - nomination_date) % interval == 0',
the following formula: (today - nomination_date) % interval == 0.',
blank=True, null=True)
initial_text = models.TextField(verbose_name='Help text for nomination form',
blank=True)

View file

@ -41,12 +41,8 @@ def add_num_nominations(user, position, nominee):
nominees__in=[nominee],
author=author,
type='comment').count()
if count:
mark = """<span style="white-space: pre; color: red;">*</span>"""
else:
mark = """<span style="white-space: pre;"> </span> """
return '<span title="%d earlier comments from you on %s as %s">%s</span>&nbsp;' % (count, nominee.email.address, position, mark)
return '<span class="badge" title="%d earlier comments from you on %s as %s">%s</span>&nbsp;' % (count, nominee.email.address, position, count)
@register.filter
@ -76,7 +72,7 @@ def decrypt(string, request, year, plain=False):
code, out, error = pipe(command % (settings.OPENSSL_COMMAND,
encrypted_file.name), key)
if code != 0:
log("openssl error: %s:\n Error %s: %s" %(command, code, error))
log("openssl error: %s:\n Error %s: %s" %(command, code, error))
os.unlink(encrypted_file.name)

View file

@ -1,4 +1,4 @@
# -*- coding: UTF-8-No-BOM -*-
# -*- coding: utf-8 -*-
import tempfile
import datetime
@ -22,7 +22,7 @@ from ietf.nomcom.test_data import nomcom_test_data, generate_cert, check_comment
from ietf.nomcom.models import NomineePosition, Position, Nominee, \
NomineePositionStateName, Feedback, FeedbackTypeName, \
Nomination
from ietf.nomcom.forms import EditChairForm, EditChairFormPreview, EditMembersForm
from ietf.nomcom.forms import EditMembersForm, EditMembersFormPreview
from ietf.nomcom.utils import get_nomcom_by_year, get_or_create_nominee
from ietf.nomcom.management.commands.send_reminders import Command, is_time_to_send
@ -54,7 +54,6 @@ class NomcomViewsTest(TestCase):
self.private_index_url = reverse('nomcom_private_index', kwargs={'year': self.year})
self.private_merge_url = reverse('nomcom_private_merge', kwargs={'year': self.year})
self.edit_members_url = reverse('nomcom_edit_members', kwargs={'year': self.year})
self.edit_chair_url = reverse('nomcom_edit_chair', kwargs={'year': self.year})
self.edit_nomcom_url = reverse('nomcom_edit_nomcom', kwargs={'year': self.year})
self.private_nominate_url = reverse('nomcom_private_nominate', kwargs={'year': self.year})
self.add_questionnaire_url = reverse('nomcom_private_questionnaire', kwargs={'year': self.year})
@ -214,31 +213,36 @@ class NomcomViewsTest(TestCase):
"primary_email": nominees[0]}
response = self.client.post(self.private_merge_url, test_data)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "info-message-error")
q = PyQuery(response.content)
self.assertTrue(q("form .has-error"))
test_data = {"primary_email": nominees[0],
"secondary_emails": ""}
response = self.client.post(self.private_merge_url, test_data)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "info-message-error")
q = PyQuery(response.content)
self.assertTrue(q("form .has-error"))
test_data = {"primary_email": "",
"secondary_emails": nominees[0]}
response = self.client.post(self.private_merge_url, test_data)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "info-message-error")
q = PyQuery(response.content)
self.assertTrue(q("form .has-error"))
test_data = {"primary_email": "unknown@example.com",
"secondary_emails": nominees[0]}
response = self.client.post(self.private_merge_url, test_data)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "info-message-error")
q = PyQuery(response.content)
self.assertTrue(q("form .has-error"))
test_data = {"primary_email": nominees[0],
"secondary_emails": "unknown@example.com"}
response = self.client.post(self.private_merge_url, test_data)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "info-message-error")
q = PyQuery(response.content)
self.assertTrue(q("form .has-error"))
test_data = {"secondary_emails": """%s,
%s,
@ -247,7 +251,7 @@ class NomcomViewsTest(TestCase):
response = self.client.post(self.private_merge_url, test_data)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "info-message-success")
self.assertContains(response, "alert-success")
self.assertEqual(Nominee.objects.filter(email__address=nominees[1],
duplicated__isnull=False).count(), 1)
@ -293,7 +297,7 @@ class NomcomViewsTest(TestCase):
# preview
self.client.post(self.edit_members_url, test_data)
hash = EditChairFormPreview(EditChairForm).security_hash(None, EditMembersForm(test_data))
hash = EditMembersFormPreview(EditMembersForm).security_hash(None, EditMembersForm(test_data))
test_data.update({'hash': hash, 'stage': 2})
# submit
@ -318,33 +322,6 @@ class NomcomViewsTest(TestCase):
self.check_url_status(self.private_index_url, 403)
self.client.logout()
def change_chair(self, user):
test_data = {'chair': '%s%s' % (user, EMAIL_DOMAIN),
'stage': 1}
# preview
self.client.post(self.edit_chair_url, test_data)
hash = EditChairFormPreview(EditChairForm).security_hash(None, EditChairForm(test_data))
test_data.update({'hash': hash, 'stage': 2})
# submit
self.client.post(self.edit_chair_url, test_data)
def test_edit_chair_view(self):
self.access_secretariat_url(self.edit_chair_url)
self.change_chair(COMMUNITY_USER)
# check chair actions
self.client.login(username=COMMUNITY_USER,password=COMMUNITY_USER+"+password")
self.check_url_status(self.edit_members_url, 200)
self.check_url_status(self.edit_nomcom_url, 200)
self.client.logout()
# revert edit nomcom chair
login_testing_unauthorized(self, SECRETARIAT_USER, self.edit_chair_url)
self.change_chair(CHAIR_USER)
self.client.logout()
def test_edit_nomcom_view(self):
r = self.access_chair_url(self.edit_nomcom_url)
q = PyQuery(r.content)
@ -433,7 +410,8 @@ class NomcomViewsTest(TestCase):
nomcom = get_nomcom_by_year(self.year)
if not nomcom.public_key:
self.assertNotContains(response, "nominateform")
q = PyQuery(response.content)
self.assertEqual(len(q("#nominate-form")), 0)
# save the cert file in tmp
nomcom.public_key.storage.location = tempfile.gettempdir()
@ -441,7 +419,8 @@ class NomcomViewsTest(TestCase):
response = self.client.get(nominate_url)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "nominateform")
q = PyQuery(response.content)
self.assertEqual(len(q("#nominate-form")), 1)
position = Position.objects.get(name=position_name)
candidate_email = nominee_email
@ -459,7 +438,8 @@ class NomcomViewsTest(TestCase):
response = self.client.post(nominate_url, test_data)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "info-message-success")
q = PyQuery(response.content)
self.assertContains(response, "alert-success")
# check objects
email = Email.objects.get(address=candidate_email)
@ -526,7 +506,7 @@ class NomcomViewsTest(TestCase):
response = self.client.post(self.add_questionnaire_url, test_data)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "info-message-success")
self.assertContains(response, "alert-success")
## check objects
feedback = Feedback.objects.filter(positions__in=[position],
@ -597,17 +577,18 @@ class NomcomViewsTest(TestCase):
nominee_position = NomineePosition.objects.get(nominee=nominee,
position=position)
state = nominee_position.state
if not state.slug == 'accepted':
if state.slug != 'accepted':
response = self.client.post(feedback_url, test_data)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "info-message-error")
q = PyQuery(response.content)
self.assertTrue(q("form .has-error"))
# accept nomination
nominee_position.state = NomineePositionStateName.objects.get(slug='accepted')
nominee_position.save()
response = self.client.post(feedback_url, test_data)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "info-message-success")
self.assertContains(response, "alert-success")
## check objects
feedback = Feedback.objects.filter(positions__in=[position],

View file

@ -1,8 +1,7 @@
from django.conf.urls import patterns, url
from django.views.generic import TemplateView
from ietf.nomcom.forms import ( EditChairForm, EditChairFormPreview,
EditMembersForm, EditMembersFormPreview )
from ietf.nomcom.forms import EditMembersForm, EditMembersFormPreview
urlpatterns = patterns('ietf.nomcom.views',
url(r'^$', 'index'),
@ -22,7 +21,6 @@ urlpatterns = patterns('ietf.nomcom.views',
# url(r'^(?P<year>\d{4})/private/send-reminder-mail/$', RedirectView.as_view(url=reverse_lazy('nomcom_send_reminder_mail',kwargs={'year':year,'type':'accept'}))),
url(r'^(?P<year>\d{4})/private/send-reminder-mail/(?P<type>\w+)/$', 'send_reminder_mail', name='nomcom_send_reminder_mail'),
url(r'^(?P<year>\d{4})/private/edit-members/$', EditMembersFormPreview(EditMembersForm), name='nomcom_edit_members'),
url(r'^(?P<year>\d{4})/private/edit-chair/$', EditChairFormPreview(EditChairForm), name='nomcom_edit_chair'),
url(r'^(?P<year>\d{4})/private/edit-nomcom/$', 'edit_nomcom', name='nomcom_edit_nomcom'),
url(r'^(?P<year>\d{4})/private/delete-nomcom/$', 'delete_nomcom', name='nomcom_delete_nomcom'),
url(r'^deleted/$', TemplateView.as_view(template_name='nomcom/deleted.html'), name='nomcom_deleted'),

View file

@ -56,7 +56,7 @@ def index(request):
nomcom.ann_url = None
return render_to_response('nomcom/index.html',
{'nomcom_list': nomcom_list,}, RequestContext(request))
def year_index(request, year):
nomcom = get_nomcom_by_year(year)
@ -70,21 +70,21 @@ def year_index(request, year):
def announcements(request):
address_re = re.compile("<.*>")
nomcoms = Group.objects.filter(type="nomcom")
regimes = []
for n in nomcoms:
e = GroupEvent.objects.filter(group=n, type="changed_state", changestategroupevent__state="active").order_by('time')[:1]
n.start_year = e[0].time.year if e else 0
e = GroupEvent.objects.filter(group=n, type="changed_state", changestategroupevent__state="conclude").order_by('time')[:1]
n.end_year = e[0].time.year if e else n.start_year + 1
r = n.role_set.select_related().filter(name="chair")
chair = None
if r:
chair = r[0]
r = n.role_set.select_related().filter(name="chair")
chair = None
if r:
chair = r[0]
announcements = Message.objects.filter(related_groups=n).order_by('-time')
for a in announcements:
@ -311,8 +311,8 @@ def nominate(request, year, public):
template = 'nomcom/private_nominate.html'
if not has_publickey:
message = ('warning', "This Nomcom is not yet accepting nominations")
return render_to_response(template,
message = ('warning', "This Nomcom is not yet accepting nominations")
return render_to_response(template,
{'message': message,
'nomcom': nomcom,
'year': year,
@ -361,17 +361,19 @@ def feedback(request, year, public):
positions = Position.objects.get_by_nomcom(nomcom=nomcom).opened()
if public:
template = 'nomcom/public_feedback.html'
base_template = "nomcom/nomcom_public_base.html"
else:
template = 'nomcom/private_feedback.html'
base_template = "nomcom/nomcom_private_base.html"
if not has_publickey:
message = ('warning', "This Nomcom is not yet accepting comments")
return render_to_response(template,
{'message': message,
'nomcom': nomcom,
'year': year,
'selected': 'feedback'}, RequestContext(request))
return render(request, 'nomcom/feedback.html', {
'message': message,
'nomcom': nomcom,
'year': year,
'selected': 'feedback',
'base_template': base_template
})
message = None
if request.method == 'POST':
@ -385,14 +387,16 @@ def feedback(request, year, public):
form = FeedbackForm(nomcom=nomcom, user=request.user, public=public,
position=position, nominee=nominee)
return render_to_response(template,
{'form': form,
'message': message,
'nomcom': nomcom,
'year': year,
'positions': positions,
'submit_disabled': submit_disabled,
'selected': 'feedback'}, RequestContext(request))
return render(request, 'nomcom/feedback.html', {
'form': form,
'message': message,
'nomcom': nomcom,
'year': year,
'positions': positions,
'submit_disabled': submit_disabled,
'selected': 'feedback',
'base_template': base_template
})
@role_required("Nomcom Chair", "Nomcom Advisor")

View file

@ -8,7 +8,7 @@ import debug # pyflakes:ignore
from ietf.person.models import Email, Person
def tokeninput_id_name_json(objs):
def select2_id_name_json(objs):
def format_email(e):
return escape(u"%s <%s>" % (e.person.name, e.address))
def format_person(p):
@ -16,63 +16,63 @@ def tokeninput_id_name_json(objs):
formatter = format_email if objs and isinstance(objs[0], Email) else format_person
return json.dumps([{ "id": o.pk, "name": formatter(o) } for o in objs if o])
return json.dumps([{ "id": o.pk, "text": formatter(o) } for o in objs if o])
class AutocompletedPersonsField(forms.CharField):
"""Tokenizing autocompleted multi-select field for choosing
persons/emails or just persons using jquery.tokeninput.js.
class SearchablePersonsField(forms.CharField):
"""Server-based multi-select field for choosing
persons/emails or just persons using select2.js.
The field operates on either Email or Person models. In the case
of Email models, the person name is shown next to the email address.
of Email models, the person name is shown next to the email
address.
The field uses a comma-separated list of primary keys in a
CharField element as its API, the tokeninput Javascript adds some
selection magic on top of this so we have to pass it a JSON
representation of ids and user-understandable labels."""
CharField element as its API with some extra attributes used by
the Javascript part."""
def __init__(self,
max_entries=None, # max number of selected objs
only_users=False, # only select persons who also have a user
model=Person, # or Email
hint_text="Type in name to search for person",
hint_text="Type in name to search for person.",
*args, **kwargs):
kwargs["max_length"] = 1000
self.max_entries = max_entries
self.only_users = only_users
self.model = model
super(AutocompletedPersonsField, self).__init__(*args, **kwargs)
super(SearchablePersonsField, self).__init__(*args, **kwargs)
self.widget.attrs["class"] = "tokenized-field"
self.widget.attrs["data-hint-text"] = hint_text
self.widget.attrs["class"] = "select2-field"
self.widget.attrs["data-placeholder"] = hint_text
if self.max_entries != None:
self.widget.attrs["data-max-entries"] = self.max_entries
def parse_tokenized_value(self, value):
def parse_select2_value(self, value):
return [x.strip() for x in value.split(",") if x.strip()]
def prepare_value(self, value):
if not value:
value = ""
if isinstance(value, basestring):
pks = self.parse_tokenized_value(value)
pks = self.parse_select2_value(value)
value = self.model.objects.filter(pk__in=pks).select_related("person")
if isinstance(value, self.model):
value = [value]
self.widget.attrs["data-pre"] = tokeninput_id_name_json(value)
self.widget.attrs["data-pre"] = select2_id_name_json(value)
# doing this in the constructor is difficult because the URL
# patterns may not have been fully constructed there yet
self.widget.attrs["data-ajax-url"] = urlreverse("ajax_tokeninput_search", kwargs={ "model_name": self.model.__name__.lower() })
self.widget.attrs["data-ajax-url"] = urlreverse("ajax_select2_search_person_email", kwargs={ "model_name": self.model.__name__.lower() })
if self.only_users:
self.widget.attrs["data-ajax-url"] += "?user=1" # require a Datatracker account
return ",".join(e.address for e in value)
return u",".join(e.address for e in value)
def clean(self, value):
value = super(AutocompletedPersonsField, self).clean(value)
pks = self.parse_tokenized_value(value)
value = super(SearchablePersonsField, self).clean(value)
pks = self.parse_select2_value(value)
objs = self.model.objects.filter(pk__in=pks)
if self.model == Email:
@ -92,32 +92,32 @@ class AutocompletedPersonsField(forms.CharField):
return objs
class AutocompletedPersonField(AutocompletedPersonsField):
"""Version of AutocompletedPersonsField specialized to a single object."""
class SearchablePersonField(SearchablePersonsField):
"""Version of SearchablePersonsField specialized to a single object."""
def __init__(self, *args, **kwargs):
kwargs["max_entries"] = 1
super(AutocompletedPersonField, self).__init__(*args, **kwargs)
super(SearchablePersonField, self).__init__(*args, **kwargs)
def clean(self, value):
return super(AutocompletedPersonField, self).clean(value).first()
return super(SearchablePersonField, self).clean(value).first()
class AutocompletedEmailsField(AutocompletedPersonsField):
"""Version of AutocompletedPersonsField with the defaults right for Emails."""
class SearchableEmailsField(SearchablePersonsField):
"""Version of SearchablePersonsField with the defaults right for Emails."""
def __init__(self, model=Email, hint_text="Type in name or email to search for person and email address",
def __init__(self, model=Email, hint_text="Type in name or email to search for person and email address.",
*args, **kwargs):
super(AutocompletedEmailsField, self).__init__(model=model, hint_text=hint_text, *args, **kwargs)
super(SearchableEmailsField, self).__init__(model=model, hint_text=hint_text, *args, **kwargs)
class AutocompletedEmailField(AutocompletedEmailsField):
"""Version of AutocompletedEmailsField specialized to a single object."""
class SearchableEmailField(SearchableEmailsField):
"""Version of SearchableEmailsField specialized to a single object."""
def __init__(self, *args, **kwargs):
kwargs["max_entries"] = 1
super(AutocompletedEmailField, self).__init__(*args, **kwargs)
super(SearchableEmailField, self).__init__(*args, **kwargs)
def clean(self, value):
return super(AutocompletedEmailField, self).clean(value).first()
return super(SearchableEmailField, self).clean(value).first()

View file

@ -11,7 +11,7 @@ class PersonTests(TestCase):
draft = make_test_data()
person = draft.ad
r = self.client.get(urlreverse("ietf.person.views.ajax_tokeninput_search", kwargs={ "model_name": "email"}), dict(q=person.name))
r = self.client.get(urlreverse("ietf.person.views.ajax_select2_search", kwargs={ "model_name": "email"}), dict(q=person.name))
self.assertEqual(r.status_code, 200)
data = json.loads(r.content)
self.assertEqual(data[0]["id"], person.email_address())

View file

@ -2,6 +2,6 @@ from django.conf.urls import patterns
from ietf.person import ajax
urlpatterns = patterns('',
(r'^search/(?P<model_name>(person|email))/$', "ietf.person.views.ajax_tokeninput_search", None, 'ajax_tokeninput_search'),
(r'^search/(?P<model_name>(person|email))/$', "ietf.person.views.ajax_select2_search", None, 'ajax_select2_search_person_email'),
(r'^(?P<personid>[a-z0-9]+).json$', ajax.person_json),
)

View file

@ -2,9 +2,9 @@ from django.http import HttpResponse
from django.db.models import Q
from ietf.person.models import Email, Person
from ietf.person.fields import tokeninput_id_name_json
from ietf.person.fields import select2_id_name_json
def ajax_tokeninput_search(request, model_name):
def ajax_select2_search(request, model_name):
if model_name == "email":
model = Email
else:
@ -39,6 +39,11 @@ def ajax_tokeninput_search(request, model_name):
if only_users:
objs = objs.exclude(user=None)
objs = objs.distinct()[:10]
try:
page = int(request.GET.get("p", 1)) - 1
except ValueError:
page = 0
return HttpResponse(tokeninput_id_name_json(objs), content_type='application/json')
objs = objs.distinct()[page:page + 10]
return HttpResponse(select2_id_name_json(objs), content_type='application/json')

View file

@ -8,7 +8,7 @@ from ietf.doc.models import Document, DocAlias, State
from ietf.name.models import IntendedStdLevelName, DocRelationshipName
from ietf.group.models import Group
from ietf.person.models import Person, Email
from ietf.person.fields import AutocompletedEmailField
from ietf.person.fields import SearchableEmailField
from ietf.secr.groups.forms import get_person
@ -82,7 +82,7 @@ class AliasModelChoiceField(forms.ModelChoiceField):
# ---------------------------------------------
class AddModelForm(forms.ModelForm):
start_date = forms.DateField()
group = GroupModelChoiceField(required=True,help_text='Use group "none" for Individual Submissions')
group = GroupModelChoiceField(required=True,help_text='Use group "none" for Individual Submissions.')
class Meta:
model = Document
@ -103,7 +103,7 @@ class AuthorForm(forms.Form):
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")
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):
@ -132,7 +132,7 @@ class EditModelForm(forms.ModelForm):
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 = AutocompletedEmailField(required=False, only_users=True)
shepherd = SearchableEmailField(required=False, only_users=True)
class Meta:
model = Document
@ -372,5 +372,5 @@ class UploadForm(forms.Form):
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')
type = forms.CharField(widget=forms.Select(choices=WITHDRAW_CHOICES),help_text='Select which type of withdraw to perform.')

View file

@ -2,7 +2,7 @@ from django import forms
from ietf.group.models import Group
from ietf.meeting.models import ResourceAssociation
from ietf.person.fields import AutocompletedPersonsField
from ietf.person.fields import SearchablePersonsField
# -------------------------------------------------
@ -67,7 +67,7 @@ class SessionForm(forms.Form):
wg_selector3 = forms.ChoiceField(choices=WG_CHOICES,required=False)
third_session = forms.BooleanField(required=False)
resources = forms.MultipleChoiceField(choices=[(x.pk,x.desc) for x in ResourceAssociation.objects.all()], widget=forms.CheckboxSelectMultiple,required=False)
bethere = AutocompletedPersonsField(label="Must be present", required=False)
bethere = SearchablePersonsField(label="Must be present", required=False)
def __init__(self, *args, **kwargs):
super(SessionForm, self).__init__(*args, **kwargs)

View file

@ -11,7 +11,7 @@
{% block extrastyle %}{% endblock %}
{% block extrahead %}
<script type="text/javascript" src="{{ SECR_STATIC_URL }}js/jquery-1.5.1.min.js"></script>
<script type="text/javascript" src="/js/lib/jquery-1.11.1.min.js"></script>
{% endblock %}
{% block blockbots %}<meta name="robots" content="NONE,NOARCHIVE" />{% endblock %}

View file

@ -7,9 +7,11 @@
<link rel="stylesheet" type="text/css" href="{{ SECR_STATIC_URL }}css/jquery.ui.autocomplete.css" />
<script type="text/javascript" src="{{ SECR_STATIC_URL }}js/jquery-ui-1.8.1.custom.min.js"></script>
<script type="text/javascript" src="{{ SECR_STATIC_URL }}js/utils.js"></script>
<link rel="stylesheet" type="text/css" href="/css/token-input.css"></link>
<script type="text/javascript" src="/js/lib/jquery.tokeninput.js"></script>
<script type="text/javascript" src="/js/tokenized-field.js"></script>
<link rel="stylesheet" href="/css/lib/select2.css">
<link rel="stylesheet" href="/css/lib/select2-bootstrap.css">
<script src="/js/lib/select2-3.5.2.min.js"></script>
<script src="/js/select2-field.js"></script>
{% endblock %}
{% block breadcrumbs %}{{ block.super }}

View file

@ -5,9 +5,11 @@
{% block extrahead %}{{ block.super }}
<script type="text/javascript" src="{{ SECR_STATIC_URL }}js/utils.js"></script>
<script type="text/javascript" src="{{ SECR_STATIC_URL }}js/sessions.js"></script>
<script type="text/javascript" src="/js/lib/jquery.tokeninput.js"></script>
<script type="text/javascript" src="/js/tokenized-field.js"></script>
<link rel="stylesheet" type="text/css" href="/css/token-input.css"></link>
<link rel="stylesheet" href="/css/lib/select2.css">
<link rel="stylesheet" href="/css/lib/select2-bootstrap.css">
<script src="/js/lib/select2-3.5.2.min.js"></script>
<script src="/js/select2-field.js"></script>
{% endblock %}
{% block breadcrumbs %}{{ block.super }}

View file

@ -1,67 +0,0 @@
{% extends "base_site.html" %}
{% load ietf_filters %}
{% block title %}Sessions{% endblock %}
{% block extrahead %}{{ block.super }}
<script type="text/javascript" src="/static/js/utils.js"></script>
{% endblock %}
{% block breadcrumbs %}{{ block.super }}
&raquo; Sessions
{% endblock %}
{% block instructions %}
<a href="https://www.ietf.org/wg/request-tool-instructions.html" target="_blank">Instructions</a>
{% endblock %}
{% block content %}
<div class="module interim-container">
<h2>
Sessions Request Tool: IETF {{ meeting.meeting_num }}
{% if user|has_role:"Secretariat" %}
{% if is_locked %}
<span class="locked"><a href="{% url "sessions_tool_status" %}">Tool Status: Locked</a></span>
{% else %}
<span class="unlocked"><a href="{% url "sessions_tool_status" %}">Tool Status: Unlocked</a></span>
{% endif %}
{% endif %}
</h2>
<div class="inline-related">
<h3><b>Request New Session</b></h3>
<p>The list below includes those working groups that you currently chair which do not already have a session scheduled. You can click on an acronym to initiate a request for a new session at the upcoming IETF meeting. Click "Group will not meet" to send a notification that the group does not plan to meet.</p>
<ul>
{% for group in unscheduled_groups %}
<li>
<a href="{% url "sessions_new" acronym=group.acronym %}">{{ group.acronym }}</a>
{% if meeting in group.meetings_not_scheduled.all %}
<span class="required"> (Currently, this group does not plan to hold a session at IETF {{ meeting.meeting_num }})</span>
{% else %}
<!--
&nbsp;&nbsp;<a href="{% url "sessions_no_session" acronym=group.acronym %}"> - [ Group will not meet ]</a>
-->
{% endif %}
</li>
{% endfor %}
</ul>
</div> <!-- inline-related -->
<br>
<div class="inline-related">
<h2></h2>
<h3><b>Edit / Cancel Previously Requested Sessions</b></h3>
<p>The list below includes those working groups for which you or your co-chair has requested sessions at the upcoming IETF meeting. You can click on an acronym to initiate changes to a session, or cancel a session.</p>
<ul>
{% for session in scheduled_sessions %}
<li><a href="{% url "sessions_view" session_id=session.session_id %}">{{ session.group }} - {% if not session.ts_status_id %}{{ session.status }}{% else %}First Two Sessions:{{ session.status }}, Third Session:{{ session.ts_status }}{% endif %}</a></li>
{% empty %}
<i>NONE</i>
{% endfor %}
</ul>
</div> <!-- inline-related -->
</div> <!-- module -->
{% endblock %}
{% block footer-extras %}
{% include "includes/sessions_footer.html" %}
{% endblock %}

View file

@ -5,9 +5,11 @@
{% block extrahead %}{{ block.super }}
<script type="text/javascript" src="{{ SECR_STATIC_URL }}js/utils.js"></script>
<script type="text/javascript" src="{{ SECR_STATIC_URL }}js/sessions.js"></script>
<script type="text/javascript" src="/js/lib/jquery.tokeninput.js"></script>
<script type="text/javascript" src="/js/tokenized-field.js"></script>
<link rel="stylesheet" type="text/css" href="/css/token-input.css"></link>
<link rel="stylesheet" href="/css/lib/select2.css">
<link rel="stylesheet" href="/css/lib/select2-bootstrap.css">
<script src="/js/lib/select2-3.5.2.min.js"></script>
<script src="/js/select2-field.js"></script>
{% endblock %}
{% block breadcrumbs %}{{ block.super }}

View file

@ -14,7 +14,7 @@ except ImportError:
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
# a place to put ajax logs if necessary.
LOG_DIR = '/var/log/datatracker'
LOG_DIR = '/var/log/datatracker'
import sys
sys.path.append(os.path.abspath(BASE_DIR + "/.."))
@ -63,9 +63,9 @@ DATABASES = {
}
DATABASE_TEST_OPTIONS = {
# Comment this out if your database doesn't support InnoDB
'init_command': 'SET storage_engine=InnoDB',
}
# Comment this out if your database doesn't support InnoDB
'init_command': 'SET storage_engine=InnoDB',
}
# Local time zone for this installation. Choices can be found here:
# http://www.postgresql.org/docs/8.1/static/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE
@ -87,10 +87,11 @@ USE_I18N = False
USE_TZ = False
MEDIA_URL = 'http://www.ietf.org/'
MEDIA_URL = '//www.ietf.org/'
STATIC_URL = "/"
STATIC_ROOT = os.path.abspath(BASE_DIR + "/../static/")
STATIC_URL = STATIC_ROOT + '/'
WSGI_APPLICATION = "ietf.wsgi.application"
@ -147,8 +148,10 @@ SESSION_EXPIRE_AT_BROWSER_CLOSE = False
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
TEMPLATE_LOADERS = (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
('django.template.loaders.cached.Loader', (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
)),
'ietf.dbtemplate.template.Loader',
)
@ -199,6 +202,9 @@ INSTALLED_APPS = (
'django.contrib.humanize',
'django.contrib.messages',
'tastypie',
'widget_tweaks',
'typogrify',
'bootstrap3',
'ietf.person',
'ietf.name',
'ietf.group',
@ -233,6 +239,28 @@ INSTALLED_APPS = (
'ietf.dbtemplate',
)
# Settings for django-bootstrap3
# See http://django-bootstrap3.readthedocs.org/en/latest/settings.html
BOOTSTRAP3 = {
# Label class to use in horizontal forms
'horizontal_label_class': 'col-md-2',
# Field class to use in horiozntal forms
'horizontal_field_class': 'col-md-10',
# Set HTML required attribute on required fields
'set_required': True,
# Set placeholder attributes to label if no placeholder is provided
'set_placeholder': False,
# Class to indicate required
'form_required_class': 'bootstrap3-required',
# Class to indicate error
'form_error_class': 'bootstrap3-error',
}
INTERNAL_IPS = (
# AMS servers
'64.170.98.32',
@ -291,22 +319,22 @@ INTERNET_DRAFT_ARCHIVE_DIR = '/a/www/www6s/draft-archive'
MEETING_RECORDINGS_DIR = '/a/www/audio'
# Mailing list info URL for lists hosted on the IETF servers
MAILING_LIST_INFO_URL = "https://www.ietf.org/mailman/listinfo/%(list_addr)s"
MAILING_LIST_INFO_URL = "//www.ietf.org/mailman/listinfo/%(list_addr)s"
# Ideally, more of these would be local -- but since we don't support
# versions right now, we'll point to external websites
DOC_HREFS = {
"charter": "http://www.ietf.org/charter/{doc.name}-{doc.rev}.txt",
"draft": "http://www.ietf.org/archive/id/{doc.name}-{doc.rev}.txt",
"slides": "http://www.ietf.org/slides/{doc.name}-{doc.rev}",
"conflrev": "http://www.ietf.org/cr/{doc.name}-{doc.rev}.txt",
"statchg": "http://www.ietf.org/sc/{doc.name}-{doc.rev}.txt",
"charter": "//www.ietf.org/charter/{doc.name}-{doc.rev}.txt",
"draft": "//www.ietf.org/archive/id/{doc.name}-{doc.rev}.txt",
"slides": "//www.ietf.org/slides/{doc.name}-{doc.rev}",
"conflrev": "//www.ietf.org/cr/{doc.name}-{doc.rev}.txt",
"statchg": "//www.ietf.org/sc/{doc.name}-{doc.rev}.txt",
}
MEETING_DOC_HREFS = {
"agenda": "/meeting/{meeting}/agenda/{doc.group.acronym}/",
"minutes": "http://www.ietf.org/proceedings/{meeting}/minutes/{doc.external_url}",
"slides": "http://www.ietf.org/proceedings/{meeting}/slides/{doc.external_url}",
"minutes": "//www.ietf.org/proceedings/{meeting}/minutes/{doc.external_url}",
"slides": "//www.ietf.org/proceedings/{meeting}/slides/{doc.external_url}",
"recording": "{doc.external_url}",
}
@ -331,15 +359,15 @@ IANA_APPROVE_EMAIL = "drafts-approval@icann.org"
# Put real password in settings_local.py
IANA_SYNC_PASSWORD = "secret"
IANA_SYNC_CHANGES_URL = "https://datatracker.iana.org:4443/data-tracker/changes"
IANA_SYNC_PROTOCOLS_URL = "http://www.iana.org/protocols/"
IANA_SYNC_CHANGES_URL = "//datatracker.iana.org:4443/data-tracker/changes"
IANA_SYNC_PROTOCOLS_URL = "//www.iana.org/protocols/"
RFC_TEXT_RSYNC_SOURCE="ftp.rfc-editor.org::rfcs-text-only"
RFC_EDITOR_SYNC_PASSWORD="secret"
RFC_EDITOR_SYNC_NOTIFICATION_URL = "http://www.rfc-editor.org/parser/parser.php"
RFC_EDITOR_QUEUE_URL = "http://www.rfc-editor.org/queue2.xml"
RFC_EDITOR_INDEX_URL = "http://www.rfc-editor.org/rfc/rfc-index.xml"
RFC_EDITOR_SYNC_NOTIFICATION_URL = "//www.rfc-editor.org/parser/parser.php"
RFC_EDITOR_QUEUE_URL = "//www.rfc-editor.org/queue2.xml"
RFC_EDITOR_INDEX_URL = "//www.rfc-editor.org/rfc/rfc-index.xml"
# Liaison Statement Tool settings
LIAISON_UNIVERSAL_FROM = 'Liaison Statement Management Tool <lsmt@' + IETF_DOMAIN + '>'
@ -375,7 +403,7 @@ INTERNET_DRAFT_DAYS_TO_EXPIRE = 185
IDSUBMIT_REPOSITORY_PATH = INTERNET_DRAFT_PATH
IDSUBMIT_STAGING_PATH = '/a/www/www6s/staging/'
IDSUBMIT_STAGING_URL = 'http://www.ietf.org/staging/'
IDSUBMIT_STAGING_URL = '//www.ietf.org/staging/'
IDSUBMIT_IDNITS_BINARY = '/a/www/ietf-datatracker/scripts/idnits'
IDSUBMIT_MAX_PLAIN_DRAFT_SIZE = 6291456 # Max size of the txt draft in bytes
@ -409,7 +437,7 @@ TZDATA_ICS_PATH = BASE_DIR + '/../vzic/zoneinfo/'
CHANGELOG_PATH = '/www/ietf-datatracker/web/changelog'
SECR_BLUE_SHEET_PATH = '/a/www/ietf-datatracker/documents/blue_sheet.rtf'
SECR_BLUE_SHEET_URL = 'https://datatracker.ietf.org/documents/blue_sheet.rtf'
SECR_BLUE_SHEET_URL = '//datatracker.ietf.org/documents/blue_sheet.rtf'
SECR_INTERIM_LISTING_DIR = '/a/www/www6/meeting/interim'
SECR_MAX_UPLOAD_SIZE = 40960000
SECR_PROCEEDINGS_DIR = '/a/www/www6s/proceedings/'
@ -453,6 +481,9 @@ GROUP_ALIAS_DOMAIN = IETF_DOMAIN
# Path to the email alias lists. Used by ietf.utils.aliases
DRAFT_ALIASES_PATH = "/a/postfix/draft-aliases"
DRAFT_VIRTUAL_PATH = "/a/postfix/draft-virtual"
# Set debug apps in DEV_APPS settings_local
DEV_APPS = ()
DRAFT_VIRTUAL_DOMAIN = "virtual.ietf.org"
GROUP_ALIASES_PATH = "/a/postfix/group-aliases"
@ -465,11 +496,17 @@ POSTCONFIRM_PATH = "/a/postconfirm/test-wrapper"
# sensitive or site-specific changes. DO NOT commit settings_local.py to svn.
from settings_local import * # pyflakes:ignore
# Add DEV_APPS to INSTALLED_APPS
INSTALLED_APPS += DEV_APPS
# We provide a secret key only for test and development modes. It's
# absolutely vital that django fails to start in production mode unless a
# secret key has been provided elsewhere, not in this file which is
# publicly available, for instance from the source repository.
if SERVER_MODE != 'production':
# stomp out the cached template loader, it's annoying
TEMPLATE_LOADERS = tuple(l for e in TEMPLATE_LOADERS for l in (e[1] if isinstance(e, tuple) and "cached.Loader" in e[0] else (e,)))
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.dummy.DummyCache',

View file

@ -47,20 +47,20 @@ class UploadForm(forms.Form):
ietf_monday = Meeting.get_ietf_monday()
if now.date() >= (first_cut_off-timedelta(days=settings.CUTOFF_WARNING_DAYS)) and now.date() < first_cut_off:
self.cutoff_warning = ( 'The pre-meeting cut-off date for new documents (i.e., version -00 Internet-Drafts) is %s at %02sh UTC.<br/>' % (first_cut_off, settings.CUTOFF_HOUR) +
'The pre-meeting cut-off date for revisions to existing documents is %s at %02sh UTC.<br/>' % (second_cut_off, settings.CUTOFF_HOUR) )
self.cutoff_warning = ( 'The pre-meeting cut-off date for new documents (i.e., version -00 Internet-Drafts) is %s at %02sh UTC.' % (first_cut_off, settings.CUTOFF_HOUR) +
'The pre-meeting cut-off date for revisions to existing documents is %s at %02sh UTC.' % (second_cut_off, settings.CUTOFF_HOUR) )
elif now.date() >= first_cut_off and now.date() < second_cut_off: # We are in the first_cut_off
if now.date() == first_cut_off and now.hour < settings.CUTOFF_HOUR:
self.cutoff_warning = 'The pre-meeting cut-off date for new documents (i.e., version -00 Internet-Drafts) is %s, at %02sh UTC. After that, you will not be able to submit a new document until %s, at %sh UTC' % (first_cut_off, settings.CUTOFF_HOUR, ietf_monday, settings.CUTOFF_HOUR, )
else: # No 00 version allowed
self.cutoff_warning = 'The pre-meeting cut-off date for new documents (i.e., version -00 Internet-Drafts) was %s at %sh UTC. You will not be able to submit a new document until %s, at %sh UTC.<br>You can still submit a version -01 or higher Internet-Draft until %sh UTC, %s' % (first_cut_off, settings.CUTOFF_HOUR, ietf_monday, settings.CUTOFF_HOUR, settings.CUTOFF_HOUR, second_cut_off, )
self.cutoff_warning = 'The pre-meeting cut-off date for new documents (i.e., version -00 Internet-Drafts) was %s at %sh UTC. You will not be able to submit a new document until %s, at %sh UTC. You can still submit a version -01 or higher Internet-Draft until %sh UTC, %s' % (first_cut_off, settings.CUTOFF_HOUR, ietf_monday, settings.CUTOFF_HOUR, settings.CUTOFF_HOUR, second_cut_off, )
self.in_first_cut_off = True
elif now.date() >= second_cut_off and now.date() < ietf_monday:
if now.date() == second_cut_off and now.hour < settings.CUTOFF_HOUR: # We are in the first_cut_off yet
self.cutoff_warning = 'The pre-meeting cut-off date for new documents (i.e., version -00 Internet-Drafts) was %s at %02sh UTC. You will not be able to submit a new document until %s, at %02sh UTC.<br>The I-D submission tool will be shut down at %02sh UTC today, and reopened at %02sh UTC on %s' % (first_cut_off, settings.CUTOFF_HOUR, ietf_monday, settings.CUTOFF_HOUR, settings.CUTOFF_HOUR, settings.CUTOFF_HOUR, ietf_monday)
self.cutoff_warning = 'The pre-meeting cut-off date for new documents (i.e., version -00 Internet-Drafts) was %s at %02sh UTC. You will not be able to submit a new document until %s, at %02sh UTC. The I-D submission tool will be shut down at %02sh UTC today, and reopened at %02sh UTC on %s' % (first_cut_off, settings.CUTOFF_HOUR, ietf_monday, settings.CUTOFF_HOUR, settings.CUTOFF_HOUR, settings.CUTOFF_HOUR, ietf_monday)
self.in_first_cut_off = True
else: # Completely shut down of the tool
self.cutoff_warning = 'The cut-off time for the I-D submission was %02dh UTC, %s.<br>The I-D submission tool will be reopened at %02dh local time at the IETF meeting location, %s.' % (settings.CUTOFF_HOUR, second_cut_off, settings.CUTOFF_HOUR, ietf_monday)
self.cutoff_warning = 'The cut-off time for the I-D submission was %02dh UTC, %s. The I-D submission tool will be reopened at %02dh local time at the IETF meeting location, %s.' % (settings.CUTOFF_HOUR, second_cut_off, settings.CUTOFF_HOUR, ietf_monday)
self.shutdown = True
def clean_file(self, field_name, parser_class):
@ -116,7 +116,7 @@ class UploadForm(forms.Form):
# check existing
existing = Submission.objects.filter(name=self.parsed_draft.filename, rev=self.parsed_draft.revision).exclude(state__in=("posted", "cancel"))
if existing:
raise forms.ValidationError(mark_safe('Submission with same name and revision is currently being processed. <a href="%s">Check the status here</a>' % urlreverse("submit_submission_status", kwargs={ 'submission_id': existing[0].pk })))
raise forms.ValidationError(mark_safe('Submission with same name and revision is currently being processed. <a href="%s">Check the status here.</a>' % urlreverse("submit_submission_status", kwargs={ 'submission_id': existing[0].pk })))
# cut-off
if self.parsed_draft.revision == '00' and self.in_first_cut_off:
@ -239,7 +239,7 @@ class EditSubmissionForm(forms.ModelForm):
pages = forms.IntegerField(required=True)
abstract = forms.CharField(widget=forms.Textarea, required=True)
note = forms.CharField(label=mark_safe(u'Comment to<br/> the Secretariat'), widget=forms.Textarea, required=False)
note = forms.CharField(label=mark_safe(u'Comment to the Secretariat'), widget=forms.Textarea, required=False)
class Meta:
model = Submission

View file

@ -31,7 +31,7 @@ class Submission(models.Model):
abstract = models.TextField(blank=True)
rev = models.CharField(max_length=3, blank=True)
pages = models.IntegerField(null=True, blank=True)
authors = models.TextField(blank=True, help_text="List of author names and emails, one author per line, e.g. \"John Doe &lt;john@example.org&gt;\"")
authors = models.TextField(blank=True, help_text="List of author names and emails, one author per line, e.g. \"John Doe &lt;john@example.org&gt;\".")
note = models.TextField(blank=True)
replaces = models.CharField(max_length=255, blank=True)
@ -41,7 +41,7 @@ class Submission(models.Model):
document_date = models.DateField(null=True, blank=True)
submission_date = models.DateField(default=datetime.date.today)
submitter = models.CharField(max_length=255, blank=True, help_text="Name and email of submitter, e.g. \"John Doe &lt;john@example.org&gt;\"")
submitter = models.CharField(max_length=255, blank=True, help_text="Name and email of submitter, e.g. \"John Doe &lt;john@example.org&gt;\".")
idnits_message = models.TextField(blank=True)

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