Merged in branch/amsl/liaisons/6.4.1@10160 from rcross@amsl.com, bringing in the new liaison tool.

- Legacy-Id: 10161
This commit is contained in:
Henrik Levkowetz 2015-10-09 19:48:21 +00:00
52 changed files with 4574 additions and 2677 deletions

View file

@ -54,6 +54,7 @@ def has_role(user, role_names, *args, **kwargs):
"IETF Chair": Q(person=person, name="chair", group__acronym="ietf"),
"IRTF Chair": Q(person=person, name="chair", group__acronym="irtf"),
"IAB Chair": Q(person=person, name="chair", group__acronym="iab"),
"IAB Executive Director": Q(person=person, name="execdir", group__acronym="iab"),
"IAB Group Chair": Q(person=person, name="chair", group__type="iab", group__state="active"),
"WG Chair": Q(person=person,name="chair", group__type="wg", group__state__in=["active","bof", "proposed"]),
"WG Secretary": Q(person=person,name="secr", group__type="wg", group__state__in=["active","bof", "proposed"]),
@ -64,6 +65,8 @@ def has_role(user, role_names, *args, **kwargs):
"Nomcom Chair": Q(person=person, name="chair", group__type="nomcom", group__state="active", group__acronym__icontains=kwargs.get('year', '0000')),
"Nomcom Advisor": Q(person=person, name="advisor", group__type="nomcom", group__state="active", group__acronym__icontains=kwargs.get('year', '0000')),
"Nomcom": Q(person=person, group__type="nomcom", group__state="active", group__acronym__icontains=kwargs.get('year', '0000')),
"Liaison Manager": Q(person=person,name="liaiman",group__type="sdo",group__state="active", ),
"Authorized Individual": Q(person=person,name="auth",group__type="sdo",group__state="active", ),
}
filter_expr = Q()

View file

@ -1,146 +0,0 @@
from ietf.person.models import Person
from ietf.group.models import Role
def proxy_personify_role(role):
"""Return person from role with an old-school email() method using
email from role."""
p = role.person
p.email = lambda: (p.plain_name(), role.email.address)
return p
LIAISON_EDIT_GROUPS = ['Secretariat'] # this is not working anymore, refers to old auth model
def get_ietf_chair():
try:
return proxy_personify_role(Role.objects.get(name="chair", group__acronym="ietf"))
except Role.DoesNotExist:
return None
def get_iesg_chair():
return get_ietf_chair()
def get_iab_chair():
try:
return proxy_personify_role(Role.objects.get(name="chair", group__acronym="iab"))
except Role.DoesNotExist:
return None
def get_irtf_chair():
try:
return proxy_personify_role(Role.objects.get(name="chair", group__acronym="irtf"))
except Role.DoesNotExist:
return None
def get_iab_executive_director():
try:
return proxy_personify_role(Role.objects.get(name="execdir", group__acronym="iab"))
except Person.DoesNotExist:
return None
def get_person_for_user(user):
if not user.is_authenticated():
return None
try:
p = user.person
p.email = lambda: (p.plain_name(), p.email_address())
return p
except Person.DoesNotExist:
return None
def is_areadirector(person):
return bool(Role.objects.filter(person=person, name="ad", group__state="active", group__type="area"))
def is_wgchair(person):
return bool(Role.objects.filter(person=person, name="chair", group__state="active", group__type="wg"))
def is_wgsecretary(person):
return bool(Role.objects.filter(person=person, name="sec", group__state="active", group__type="wg"))
def is_ietfchair(person):
return bool(Role.objects.filter(person=person, name="chair", group__acronym="ietf"))
def is_iabchair(person):
return bool(Role.objects.filter(person=person, name="chair", group__acronym="iab"))
def is_iab_executive_director(person):
return bool(Role.objects.filter(person=person, name="execdir", group__acronym="iab"))
def is_irtfchair(person):
return bool(Role.objects.filter(person=person, name="chair", group__acronym="irtf"))
def can_add_outgoing_liaison(user):
person = get_person_for_user(user)
if not person:
return False
if (is_areadirector(person) or is_wgchair(person) or
is_wgsecretary(person) or is_ietfchair(person) or
is_iabchair(person) or is_iab_executive_director(person) or
is_sdo_liaison_manager(person) or is_secretariat(user)):
return True
return False
def is_sdo_liaison_manager(person):
return bool(Role.objects.filter(person=person, name="liaiman", group__type="sdo"))
def is_sdo_authorized_individual(person):
return bool(Role.objects.filter(person=person, name="auth", group__type="sdo"))
def is_secretariat(user):
if isinstance(user, basestring):
return False
return user.is_authenticated() and bool(Role.objects.filter(person__user=user, name="secr", group__acronym="secretariat"))
def can_add_incoming_liaison(user):
person = get_person_for_user(user)
if not person:
return False
if (is_sdo_liaison_manager(person) or
is_sdo_authorized_individual(person) or
is_secretariat(user)):
return True
return False
def can_add_liaison(user):
return can_add_incoming_liaison(user) or can_add_outgoing_liaison(user)
def is_sdo_manager_for_outgoing_liaison(person, liaison):
if liaison.from_group and liaison.from_group.type_id == "sdo":
return bool(liaison.from_group.role_set.filter(name="liaiman", person=person))
return False
def is_sdo_manager_for_incoming_liaison(person, liaison):
if liaison.to_group and liaison.to_group.type_id == "sdo":
return bool(liaison.to_group.role_set.filter(name="liaiman", person=person))
return False
def can_edit_liaison(user, liaison):
if is_secretariat(user):
return True
person = get_person_for_user(user)
if is_sdo_liaison_manager(person):
return (is_sdo_manager_for_outgoing_liaison(person, liaison) or
is_sdo_manager_for_incoming_liaison(person, liaison))
return False

View file

@ -1,10 +1,44 @@
from django.contrib import admin
from django.core.urlresolvers import reverse
from ietf.liaisons.models import LiaisonStatement
from ietf.liaisons.models import ( LiaisonStatement, LiaisonStatementEvent,
LiaisonStatementGroupContacts, RelatedLiaisonStatement, LiaisonStatementAttachment )
class RelatedLiaisonStatementInline(admin.TabularInline):
model = RelatedLiaisonStatement
fk_name = 'source'
raw_id_fields = ['target']
extra = 1
class LiaisonStatementAttachmentInline(admin.TabularInline):
model = LiaisonStatementAttachment
raw_id_fields = ['document']
extra = 1
class LiaisonStatementAdmin(admin.ModelAdmin):
list_display = ['id', 'title', 'from_name', 'to_name', 'submitted', 'purpose', 'related_to']
list_display = ['id', 'title', 'submitted', 'from_groups_short_display', 'purpose', 'related_to']
list_display_links = ['id', 'title']
ordering = ('title', )
raw_id_fields = ('from_contact', 'related_to', 'from_group', 'to_group', 'attachments')
raw_id_fields = ('from_contact', 'attachments', 'from_groups', 'to_groups')
#filter_horizontal = ('from_groups', 'to_groups')
inlines = [ RelatedLiaisonStatementInline, LiaisonStatementAttachmentInline ]
def related_to(self, obj):
return '<br />'.join(['<a href="%s">%s</a>' % (reverse('admin:liaisons_liaisonstatement_change', None, (i.target.id, )), str(i.target)) for i in obj.source_of_set.select_related('target').all()])
related_to.allow_tags = True
class LiaisonStatementEventAdmin(admin.ModelAdmin):
list_display = ["statement", "type", "by", "time"]
search_fields = ["statement__title", "by__name"]
raw_id_fields = ["statement", "by"]
class LiaisonStatementGroupContactsAdmin(admin.ModelAdmin):
list_display = ["group", "contacts"]
raw_id_fields = ["group"]
search_fields = ["group__acronym", "contacts"]
ordering = ["group__name"]
admin.site.register(LiaisonStatement, LiaisonStatementAdmin)
admin.site.register(LiaisonStatementEvent, LiaisonStatementEventAdmin)
admin.site.register(LiaisonStatementGroupContacts, LiaisonStatementGroupContactsAdmin)

View file

@ -16,7 +16,7 @@ from ietf.liaisons.models import LiaisonStatement
# to construct a queryset.
class LiaisonStatementsFeed(Feed):
feed_type = Atom1Feed
link = reverse_lazy("liaison_list")
link = reverse_lazy("ietf.liaisons.views.liaison_list")
description_template = "liaisons/feed_item_description.html"
def get_object(self, request, kind, search=None):
@ -24,8 +24,8 @@ class LiaisonStatementsFeed(Feed):
if kind == 'recent':
obj['title'] = 'Recent Liaison Statements'
obj['limit'] = 15
return obj
obj['limit'] = 15
return obj
if kind == 'from':
if not search:
@ -33,7 +33,7 @@ class LiaisonStatementsFeed(Feed):
try:
group = Group.objects.get(acronym=search)
obj['filter'] = { 'from_group': group }
obj['filter'] = { 'from_groups': group }
obj['title'] = u'Liaison Statements from %s' % group.name
return obj
except Group.DoesNotExist:
@ -54,46 +54,47 @@ class LiaisonStatementsFeed(Feed):
if not search:
raise FeedDoesNotExist
obj['filter'] = dict(to_name__icontains=search)
obj['title'] = 'Liaison Statements where to matches %s' % search
group = Group.objects.get(acronym=search)
obj['filter'] = { 'to_groups': group }
obj['title'] = u'Liaison Statements to %s' % group.name
return obj
if kind == 'subject':
if kind == 'subject':
if not search:
raise FeedDoesNotExist
raise FeedDoesNotExist
obj['q'] = [ Q(title__icontains=search) | Q(attachments__title__icontains=search) ]
obj['title'] = 'Liaison Statements where subject matches %s' % search
return obj
raise FeedDoesNotExist
raise FeedDoesNotExist
def items(self, obj):
qs = LiaisonStatement.objects.all().order_by("-submitted")
if obj.has_key('q'):
qs = qs.filter(*obj['q'])
if obj.has_key('filter'):
qs = qs.filter(**obj['filter'])
if obj.has_key('limit'):
qs = qs[:obj['limit']]
return qs
qs = LiaisonStatement.objects.all().order_by("-id")
if obj.has_key('q'):
qs = qs.filter(*obj['q'])
if obj.has_key('filter'):
qs = qs.filter(**obj['filter'])
if obj.has_key('limit'):
qs = qs[:obj['limit']]
return qs
def title(self, obj):
return obj['title']
return obj['title']
def description(self, obj):
return self.title(obj)
return self.title(obj)
def item_title(self, item):
return render_to_string("liaisons/liaison_title.html", { 'liaison': item }).strip()
def item_link(self, item):
return urlreverse("liaison_detail", kwargs={ "object_id": item.pk })
return urlreverse("ietf.liaisons.views.liaison_detail", kwargs={ "object_id": item.pk })
def item_pubdate(self, item):
# this method needs to return a datetime instance, even
# though the database has only date, not time
# though the database has only date, not time
return item.submitted
def item_author_name(self, item):
return item.from_name

View file

@ -7,44 +7,65 @@ 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])
return json.dumps([{ "id": o.pk, "text":u"[{}] {}".format(o.pk, escape(o.title)) } for o in objs])
class SearchableLiaisonStatementField(forms.IntegerField):
def select2_id_group_json(objs):
return json.dumps([{ "id": o.pk, "text": escape(o.acronym) } for o in objs])
class SearchableLiaisonStatementsField(forms.CharField):
"""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)
def __init__(self,
max_entries = None,
hint_text="Type in title to search for document",
model = LiaisonStatement,
*args, **kwargs):
kwargs["max_length"] = 10000
self.model = model
self.max_entries = max_entries
self.widget.attrs["class"] = "select2-field"
super(SearchableLiaisonStatementsField, self).__init__(*args, **kwargs)
self.widget.attrs["class"] = "select2-field form-control"
self.widget.attrs["data-placeholder"] = hint_text
self.widget.attrs["data-max-entries"] = 1
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 = None
elif isinstance(value, LiaisonStatement):
value = value
else:
value = LiaisonStatement.objects.exclude(approved=None).filter(pk=value).first()
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)
if isinstance(value, LiaisonStatement):
value = [value]
self.widget.attrs["data-pre"] = select2_id_liaison_json([value] if value else [])
self.widget.attrs["data-pre"] = select2_id_liaison_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_liaison_statements")
self.widget.attrs["data-ajax-url"] = urlreverse("ietf.liaisons.views.ajax_select2_search_liaison_statements")
return value
return u",".join(unicode(o.pk) for o in value)
def clean(self, value):
value = super(SearchableLiaisonStatementField, self).clean(value)
value = super(SearchableLiaisonStatementsField, self).clean(value)
pks = self.parse_select2_value(value)
if value == None:
return None
objs = self.model.objects.filter(pk__in=pks)
obj = LiaisonStatement.objects.filter(pk=value).first()
if not obj and self.required:
raise forms.ValidationError(u"You must select a value.")
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 groups: {pks}.".format(pks=", ".join(failed_pks)))
return obj
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

View file

@ -1,43 +1,187 @@
import datetime, os
import operator
import six
from email.utils import parseaddr
from form_utils.forms import BetterModelForm
from django import forms
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.db.models.query import QuerySet
from django.forms.util import ErrorList
from django.db.models import Q
from django.forms.widgets import RadioFieldRenderer
from django.core.validators import validate_email, ValidationError
from django.template.loader import render_to_string
from django.utils.html import format_html
from django.utils.encoding import force_text
from django.utils.safestring import mark_safe
import debug # pyflakes:ignore
from ietf.liaisons.accounts import (can_add_outgoing_liaison, can_add_incoming_liaison,
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)
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.ietfauth.utils import has_role
from ietf.name.models import DocRelationshipName
from ietf.liaisons.utils import get_person_for_user,is_authorized_individual
from ietf.liaisons.widgets import ButtonWidget,ShowAttachmentsWidget
from ietf.liaisons.models import (LiaisonStatement,
LiaisonStatementEvent,LiaisonStatementAttachment,LiaisonStatementPurposeName)
from ietf.liaisons.fields import SearchableLiaisonStatementsField
from ietf.group.models import Group
from ietf.person.models import Email
from ietf.person.fields import SearchableEmailField
from ietf.doc.models import Document
from ietf.utils.fields import DatepickerDateField
'''
NOTES:
Authorized individuals are people (in our Person table) who are authorized to send
messages on behalf of some other group - they have a formal role in the other group,
whereas the liasion manager has a formal role with the IETF (or more correctly,
with the IAB).
'''
class LiaisonForm(forms.Form):
person = forms.ModelChoiceField(Person.objects.all())
from_field = forms.ChoiceField(widget=FromWidget, label=u'From')
replyto = forms.CharField(label=u'Reply to')
organization = forms.ChoiceField()
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.')
purpose = forms.ChoiceField()
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)
# -------------------------------------------------
# Helper Functions
# -------------------------------------------------
def liaison_manager_sdos(person):
return Group.objects.filter(type="sdo", state="active", role__person=person, role__name="liaiman").distinct()
def get_internal_choices(user):
'''Returns the set of internal IETF groups the user has permissions for, as a list
of choices suitable for use in a select widget. If user == None, all active internal
groups are included.'''
choices = []
groups = get_groups_for_person(user.person if user else None)
main = [ (g.pk, 'The {}'.format(g.acronym.upper())) for g in groups.filter(acronym__in=('ietf','iesg','iab')) ]
areas = [ (g.pk, '{} - {}'.format(g.acronym,g.name)) for g in groups.filter(type='area') ]
wgs = [ (g.pk, '{} - {}'.format(g.acronym,g.name)) for g in groups.filter(type='wg') ]
choices.append(('Main IETF Entities', main))
choices.append(('IETF Areas', areas))
choices.append(('IETF Working Groups', wgs ))
return choices
def get_groups_for_person(person):
'''Returns queryset of internal Groups the person has interesting roles in.
This is a refactor of IETFHierarchyManager.get_entities_for_person(). If Person
is None or Secretariat or Liaison Manager all internal IETF groups are returned.
'''
if person == None or has_role(person.user, "Secretariat") or has_role(person.user, "Liaison Manager"):
# collect all internal IETF groups
queries = [Q(acronym__in=('ietf','iesg','iab')),
Q(type='area',state='active'),
Q(type='wg',state='active')]
else:
# Interesting roles, as Group queries
queries = [Q(role__person=person,role__name='chair',acronym='ietf'),
Q(role__person=person,role__name__in=('chair','execdir'),acronym='iab'),
Q(role__person=person,role__name='ad',type='area',state='active'),
Q(role__person=person,role__name__in=('chair','secretary'),type='wg',state='active')]
return Group.objects.filter(reduce(operator.or_,queries)).order_by('acronym').distinct()
def liaison_form_factory(request, type=None, **kwargs):
"""Returns appropriate Liaison entry form"""
user = request.user
if kwargs.get('instance',None):
return EditLiaisonForm(user, **kwargs)
elif type == 'incoming':
return IncomingLiaisonForm(user, **kwargs)
elif type == 'outgoing':
return OutgoingLiaisonForm(user, **kwargs)
return None
def validate_emails(value):
'''Custom validator for emails'''
value = value.strip() # strip whitespace
if '\r\n' in value: # cc_contacts has newlines
value = value.replace('\r\n',',')
emails = value.split(',')
for email in emails:
name, addr = parseaddr(email)
try:
validate_email(addr)
except ValidationError:
raise forms.ValidationError('Invalid email address: %s' % addr)
try:
addr.encode('ascii')
except UnicodeEncodeError as e:
raise forms.ValidationError('Invalid email address: %s (check character %d)' % (addr,e.start))
# -------------------------------------------------
# Form Classes
# -------------------------------------------------
class RadioRenderer(RadioFieldRenderer):
def render(self):
output = []
for widget in self:
output.append(format_html(force_text(widget)))
return mark_safe('\n'.join(output))
class SearchLiaisonForm(forms.Form):
text = forms.CharField(required=False)
scope = forms.ChoiceField(choices=(("all", "All text fields"), ("title", "Title field")), required=False, initial='title', widget=forms.RadioSelect(renderer=RadioRenderer))
source = forms.CharField(required=False)
destination = forms.CharField(required=False)
start_date = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1" }, label='Start date', required=False)
end_date = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1" }, label='End date', required=False)
def get_results(self):
results = LiaisonStatement.objects.filter(state__slug='posted')
if self.is_bound:
query = self.cleaned_data.get('text')
if query:
q = (Q(title__icontains=query) | Q(other_identifiers__icontains=query) | Q(body__icontains=query) |
Q(attachments__title__icontains=query,liaisonstatementattachment__removed=False) |
Q(technical_contacts__icontains=query) | Q(action_holder_contacts__icontains=query) |
Q(cc_contacts=query) | Q(response_contacts__icontains=query))
results = results.filter(q)
source = self.cleaned_data.get('source')
if source:
results = results.filter(Q(from_groups__name__icontains=source) | Q(from_groups__acronym__iexact=source))
destination = self.cleaned_data.get('destination')
if destination:
results = results.filter(Q(to_groups__name__icontains=destination) | Q(to_groups__acronym__iexact=destination))
start_date = self.cleaned_data.get('start_date')
end_date = self.cleaned_data.get('end_date')
events = None
if start_date:
events = LiaisonStatementEvent.objects.filter(type='posted', time__gte=start_date)
if end_date:
events = events.filter(time__lte=end_date)
elif end_date:
events = LiaisonStatementEvent.objects.filter(type='posted', time__lte=end_date)
if events:
results = results.filter(liaisonstatementevent__in=events)
results = results.distinct().order_by('title')
return results
class CustomModelMultipleChoiceField(forms.ModelMultipleChoiceField):
'''If value is a QuerySet, return it as is (for use in widget.render)'''
def prepare_value(self, value):
if isinstance(value, QuerySet):
return value
if (hasattr(value, '__iter__') and
not isinstance(value, six.text_type) and
not hasattr(value, '_meta')):
return [super(forms.ModelMultipleChoiceField, self).prepare_value(v) for v in value]
return super(forms.ModelMultipleChoiceField, self).prepare_value(value)
class LiaisonModelForm(BetterModelForm):
'''Specify fields which require a custom widget or that are not part of the model'''
from_groups = forms.ModelMultipleChoiceField(queryset=Group.objects.all(),label=u'Groups')
from_contact = forms.EmailField()
to_groups = forms.ModelMultipleChoiceField(queryset=Group.objects,label=u'Groups')
deadline = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1" }, label='Deadline', required=True)
related_to = SearchableLiaisonStatementsField(label=u'Related Liaison Statement', required=False)
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)
attachments = CustomModelMultipleChoiceField(queryset=Document.objects,label='Attachments', widget=ShowAttachmentsWidget, required=False)
attach_title = forms.CharField(label='Title', required=False)
attach_file = forms.FileField(label='File', required=False)
attach_button = forms.CharField(label='',
@ -45,100 +189,62 @@ class LiaisonForm(forms.Form):
require=['id_attach_title', 'id_attach_file'],
required_label='title and file'),
required=False)
fieldsets = [('From', ('from_field', 'replyto')),
('To', ('organization', 'to_poc')),
('Other email addresses', ('response_contact', 'technical_contact', 'cc1')),
('Purpose', ('purpose', 'deadline_date')),
('Reference', ('related_to', )),
('Liaison Statement', ('title', 'submitted_date', 'body', 'attachments')),
('Add attachment', ('attach_title', 'attach_file', 'attach_button')),
]
class Meta:
model = LiaisonStatement
exclude = ('attachments','state','from_name','to_name')
fieldsets = [('From', {'fields': ['from_groups','from_contact', 'response_contacts'], 'legend': ''}),
('To', {'fields': ['to_groups','to_contacts'], 'legend': ''}),
('Other email addresses', {'fields': ['technical_contacts','action_holder_contacts','cc_contacts'], 'legend': ''}),
('Purpose', {'fields':['purpose', 'deadline'], 'legend': ''}),
('Reference', {'fields': ['other_identifiers','related_to'], 'legend': ''}),
('Liaison Statement', {'fields': ['title', 'submitted_date', 'body', 'attachments'], 'legend': ''}),
('Add attachment', {'fields': ['attach_title', 'attach_file', 'attach_button'], 'legend': ''})]
def __init__(self, user, *args, **kwargs):
super(LiaisonModelForm, self).__init__(*args, **kwargs)
self.user = user
self.fake_person = None
self.person = get_person_for_user(user)
if kwargs.get('data', None):
if is_secretariat(self.user) and 'from_fake_user' in kwargs['data'].keys():
self.fake_person = Person.objects.get(pk=kwargs['data']['from_fake_user'])
kwargs['data'].update({'person': self.fake_person.pk})
else:
kwargs['data'].update({'person': self.person.pk})
self.is_new = not self.instance.pk
self.instance = kwargs.pop("instance", None)
super(LiaisonForm, self).__init__(*args, **kwargs)
# now copy in values from instance, like a ModelForm
if self.instance:
self.initial["person"] = self.instance.from_contact.person_id if self.instance.from_contact else None
self.initial["replyto"] = self.instance.reply_to
self.initial["to_poc"] = self.instance.to_contact
self.initial["response_contact"] = self.instance.response_contact
self.initial["technical_contact"] = self.instance.technical_contact
self.initial["cc1"] = self.instance.cc
self.initial["purpose"] = self.instance.purpose.order
self.initial["deadline_date"] = self.instance.deadline
self.initial["submitted_date"] = self.instance.submitted.date() if self.instance.submitted else None
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
if "approved" in self.fields:
self.initial["approved"] = bool(self.instance.approved)
self.fields["from_groups"].widget.attrs["placeholder"] = "Type in name to search for group"
self.fields["to_groups"].widget.attrs["placeholder"] = "Type in name to search for group"
self.fields["to_contacts"].label = 'Contacts'
self.fields["purpose"].choices = [("", "---------")] + [(str(l.order), l.name) for l in LiaisonStatementPurposeName.objects.all()]
self.hm = IETFHM
self.set_from_field()
self.set_replyto_field()
self.set_organization_field()
# add email validators
for field in ['from_contact','to_contacts','technical_contacts','action_holder_contacts','cc_contacts']:
if field in self.fields:
self.fields[field].validators.append(validate_emails)
def __unicode__(self):
return self.as_div()
self.set_from_fields()
self.set_to_fields()
def get_post_only(self):
return False
def clean_from_contact(self):
contact = self.cleaned_data.get('from_contact')
try:
email = Email.objects.get(address=contact)
except ObjectDoesNotExist:
raise forms.ValidationError('Email address does not exist')
return email
def set_required_fields(self):
purpose = self.data.get('purpose', None)
if purpose in ['1', '2']:
self.fields['deadline_date'].required = True
else:
self.fields['deadline_date'].required = False
def clean_cc_contacts(self):
'''Return a comma separated list of addresses'''
cc_contacts = self.cleaned_data.get('cc_contacts')
return cc_contacts.replace('\r\n',',')
def reset_required_fields(self):
self.fields['deadline_date'].required = True
def clean(self):
if not self.cleaned_data.get('body', None) and not self.has_attachments():
self._errors['body'] = ErrorList([u'You must provide a body or attachment files'])
self._errors['attachments'] = ErrorList([u'You must provide a body or attachment files'])
def set_from_field(self):
assert NotImplemented
def set_replyto_field(self):
self.fields['replyto'].initial = self.person.email()[1]
def set_organization_field(self):
assert NotImplemented
def as_div(self):
return render_to_string('liaisons/liaisonform.html', {'form': self})
def get_fieldsets(self):
if not self.fieldsets:
yield dict(name=None, fields=self)
else:
for fieldset, fields in self.fieldsets:
fieldset_dict = dict(name=fieldset, fields=[])
for field_name in fields:
if field_name in self.fields:
fieldset_dict['fields'].append(self[field_name])
if not fieldset_dict['fields']:
# if there is no fields in this fieldset, we continue to next fieldset
continue
yield fieldset_dict
# if purpose=response there must be a related statement
purpose = LiaisonStatementPurposeName.objects.get(slug='response')
if self.cleaned_data.get('purpose') == purpose and not self.cleaned_data.get('related_to'):
self._errors['related_to'] = ErrorList([u'You must provide a related statement when purpose is In Response'])
return self.cleaned_data
def full_clean(self):
self.set_required_fields()
super(LiaisonForm, self).full_clean()
super(LiaisonModelForm, self).full_clean()
self.reset_required_fields()
def has_attachments(self):
@ -147,120 +253,41 @@ class LiaisonForm(forms.Form):
return True
return False
def check_email(self, value):
if not value:
return
emails = value.split(',')
for email in emails:
name, addr = parseaddr(email)
try:
validate_email(addr)
except ValidationError:
raise forms.ValidationError('Invalid email address: %s' % addr)
try:
addr.encode('ascii')
except UnicodeEncodeError as e:
raise forms.ValidationError('Invalid email address: %s (check character %d)' % (addr,e.start))
def clean_response_contact(self):
value = self.cleaned_data.get('response_contact', None)
self.check_email(value)
return value
def clean_technical_contact(self):
value = self.cleaned_data.get('technical_contact', None)
self.check_email(value)
return value
def clean_reply_to(self):
value = self.cleaned_data.get('reply_to', None)
self.check_email(value)
return value
def clean(self):
if not self.cleaned_data.get('body', None) and not self.has_attachments():
self._errors['body'] = ErrorList([u'You must provide a body or attachment files'])
self._errors['attachments'] = ErrorList([u'You must provide a body or attachment files'])
return self.cleaned_data
def get_from_entity(self):
organization_key = self.cleaned_data.get('from_field')
return self.hm.get_entity_by_key(organization_key)
def get_to_entity(self):
organization_key = self.cleaned_data.get('organization')
return self.hm.get_entity_by_key(organization_key)
def get_poc(self, organization):
return ', '.join(u"%s <%s>" % i.email() for i in organization.get_poc())
def clean_cc1(self):
value = self.cleaned_data.get('cc1', '')
result = []
errors = []
for address in value.split('\n'):
address = address.strip();
if not address:
continue
try:
self.check_email(address)
except forms.ValidationError:
errors.append(address)
result.append(address)
if errors:
raise forms.ValidationError('Invalid email addresses: %s' % ', '.join(errors))
return ','.join(result)
def get_cc(self, from_entity, to_entity):
return self.cleaned_data.get('cc1', '')
def is_approved(self):
assert NotImplemented
def save(self, *args, **kwargs):
l = self.instance
if not l:
l = LiaisonStatement()
super(LiaisonModelForm, self).save(*args,**kwargs)
l.title = self.cleaned_data["title"]
l.purpose = LiaisonStatementPurposeName.objects.get(order=self.cleaned_data["purpose"])
l.body = self.cleaned_data["body"].strip()
l.deadline = self.cleaned_data["deadline_date"]
l.related_to = self.cleaned_data["related_to"]
l.reply_to = self.cleaned_data["replyto"]
l.response_contact = self.cleaned_data["response_contact"]
l.technical_contact = self.cleaned_data["technical_contact"]
now = datetime.datetime.now()
l.modified = now
l.submitted = datetime.datetime.combine(self.cleaned_data["submitted_date"], now.time())
if not l.approved:
l.approved = now
# set state for new statements
if self.is_new:
self.instance.change_state(state_id='pending',person=self.person)
if self.is_approved():
self.instance.change_state(state_id='posted',person=self.person)
else:
# create modified event
LiaisonStatementEvent.objects.create(
type_id='modified',
by=self.person,
statement=self.instance,
desc='Statement Modified'
)
self.save_extra_fields(l)
l.save() # we have to save here to make sure we get an id for the attachments
self.save_attachments(l)
return l
self.save_related_liaisons()
self.save_attachments()
def save_extra_fields(self, liaison):
from_entity = self.get_from_entity()
liaison.from_name = from_entity.name
liaison.from_group = from_entity.obj
e = self.cleaned_data["person"].email_set.order_by('-active', '-time')
if e:
liaison.from_contact = e[0]
return self.instance
organization = self.get_to_entity()
liaison.to_name = organization.name
liaison.to_group = organization.obj
liaison.to_contact = self.get_poc(organization)
liaison.cc = self.get_cc(from_entity, organization)
def save_attachments(self, instance):
written = instance.attachments.all().count()
def save_attachments(self):
'''Saves new attachments.
Files come in with keys like "attach_file_N" where N is index of attachments
displayed in the form. The attachment title is in the corresponding
request.POST[attach_title_N]
'''
written = self.instance.attachments.all().count()
for key in self.files.keys():
title_key = key.replace('file', 'title')
attachment_title = self.data.get(title_key)
if not key.startswith('attach_file_') or not title_key in self.data.keys():
continue
attached_file = self.files.get(key)
@ -270,205 +297,193 @@ class LiaisonForm(forms.Form):
else:
extension = ''
written += 1
name = instance.name() + ("-attachment-%s" % written)
name = self.instance.name() + ("-attachment-%s" % written)
attach, _ = Document.objects.get_or_create(
name = name,
defaults=dict(
title = self.data.get(title_key),
title = attachment_title,
type_id = "liai-att",
external_url = name + extension, # strictly speaking not necessary, but just for the time being ...
)
)
instance.attachments.add(attach)
LiaisonStatementAttachment.objects.create(statement=self.instance,document=attach)
attach_file = open(os.path.join(settings.LIAISON_ATTACH_PATH, attach.name + extension), 'w')
attach_file.write(attached_file.read())
attach_file.close()
def clean_title(self):
title = self.cleaned_data.get('title', None)
if self.instance and self.instance.pk:
exclude_filter = {'pk': self.instance.pk}
if not self.is_new:
# create modified event
LiaisonStatementEvent.objects.create(
type_id='modified',
by=self.person,
statement=self.instance,
desc='Added attachment: {}'.format(attachment_title)
)
def save_related_liaisons(self):
rel = DocRelationshipName.objects.get(slug='refold')
new_related = self.cleaned_data.get('related_to', [])
# add new ones
for stmt in new_related:
self.instance.source_of_set.get_or_create(target=stmt,relationship=rel)
# delete removed ones
for related in self.instance.source_of_set.all():
if related.target not in new_related:
related.delete()
def set_from_fields(self):
assert NotImplemented
def set_required_fields(self):
purpose = self.data.get('purpose', None)
if purpose in ['action', 'comment']:
self.fields['deadline'].required = True
else:
exclude_filter = {}
if LiaisonStatement.objects.exclude(**exclude_filter).filter(title__iexact=title).exists():
raise forms.ValidationError('A liaison statement with the same title has previously been submitted.')
return title
self.fields['deadline'].required = False
def reset_required_fields(self):
self.fields['deadline'].required = True
class IncomingLiaisonForm(LiaisonForm):
def set_to_fields(self):
assert NotImplemented
def set_from_field(self):
if is_secretariat(self.user):
sdos = Group.objects.filter(type="sdo", state="active")
else:
sdos = Group.objects.filter(type="sdo", state="active", role__person=self.person, role__name__in=("liaiman", "auth")).distinct()
self.fields['from_field'].choices = [('sdo_%s' % i.pk, i.name) for i in sdos.order_by("name")]
self.fields['from_field'].widget.submitter = unicode(self.person)
class IncomingLiaisonForm(LiaisonModelForm):
def clean(self):
if 'send' in self.data.keys() and self.get_post_only():
raise forms.ValidationError('As an IETF Liaison Manager you can not send incoming liaison statements, you only can post them')
return super(IncomingLiaisonForm, self).clean()
def set_replyto_field(self):
e = Email.objects.filter(person=self.person, role__group__state="active", role__name__in=["liaiman", "auth"])
if e:
addr = e[0].address
else:
addr = self.person.email_address()
self.fields['replyto'].initial = addr
def set_organization_field(self):
self.fields['organization'].choices = self.hm.get_all_incoming_entities()
def is_approved(self):
'''Incoming Liaison Statements do not required approval'''
return True
def get_post_only(self):
from_entity = self.get_from_entity()
if is_secretariat(self.user) or Role.objects.filter(person=self.person, group=from_entity.obj, name="auth"):
from_groups = self.cleaned_data.get('from_groups')
if has_role(self.user, "Secretariat") or is_authorized_individual(self.user,from_groups):
return False
return True
def clean(self):
if 'send' in self.data.keys() and self.get_post_only():
self._errors['from_field'] = ErrorList([u'As an IETF Liaison Manager you can not send an incoming liaison statements, you only can post them'])
return super(IncomingLiaisonForm, self).clean()
def set_from_fields(self):
'''Set from_groups and from_contact options and initial value based on user
accessing the form.'''
if has_role(self.user, "Secretariat"):
queryset = Group.objects.filter(type="sdo", state="active").order_by('name')
else:
queryset = Group.objects.filter(type="sdo", state="active", role__person=self.person, role__name__in=("liaiman", "auth")).distinct().order_by('name')
self.fields['from_contact'].initial = self.person.role_set.filter(group=queryset[0]).first().email.address
self.fields['from_contact'].widget.attrs['readonly'] = True
self.fields['from_groups'].queryset = queryset
self.fields['from_groups'].widget.submitter = unicode(self.person)
# if there's only one possibility make it the default
if len(queryset) == 1:
self.fields['from_groups'].initial = queryset
def set_to_fields(self):
'''Set to_groups and to_contacts options and initial value based on user
accessing the form. For incoming Liaisons, to_groups choices is the full set.
'''
self.fields['to_groups'].choices = get_internal_choices(None)
def liaison_manager_sdos(person):
return Group.objects.filter(type="sdo", state="active", role__person=person, role__name="liaiman").distinct()
class OutgoingLiaisonForm(LiaisonForm):
to_poc = forms.CharField(label="POC", required=True)
class OutgoingLiaisonForm(LiaisonModelForm):
from_contact = SearchableEmailField(only_users=True)
approved = forms.BooleanField(label="Obtained prior approval", required=False)
other_organization = forms.CharField(label="Other SDO", required=True)
def get_to_entity(self):
organization_key = self.cleaned_data.get('organization')
organization = self.hm.get_entity_by_key(organization_key)
if organization_key == 'othersdo' and self.cleaned_data.get('other_organization', None):
organization.name=self.cleaned_data['other_organization']
return organization
def set_from_field(self):
if is_secretariat(self.user):
self.fields['from_field'].choices = self.hm.get_all_incoming_entities()
elif is_sdo_liaison_manager(self.person):
self.fields['from_field'].choices = self.hm.get_all_incoming_entities()
all_entities = []
for i in self.hm.get_entities_for_person(self.person):
all_entities += i[1]
if all_entities:
self.fields['from_field'].widget.full_power_on = [i[0] for i in all_entities]
self.fields['from_field'].widget.reduced_to_set = ['sdo_%s' % i.pk for i in liaison_manager_sdos(self.person)]
else:
self.fields['from_field'].choices = self.hm.get_entities_for_person(self.person)
self.fields['from_field'].widget.submitter = unicode(self.person)
self.fieldsets[0] = ('From', ('from_field', 'replyto', 'approved'))
def set_replyto_field(self):
e = Email.objects.filter(person=self.person, role__group__state="active", role__name__in=["ad", "chair"])
if e:
addr = e[0].address
else:
addr = self.person.email_address()
self.fields['replyto'].initial = addr
def set_organization_field(self):
# If the user is a liaison manager and is nothing more, reduce the To field to his SDOs
if not self.hm.get_entities_for_person(self.person) and is_sdo_liaison_manager(self.person):
self.fields['organization'].choices = [('sdo_%s' % i.pk, i.name) for i in liaison_manager_sdos(self.person)]
else:
self.fields['organization'].choices = self.hm.get_all_outgoing_entities()
self.fieldsets[1] = ('To', ('organization', 'other_organization', 'to_poc'))
def set_required_fields(self):
super(OutgoingLiaisonForm, self).set_required_fields()
organization = self.data.get('organization', None)
if organization == 'othersdo':
self.fields['other_organization'].required=True
else:
self.fields['other_organization'].required=False
def reset_required_fields(self):
super(OutgoingLiaisonForm, self).reset_required_fields()
self.fields['other_organization'].required=True
def get_poc(self, organization):
return self.cleaned_data['to_poc']
def save_extra_fields(self, liaison):
super(OutgoingLiaisonForm, self).save_extra_fields(liaison)
from_entity = self.get_from_entity()
needs_approval = from_entity.needs_approval(self.person)
if not needs_approval or self.cleaned_data.get('approved', False):
liaison.approved = datetime.datetime.now()
else:
liaison.approved = None
def clean_to_poc(self):
value = self.cleaned_data.get('to_poc', None)
self.check_email(value)
return value
def clean_organization(self):
to_code = self.cleaned_data.get('organization', None)
from_code = self.cleaned_data.get('from_field', None)
if not to_code or not from_code:
return to_code
all_entities = []
person = self.fake_person or self.person
for i in self.hm.get_entities_for_person(person):
all_entities += i[1]
# If the from entity is one in which the user has full privileges the to entity could be anyone
if from_code in [i[0] for i in all_entities]:
return to_code
sdo_codes = ['sdo_%s' % i.pk for i in liaison_manager_sdos(person)]
if to_code in sdo_codes:
return to_code
entity = self.get_to_entity()
entity_name = entity and entity.name or to_code
if self.fake_person:
raise forms.ValidationError('%s is not allowed to send a liaison to: %s' % (self.fake_person, entity_name))
else:
raise forms.ValidationError('You are not allowed to send a liaison to: %s' % entity_name)
class EditLiaisonForm(LiaisonForm):
from_field = forms.CharField(widget=forms.TextInput, label=u'From')
replyto = forms.CharField(label=u'Reply to', widget=forms.TextInput)
organization = forms.CharField(widget=forms.TextInput)
to_poc = forms.CharField(widget=forms.TextInput, label="POC", required=False)
cc1 = forms.CharField(widget=forms.TextInput, label="CC", required=False)
class Meta:
fields = ('from_raw_body', 'to_body', 'to_poc', 'cc1', 'last_modified_date', 'title',
'response_contact', 'technical_contact', 'body',
'deadline_date', 'purpose', 'replyto', 'related_to')
model = LiaisonStatement
exclude = ('attachments','state','from_name','to_name','action_holder_contacts')
# add approved field, no action_holder_contacts
fieldsets = [('From', {'fields': ['from_groups','from_contact','response_contacts','approved'], 'legend': ''}),
('To', {'fields': ['to_groups','to_contacts'], 'legend': ''}),
('Other email addresses', {'fields': ['technical_contacts','cc_contacts'], 'legend': ''}),
('Purpose', {'fields':['purpose', 'deadline'], 'legend': ''}),
('Reference', {'fields': ['other_identifiers','related_to'], 'legend': ''}),
('Liaison Statement', {'fields': ['title', 'submitted_date', 'body', 'attachments'], 'legend': ''}),
('Add attachment', {'fields': ['attach_title', 'attach_file', 'attach_button'], 'legend': ''})]
def is_approved(self):
return self.cleaned_data['approved']
def set_from_fields(self):
'''Set from_groups and from_contact options and initial value based on user
accessing the form'''
self.fields['from_groups'].choices = get_internal_choices(self.user)
if has_role(self.user, "Secretariat"):
return
if self.person.role_set.filter(name='liaiman',group__state='active'):
email = self.person.role_set.filter(name='liaiman',group__state='active').first().email.address
elif self.person.role_set.filter(name__in=('ad','chair'),group__state='active'):
email = self.person.role_set.filter(name__in=('ad','chair'),group__state='active').first().email.address
else:
email = self.person.email_address()
self.fields['from_contact'].initial = email
self.fields['from_contact'].widget.attrs['readonly'] = True
def set_to_fields(self):
'''Set to_groups and to_contacts options and initial value based on user
accessing the form'''
# set options. if the user is a Liaison Manager and nothing more, reduce set to his SDOs
if has_role(self.user, "Liaison Manager") and not self.person.role_set.filter(name__in=('ad','chair'),group__state='active'):
queryset = Group.objects.filter(type="sdo", state="active", role__person=self.person, role__name="liaiman").distinct().order_by('name')
else:
# get all outgoing entities
queryset = Group.objects.filter(type="sdo", state="active").order_by('name')
self.fields['to_groups'].queryset = queryset
# set initial
if has_role(self.user, "Liaison Manager"):
self.fields['to_groups'].initial = [queryset.first()]
class EditLiaisonForm(LiaisonModelForm):
def __init__(self, *args, **kwargs):
super(EditLiaisonForm, self).__init__(*args, **kwargs)
self.edit = True
self.fields['attachments'].initial = self.instance.liaisonstatementattachment_set.exclude(removed=True)
related = [ str(x.pk) for x in self.instance.source_of_set.all() ]
self.fields['related_to'].initial = ','.join(related)
self.fields['submitted_date'].initial = self.instance.submitted
def set_from_field(self):
self.fields['from_field'].initial = self.instance.from_name
def save(self, *args, **kwargs):
super(EditLiaisonForm, self).save(*args,**kwargs)
if self.has_changed() and 'submitted_date' in self.changed_data:
event = self.instance.liaisonstatementevent_set.filter(type='submitted').first()
event.time = self.cleaned_data.get('submitted_date')
event.save()
def set_replyto_field(self):
self.fields['replyto'].initial = self.instance.reply_to
return self.instance
def set_organization_field(self):
self.fields['organization'].initial = self.instance.to_name
def set_from_fields(self):
'''Set from_groups and from_contact options and initial value based on user
accessing the form.'''
if self.instance.is_outgoing():
self.fields['from_groups'].choices = get_internal_choices(self.user)
else:
if has_role(self.user, "Secretariat"):
queryset = Group.objects.filter(type="sdo", state="active").order_by('name')
else:
queryset = Group.objects.filter(type="sdo", state="active", role__person=self.person, role__name__in=("liaiman", "auth")).distinct().order_by('name')
self.fields['from_contact'].widget.attrs['readonly'] = True
self.fields['from_groups'].queryset = queryset
def save_extra_fields(self, liaison):
liaison.from_name = self.cleaned_data.get('from_field')
liaison.to_name = self.cleaned_data.get('organization')
liaison.to_contact = self.cleaned_data['to_poc']
liaison.cc = self.cleaned_data['cc1']
def set_to_fields(self):
'''Set to_groups and to_contacts options and initial value based on user
accessing the form. For incoming Liaisons, to_groups choices is the full set.
'''
if self.instance.is_outgoing():
# if the user is a Liaison Manager and nothing more, reduce to set to his SDOs
if has_role(self.user, "Liaison Manager") and not self.person.role_set.filter(name__in=('ad','chair'),group__state='active'):
queryset = Group.objects.filter(type="sdo", state="active", role__person=self.person, role__name="liaiman").distinct().order_by('name')
else:
# get all outgoing entities
queryset = Group.objects.filter(type="sdo", state="active").order_by('name')
self.fields['to_groups'].queryset = queryset
else:
self.fields['to_groups'].choices = get_internal_choices(None)
def liaison_form_factory(request, **kwargs):
user = request.user
force_incoming = 'incoming' in request.GET.keys()
liaison = kwargs.pop('liaison', None)
if liaison:
return EditLiaisonForm(user, instance=liaison, **kwargs)
if not force_incoming and can_add_outgoing_liaison(user):
return OutgoingLiaisonForm(user, **kwargs)
elif can_add_incoming_liaison(user):
return IncomingLiaisonForm(user, **kwargs)
return None
class EditAttachmentForm(forms.Form):
title = forms.CharField(max_length=255)

View file

@ -2,72 +2,56 @@ import datetime
from django.conf import settings
from django.template.loader import render_to_string
from django.core.urlresolvers import reverse as urlreverse
from ietf.utils.mail import send_mail_text
from ietf.liaisons.utils import role_persons_with_fixed_email
from ietf.liaisons.utils import approval_roles
from ietf.group.models import Role
def send_liaison_by_email(request, liaison):
subject = u'New Liaison Statement, "%s"' % (liaison.title)
from_email = settings.LIAISON_UNIVERSAL_FROM
to_email = liaison.to_contact.split(',')
cc = liaison.cc.split(',')
if liaison.technical_contact:
cc += liaison.technical_contact.split(',')
if liaison.response_contact:
cc += liaison.response_contact.split(',')
to_email = liaison.to_contacts.split(',')
cc = liaison.cc_contacts.split(',')
if liaison.technical_contacts:
cc += liaison.technical_contacts.split(',')
if liaison.response_contacts:
cc += liaison.response_contacts.split(',')
bcc = ['statements@ietf.org']
body = render_to_string('liaisons/liaison_mail.txt', dict(
liaison=liaison,
url=settings.IDTRACKER_BASE_URL + urlreverse("liaison_detail", kwargs=dict(object_id=liaison.pk)),
referenced_url=settings.IDTRACKER_BASE_URL + urlreverse("liaison_detail", kwargs=dict(object_id=liaison.related_to.pk)) if liaison.related_to else None,
))
body = render_to_string('liaisons/liaison_mail.txt', dict(liaison=liaison))
send_mail_text(request, to_email, from_email, subject, body, cc=", ".join(cc), bcc=", ".join(bcc))
def notify_pending_by_email(request, liaison):
'''Send mail requesting approval of pending liaison statement. Send mail to
the intersection of approvers for all from_groups
'''
approval_set = set(approval_roles(liaison.from_groups.first()))
if liaison.from_groups.count() > 1:
for group in liaison.from_groups.all():
approval_set.intersection_update(approval_roles(group))
to_emails = [ r.email.address for r in approval_set ]
# Broken: this does not find the list of approvers for the sending body
# For now, we are sending to statements@ietf.org so the Secretariat can nudge
# Bug 880: https://trac.tools.ietf.org/tools/ietfdb/ticket/880
#
# from ietf.liaisons.utils import IETFHM
#
# from_entity = IETFHM.get_entity_by_key(liaison.from_raw_code)
# if not from_entity:
# return None
# to_email = []
# for person in from_entity.can_approve():
# to_email.append('%s <%s>' % person.email())
subject = u'New Liaison Statement, "%s" needs your approval' % (liaison.title)
from_email = settings.LIAISON_UNIVERSAL_FROM
body = render_to_string('liaisons/pending_liaison_mail.txt', dict(
liaison=liaison,
url=settings.IDTRACKER_BASE_URL + urlreverse("liaison_approval_detail", kwargs=dict(object_id=liaison.pk)),
referenced_url=settings.IDTRACKER_BASE_URL + urlreverse("liaison_detail", kwargs=dict(object_id=liaison.related_to.pk)) if liaison.related_to else None,
))
# send_mail_text(request, to_email, from_email, subject, body)
send_mail_text(request, ['statements@ietf.org'], from_email, subject, body)
body = render_to_string('liaisons/pending_liaison_mail.txt', dict(liaison=liaison))
send_mail_text(request, to_emails, from_email, subject, body)
def send_sdo_reminder(sdo):
roles = Role.objects.filter(name="liaiman", group=sdo)
if not roles: # no manager to contact
return None
manager_role = roles[0]
subject = 'Request for update of list of authorized individuals'
to_email = manager_role.email.address
name = manager_role.person.plain_name()
authorized_list = role_persons_with_fixed_email(sdo, "auth")
authorized_list = Role.objects.filter(group=sdo, name='auth').select_related("person").distinct()
body = render_to_string('liaisons/sdo_reminder.txt', dict(
manager_name=name,
sdo_name=sdo.name,
individuals=authorized_list,
))
send_mail_text(None, to_email, settings.LIAISON_UNIVERSAL_FROM, subject, body)
return body
@ -82,11 +66,11 @@ def possibly_send_deadline_reminder(liaison):
1: 'tomorrow',
0: 'today'
}
days_to_go = (liaison.deadline - datetime.date.today()).days
if not (days_to_go < 0 or days_to_go in PREVIOUS_DAYS.keys()):
return None # no reminder
if days_to_go < 0:
subject = '[Liaison OUT OF DATE] %s' % liaison.title
days_msg = 'is out of date for %s days' % (-days_to_go)
@ -95,20 +79,16 @@ def possibly_send_deadline_reminder(liaison):
days_msg = 'expires %s' % PREVIOUS_DAYS[days_to_go]
from_email = settings.LIAISON_UNIVERSAL_FROM
to_email = liaison.to_contact.split(',')
cc = liaison.cc.split(',')
if liaison.technical_contact:
cc += liaison.technical_contact.split(',')
if liaison.response_contact:
cc += liaison.response_contact.split(',')
to_email = liaison.to_contacts.split(',')
cc = liaison.cc_contacts.split(',')
if liaison.technical_contacts:
cc += liaison.technical_contacts.split(',')
if liaison.response_contacts:
cc += liaison.response_contacts.split(',')
bcc = 'statements@ietf.org'
body = render_to_string('liaisons/liaison_deadline_mail.txt',
dict(liaison=liaison,
days_msg=days_msg,
url=settings.IDTRACKER_BASE_URL + urlreverse("liaison_approval_detail", kwargs=dict(object_id=liaison.pk)),
referenced_url=settings.IDTRACKER_BASE_URL + urlreverse("liaison_detail", kwargs=dict(object_id=liaison.related_to.pk)) if liaison.related_to else None,
))
dict(liaison=liaison,days_msg=days_msg,))
send_mail_text(None, to_email, from_email, subject, body, cc=cc, bcc=bcc)
return body

View file

@ -11,10 +11,12 @@ class Command(BaseCommand):
def handle(self, *args, **options):
today = datetime.date.today()
cutoff = today - datetime.timedelta(14)
for l in LiaisonStatement.objects.filter(action_taken=False, deadline__gte=cutoff).exclude(deadline=None):
msgs = []
for l in LiaisonStatement.objects.filter(deadline__gte=cutoff).exclude(tags__slug='taken'):
r = possibly_send_deadline_reminder(l)
if r:
print 'Liaison %05s#: Deadline reminder sent!' % l.pk
msgs.append('Liaison %05s#: Deadline reminder sent!' % l.pk)
return '\n'.join(msgs)

View file

@ -10,10 +10,9 @@ class Command(BaseCommand):
help = (u"Send a remind to each SDO Liaison Manager to update the list of persons authorized to send liaison statements on behalf of his SDO")
option_list = BaseCommand.option_list + (
make_option('-s', '--sdo-pk', dest='sdo_pk',
help='Send the reminder to the SDO whith this primary key. If not provided reminder will be sent to all SDOs'),
help='Send the reminder to the SDO with this primary key. If not provided reminder will be sent to all SDOs'),
)
def handle(self, *args, **options):
sdo_pk = options.get('sdo_pk', None)
return_output = options.get('return_output', False)
@ -21,7 +20,6 @@ class Command(BaseCommand):
msg_list = send_reminders_to_sdos(sdo_pk=sdo_pk)
return msg_list if return_output else None
def send_reminders_to_sdos(sdo_pk=None):
sdos = Group.objects.filter(type="sdo").order_by('pk')
if sdo_pk:
@ -29,7 +27,7 @@ def send_reminders_to_sdos(sdo_pk=None):
if not sdos:
print "No SDOs found!"
msgs = []
for sdo in sdos:
body = send_sdo_reminder(sdo)
@ -38,10 +36,6 @@ def send_reminders_to_sdos(sdo_pk=None):
msg = u'%05s#: %s has no liaison manager' % (sdo.pk, sdo.name)
else:
msg = u'%05s#: %s mail sent!' % (sdo.pk, sdo.name)
print msg
msgs.append(msg)
return msgs
return msgs

View file

@ -1,264 +1,45 @@
# -*- coding: utf-8 -*-
from south.v2 import SchemaMigration
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(SchemaMigration):
class Migration(migrations.Migration):
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'])
dependencies = [
('doc', '0002_auto_20141222_1749'),
('person', '0001_initial'),
('name', '0007_populate_liaison_names'),
]
# # 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']
operations = [
migrations.CreateModel(
name='LiaisonStatement',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('title', models.CharField(max_length=255, blank=True)),
('body', models.TextField(blank=True)),
('deadline', models.DateField(null=True, blank=True)),
('from_name', models.CharField(help_text=b'Name of the sender body', max_length=255)),
('to_name', models.CharField(help_text=b'Name of the recipient body', max_length=255)),
('to_contact', models.CharField(help_text=b'Contacts at recipient body', max_length=255, blank=True)),
('reply_to', models.CharField(max_length=255, blank=True)),
('response_contact', models.CharField(max_length=255, blank=True)),
('technical_contact', models.CharField(max_length=255, blank=True)),
('cc', models.TextField(blank=True)),
('submitted', models.DateTimeField(null=True, blank=True)),
('modified', models.DateTimeField(null=True, blank=True)),
('approved', models.DateTimeField(null=True, blank=True)),
('action_taken', models.BooleanField(default=False)),
('attachments', models.ManyToManyField(to='doc.Document', blank=True)),
('from_contact', models.ForeignKey(blank=True, to='person.Email', null=True)),
('from_group', models.ForeignKey(related_name='liaisonstatement_from_set', blank=True, to='group.Group', help_text=b'Sender group, if it exists', null=True)),
('purpose', models.ForeignKey(to='name.LiaisonStatementPurposeName')),
('related_to', models.ForeignKey(blank=True, to='liaisons.LiaisonStatement', null=True)),
('to_group', models.ForeignKey(related_name='liaisonstatement_to_set', blank=True, to='group.Group', help_text=b'Recipient group, if it exists', null=True)),
],
options={
},
bases=(models.Model,),
),
]

View file

@ -1,229 +0,0 @@
# -*- 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

@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('group', '0003_auto_20150304_0743'),
('person', '0001_initial'),
('doc', '0002_auto_20141222_1749'),
('name', '0007_populate_liaison_names'),
('liaisons', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='LiaisonStatementEvent',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('time', models.DateTimeField(auto_now_add=True)),
('desc', models.TextField()),
('by', models.ForeignKey(to='person.Person')),
('statement', models.ForeignKey(to='liaisons.LiaisonStatement')),
('type', models.ForeignKey(to='name.LiaisonStatementEventTypeName')),
],
options={'ordering': ['-time', '-id']},
bases=(models.Model,),
),
migrations.CreateModel(
name='LiaisonStatementGroupContacts',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('contacts', models.CharField(max_length=255,blank=True)),
('cc_contacts', models.CharField(max_length=255,blank=True)),
('group', models.ForeignKey(to='group.Group', unique=True)),
],
options={
},
bases=(models.Model,),
),
migrations.CreateModel(
name='RelatedLiaisonStatement',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('relationship', models.ForeignKey(to='name.DocRelationshipName')),
('source', models.ForeignKey(related_name='source_of_set', to='liaisons.LiaisonStatement')),
('target', models.ForeignKey(related_name='target_of_set', to='liaisons.LiaisonStatement')),
],
options={
},
bases=(models.Model,),
),
migrations.RenameField(
model_name='liaisonstatement',
old_name='cc',
new_name='cc_contacts',
),
migrations.RenameField(
model_name='liaisonstatement',
old_name='to_contact',
new_name='to_contacts',
),
migrations.RenameField(
model_name='liaisonstatement',
old_name='technical_contact',
new_name='technical_contacts',
),
migrations.RenameField(
model_name='liaisonstatement',
old_name='response_contact',
new_name='response_contacts',
),
migrations.AddField(
model_name='liaisonstatement',
name='action_holder_contacts',
field=models.CharField(help_text=b'Who makes sure action is completed', max_length=255, blank=True),
preserve_default=True,
),
migrations.AddField(
model_name='liaisonstatement',
name='from_groups',
field=models.ManyToManyField(related_name='liaisonsatement_from_set', to='group.Group', blank=True),
preserve_default=True,
),
migrations.AddField(
model_name='liaisonstatement',
name='other_identifiers',
field=models.TextField(null=True, blank=True),
preserve_default=True,
),
migrations.AddField(
model_name='liaisonstatement',
name='state',
field=models.ForeignKey(default=b'pending', to='name.LiaisonStatementState'),
preserve_default=True,
),
migrations.AddField(
model_name='liaisonstatement',
name='tags',
field=models.ManyToManyField(to='name.LiaisonStatementTagName', null=True, blank=True),
preserve_default=True,
),
migrations.AddField(
model_name='liaisonstatement',
name='to_groups',
field=models.ManyToManyField(related_name='liaisonsatement_to_set', to='group.Group', blank=True),
preserve_default=True,
),
migrations.AlterField(
model_name='liaisonstatement',
name='response_contacts',
field=models.CharField(help_text=b'Where to send a response', max_length=255, blank=True),
preserve_default=True,
),
migrations.AlterField(
model_name='liaisonstatement',
name='technical_contacts',
field=models.CharField(help_text=b'Who to contact for clarification', max_length=255, blank=True),
preserve_default=True,
),
]

View file

@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
def migrate_tags(apps, schema_editor):
LiaisonStatement = apps.get_model("liaisons", "LiaisonStatement")
for s in LiaisonStatement.objects.filter(action_taken=True):
s.tags.add('taken')
def migrate_state(apps, schema_editor):
LiaisonStatement = apps.get_model("liaisons", "LiaisonStatement")
for s in LiaisonStatement.objects.all():
if s.approved:
s.state_id='posted'
else:
s.state_id='pending'
s.save()
def create_events(apps, schema_editor):
LiaisonStatement = apps.get_model("liaisons", "LiaisonStatement")
LiaisonStatementEvent = apps.get_model("liaisons", "LiaisonStatementEvent")
Person = apps.get_model("person","Person")
system = Person.objects.get(name="(system)")
for s in LiaisonStatement.objects.all():
if s.submitted:
event = LiaisonStatementEvent.objects.create(
type_id='submitted',
by=system,
statement=s,
desc='Statement Submitted')
event.time=s.submitted
event.save()
if s.approved:
# create posted event
event = LiaisonStatementEvent.objects.create(
type_id='posted',
by=system,
statement=s,
desc='Statement Posted')
event.time=s.approved
event.save()
# create approved event for outgoing only
if s.approved != s.submitted:
event = LiaisonStatementEvent.objects.create(
type_id='approved',
by=system,
statement=s,
desc='Statement Approved')
event.time=s.approved
event.save()
if s.modified and ( s.modified != s.submitted and s.modified != s.approved ):
event = LiaisonStatementEvent.objects.create(
type_id='modified',
by=system,
statement=s,
desc='Statement Modified')
event.time=s.modified
event.save()
def migrate_relations(apps, schema_editor):
LiaisonStatement = apps.get_model("liaisons", "LiaisonStatement")
RelatedLiaisonStatement = apps.get_model("liaisons", "RelatedLiaisonStatement")
for liaison in LiaisonStatement.objects.filter(related_to__isnull=False):
RelatedLiaisonStatement.objects.create(
source=liaison,
target=liaison.related_to,
relationship_id='refold')
# XXX: Now done in migrate_groups
def merge_reply_to(apps, schema_editor):
"""Merge contents of reply_to field into response_contact and create comment Event"""
LiaisonStatement = apps.get_model("liaisons", "LiaisonStatement")
LiaisonStatementEvent = apps.get_model("liaisons", "LiaisonStatementEvent")
Person = apps.get_model("person","Person")
system = Person.objects.get(name="(system)")
for liaison in LiaisonStatement.objects.exclude(reply_to=''):
if liaison.reply_to in liaison.response_contacts:
continue
LiaisonStatementEvent.objects.create(
type_id='comment',
statement=liaison,
desc='Merged reply_to field into response_contacts\nOriginal reply_to: %s\nOriginal response_contacts: %s' % (liaison.reply_to, liaison.response_contacts),
by=system
)
liaison.response_contacts += ',%s' % liaison.reply_to
liaison.save()
class Migration(migrations.Migration):
dependencies = [
('liaisons', '0002_schema_changes'),
]
operations = [
migrations.RunPython(migrate_tags),
migrations.RunPython(migrate_state),
migrations.RunPython(create_events),
migrations.RunPython(migrate_relations),
#migrations.RunPython(merge_reply_to),
]

View file

@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
# This migration handles converting a standard Many-to-Many field to one
# with a through table
def copy_attachments(apps, schema_editor):
LiaisonStatement = apps.get_model("liaisons", "LiaisonStatement")
LiaisonStatementAttachment = apps.get_model("liaisons", "LiaisonStatementAttachment")
for liaison in LiaisonStatement.objects.all():
for doc in liaison.attachments.all():
LiaisonStatementAttachment.objects.create(
statement=liaison,
document=doc,
removed=False)
class Migration(migrations.Migration):
dependencies = [
('liaisons', '0003_migrate_general'),
]
operations = [
migrations.CreateModel(
name='LiaisonStatementAttachment',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('removed', models.BooleanField(default=False)),
('document', models.ForeignKey(to='doc.Document')),
('statement', models.ForeignKey(to='liaisons.LiaisonStatement')),
],
options={
},
bases=(models.Model,),
),
migrations.RunPython(copy_attachments),
migrations.RemoveField(
model_name='liaisonstatement',
name='attachments',
),
migrations.AddField(
model_name='liaisonstatement',
name='attachments',
field=models.ManyToManyField(to='doc.Document', through='liaisons.LiaisonStatementAttachment', blank=True),
preserve_default=True,
),
]

View file

@ -0,0 +1,792 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
def create_new_groups(apps, schema_editor):
Group = apps.get_model("group","Group")
for group in NEW_GROUPS:
if group[2]:
#print "Get parent: {}".format(group[2])
parent = Group.objects.get(acronym=group[2])
else:
parent = None
Group.objects.create(
acronym=group[0],
name=group[1],
parent=parent,
type_id='sdo',
state_id=group[3])
def change_acronyms(apps, schema_editor):
'''Modify some existing groups'''
Group = apps.get_model("group","Group")
for old,new in CHANGE_ACRONYM:
group = Group.objects.get(acronym=old)
group.acronym = new
group.save()
def set_parents(apps, schema_editor):
'''Modify some existing groups'''
Group = apps.get_model("group","Group")
for child_acronym,parent_acronym in SET_PARENT:
#print "Setting parent {}:{}".format(child_acronym,parent_acronym)
child = Group.objects.get(acronym=child_acronym)
parent = Group.objects.get(acronym=parent_acronym)
child.parent = parent
child.save()
def reassign_groups(apps,schema_editor):
'''For Statements that have a multi to_group assignment, remove the group
assignment and populate the to_name field for conversion to multiple groups
in later function'''
LiaisonStatement = apps.get_model("liaisons", "LiaisonStatement")
for acronym,name in MULTI_TO_GROUPS:
for stmt in LiaisonStatement.objects.filter(to_group__acronym=acronym):
stmt.to_name=name
stmt.to_group=None
stmt.save()
def cleanup_groups(apps, schema_editor):
Group = apps.get_model("group","Group")
for group,x in MULTI_TO_GROUPS:
Group.objects.get(acronym=group).delete()
def copy_to_group(apps, schema_editor):
'''For this migration we are favoring the value in to_name over to_group. Based
on observation there are statements with multiple groups in the to_name but
restricted to one to_group.'''
LiaisonStatement = apps.get_model("liaisons", "LiaisonStatement")
Group = apps.get_model("group","Group")
for s in LiaisonStatement.objects.all():
if s.to_name and s.to_name in TO_NAME_MAPPING:
if TO_NAME_MAPPING[s.to_name]:
got_exception = False
for acronym in TO_NAME_MAPPING[s.to_name]:
try:
s.to_groups.add(Group.objects.get(acronym=acronym))
except Group.DoesNotExist:
print "Group Does Not Exist: {},{},{}".format(s.pk,s.to_name,acronym)
got_exception = True
if not got_exception:
s.to_name = ''
s.save()
else:
print "{}:{} empty to_group mapping".format(s.pk,s.to_name)
elif s.to_group:
s.to_groups.add(s.to_group)
s.to_name = ''
s.save()
def copy_from_group(apps, schema_editor):
LiaisonStatement = apps.get_model("liaisons", "LiaisonStatement")
Group = apps.get_model("group","Group")
for s in LiaisonStatement.objects.all():
if s.from_name and s.from_name in FROM_NAME_MAPPING:
if FROM_NAME_MAPPING[s.from_name]:
got_exception = False
for acronym in FROM_NAME_MAPPING[s.from_name]:
try:
s.from_groups.add(Group.objects.get(acronym=acronym))
except Group.DoesNotExist:
print "Group Does Not Exist: {}".format(acronym)
got_exception = True
if not got_exception:
s.from_name = ''
s.save()
else:
print "{}:{} empty from_group mapping".format(s.pk,s.from_name)
elif s.from_group:
s.from_groups.add(s.from_group)
s.from_name = ''
s.save()
else:
print "from_name not mapped and no from_group {}".format(s.pk)
# set from_contact
#if s.from_contact:
# for fg in s.fromgroup_set.all():
# fg.contact = s.from_contact
# fg.save()
def set_default_poc(apps, schema_editor):
"""Set default group POC if there is only one unique value"""
LiaisonStatementGroupContacts = apps.get_model("liaisons", "LiaisonStatementGroupContacts")
Group = apps.get_model("group", "Group")
for group in Group.objects.filter(liaisonstatement_to_set__isnull=False).distinct():
contacts = set()
for stmt in group.liaisonstatement_to_set.all():
if stmt.to_contacts:
contacts.add(stmt.to_contacts)
if len(contacts) == 1:
LiaisonStatementGroupContacts.objects.create(group=group,contacts=contacts.pop())
# do explicit mappings
for acronym,contacts in DEFAULT_POC.items():
group = Group.objects.get(acronym=acronym)
try:
lsgc = LiaisonStatementGroupContacts.objects.get(group=group)
lsgc.contacts = contacts
lsgc.save()
except LiaisonStatementGroupContacts.DoesNotExist:
LiaisonStatementGroupContacts.objects.create(group=group,contacts=contacts)
def set_cc_contacts(apps, schema_editor):
"""Set initial LiaisonStatementGroupContacts.cc_contacts"""
LiaisonStatementGroupContacts = apps.get_model("liaisons", "LiaisonStatementGroupContacts")
Group = apps.get_model("group", "Group")
cc_contacts = 'itu-t-liaison@iab.org'
for group in Group.objects.filter(acronym__startswith='itu'):
lsgc = group.liaisonstatementgroupcontacts_set.first()
if lsgc:
lsgc.cc_contacts = cc_contacts
lsgc.save()
else:
LiaisonStatementGroupContacts.objects.create(group=group,cc_contacts=cc_contacts)
def explicit_mappings(apps, schema_editor):
"""In some cases the to_name cannot be mapped one-to-one with a group. The
following liaison statements are modified individually
"""
#LiaisonStatementFromGroup = apps.get_model("liaisons", "LiaisonStatmentFromGroup")
LiaisonStatement = apps.get_model("liaisons", "LiaisonStatement")
Group = apps.get_model("group", "Group")
def _setgroup(to=None,frm=None,pks=None):
for pk in pks:
s = LiaisonStatement.objects.get(pk=pk)
if to:
s.to_groups.add(*Group.objects.filter(acronym__in=to))
s.to_name = ''
if frm:
#for acronym in frm:
# LiaisonStatementFromGroup.objects.create(statement=s,group=Group.objects.get(acronym=acronym))
s.from_groups.add(*Group.objects.filter(acronym__in=frm))
s.from_name = ''
s.save()
_setgroup(to=['ietf'],pks=[116,782,796,797,823,835,836,837,840])
_setgroup(to=['sipping'],pks=[809])
_setgroup(to=['ieprep'],pks=[810])
_setgroup(to=['atm-forum'],frm=['megaco'],pks=[816])
_setgroup(to=['ccamp'],pks=[827,829])
_setgroup(to=['sub','tsv'],pks=[828])
_setgroup(to=['sigtran'],pks=[830])
_setgroup(to=['irtf'],pks=[831,832,833,834])
_setgroup(to=['rmt'],pks=[838,839])
_setgroup(to=['ietf','iana'],pks=[841])
_setgroup(to=['isoc','iana'],pks=[842])
_setgroup(to=['ietf','avt'],pks=[811,812])
_setgroup(to=['avt'],pks=[822])
# 821 / 824
class Migration(migrations.Migration):
dependencies = [
('liaisons', '0004_migrate_attachments'),
]
operations = [
migrations.RunPython(change_acronyms),
migrations.RunPython(create_new_groups),
migrations.RunPython(set_parents),
migrations.RunPython(reassign_groups),
migrations.RunPython(copy_to_group),
migrations.RunPython(copy_from_group),
migrations.RunPython(set_default_poc),
migrations.RunPython(set_cc_contacts),
migrations.RunPython(cleanup_groups),
migrations.RunPython(explicit_mappings),
]
# ----------------------------------------------------------
# x_name to group mappings
# -----------------------------------------------------------
NEW_GROUPS = [
('3gpp-tsgsa','SGPP TSG SA','3gpp','active'),
('3gpp-tsgsa-sa2','3GPP TSG SA WG2','3gpp-tsgsa','active'),
('3gpp-tsgsa-sa3','3GPP TSG SA WG3','3gpp-tsgsa','active'),
('3gpp-tsgct','SGPP TSG CT','3gpp','active'),
('3gpp-tsgct-ct1','3GPP TSG CT WG1','3gpp-tsgct','active'),
('3gpp-tsgct-ct4','3GPP TSG CT WG4','3gpp-tsgct','active'),
('3gpp-tsgran','SGPP TSG RAN','3gpp','active'),
('3gpp-tsgran-ran2','3GPP TSG RAN WG2','3gpp-tsgran','active'),
('3gpp-tsgt-wg2','3GPP-TSGT-WG2','3gpp','active'),
('acif','Australian Communications Industry Forum',None,'active'),
('arib','Association of Radio Industries and Business',None,'active'),
('ashrae','American Society of Heating, Refrigerating, and Air-Conditioning Engineers',None,'active'),
('atis','ATIS',None,'active'),
('atm-forum','ATM Forum',None,'active'),
('ccsa','China Communications Standards Association',None,'active'),
('dlna','Digital Living Network Alliance',None,'active'),
('dsl-forum','DSL Forum',None,'active'),
('dsl-forum-twg','DSL Forum Architecture & Transport Working Group','dsl-forum','active'),
('dvb-tm-ipi','DVB TM-IPI',None,'active'),
('epcglobal','EPCGlobal',None,'active'),
('etsi','ETSI',None,'active'),
('etsi-at-digital','ETSI AT Digital','etsi','active'),
('etsi-bran','ETSI BRAN','etsi','active'),
('etsi-dect','ETSI DECT','etsi','active'),
('etsi-emtel','ETSI EMTEL','etsi','active'),
('etsi-tc-hf','ETSI TC HF','etsi','active'),
('etsi-tispan','ETSI TISPAN','etsi','active'),
('etsi-tispan-wg4','ETSI TISPAN WG4','etsi-tispan','active'),
('etsi-tispan-wg5','ETSI TISPAN WG5','etsi-tispan','active'),
('femto-forum','Femto Forum',None,'active'),
('gsma','GSMA',None,'active'),
('gsma-wlan','GSMA WLAN','gsma','active'),
('incits-t11-5','INCITS T11.5',None,'active'),
('isma','Internet Streaming Media Alliance',None,'active'),
('itu','ITU',None,'active'),
('itu-r-wp5a','ITU-R-WP5A','itu-r','active'),
('itu-r-wp5d','ITU-R-WP5D','itu-r','active'),
('itu-r-wp8a','ITU-R-WP8A','itu-r','active'),
('itu-r-wp8f','ITU-R-WP8F','itu-r','active'),
('itu-t-ipv6-group','ITU-T-IPV6-GROUP','itu-t','active'),
('itu-t-fg-cloud','ITU-T-FG-CLOUD','itu-t','conclude'),
('itu-t-fg-iptv','ITU-T-FG-IPTV','itu-t','conclude'),
('itu-t-fg-ngnm','ITU-T-FG-NGNM','itu-t','conclude'),
('itu-t-jca-idm','ITU-T-JCA-IDM','itu-t','active'),
('itu-t-ngnmfg','ITU-T-NGNMFG','itu-t','active'),
('itu-t-sg-4','ITU-T-SG-4','itu-t','conclude'),
('itu-t-sg-6','ITU-T-SG-6','itu-t','conclude'),
('itu-t-sg-7','ITU-T-SG-7','itu-t','conclude'),
('itu-t-sg-8','ITU-T-SG-8','itu-t','conclude'),
('itu-t-sg-9','ITU-T-SG-9','itu-t','active'),
('itu-t-sg-2-q1','ITU-T-SG-2-Q1','itu-t-sg-2','active'),
('itu-t-sg-11-q5','ITU-T-SG-11-Q5','itu-t-sg-11','active'),
('itu-t-sg-11-wp2','ITU-T-SG-11-WP2','itu-t-sg-11','active'),
('itu-t-sg-12-q12','ITU-T-SG-12-Q12','itu-t-sg-12','active'),
('itu-t-sg-12-q17','ITU-T-SG-12-Q17','itu-t-sg-12','active'),
('itu-t-sg-13-q3','ITU-T-SG-13-Q3','itu-t-sg-13','active'),
('itu-t-sg-13-q5','ITU-T-SG-13-Q5','itu-t-sg-13','active'),
('itu-t-sg-13-q7','ITU-T-SG-13-Q7','itu-t-sg-13','active'),
('itu-t-sg-13-q9','ITU-T-SG-13-Q9','itu-t-sg-13','active'),
('itu-t-sg-13-q11','ITU-T-SG-13-Q11','itu-t-sg-13','active'),
('itu-t-sg-13-wp3','ITU-T-SG-13-WP3','itu-t-sg-13','conclude'),
('itu-t-sg-13-wp4','ITU-T-SG-13-WP4','itu-t-sg-13','conclude'),
('itu-t-sg-13-wp5','ITU-T-SG-13-WP5','itu-t-sg-13','conclude'),
('itu-t-sg-14','ITU-T-SG-14','itu-t','active'),
('itu-t-sg-15-q1','ITU-T-SG-15-Q1','itu-t-sg-15','active'),
('itu-t-sg-15-q3','ITU-T-SG-15-Q3','itu-t-sg-15','active'),
('itu-t-sg-15-q4','ITU-T-SG-15-Q4','itu-t-sg-15','active'),
('itu-t-sg-15-q6','ITU-T-SG-15-Q6','itu-t-sg-15','active'),
('itu-t-sg-15-q9','ITU-T-SG-15-Q9','itu-t-sg-15','active'),
('itu-t-sg-15-q10','ITU-T-SG-15-Q10','itu-t-sg-15','active'),
('itu-t-sg-15-q11','ITU-T-SG-15-Q11','itu-t-sg-15','active'),
('itu-t-sg-15-q12','ITU-T-SG-15-Q12','itu-t-sg-15','active'),
('itu-t-sg-15-q14','ITU-T-SG-15-Q14','itu-t-sg-15','active'),
('itu-t-sg-15-q15','ITU-T-SG-15-Q15','itu-t-sg-15','active'),
('itu-t-sg-15-wp1','ITU-T-SG-15-WP1','itu-t-sg-15','active'),
('itu-t-sg-15-wp3','ITU-T-SG-15-WP3','itu-t-sg-15','active'),
('itu-t-sg-16-q8','ITU-T-SG-16-Q8','itu-t-sg-16','active'),
('itu-t-sg-16-q9','ITU-T-SG-16-Q9','itu-t-sg-16','active'),
('itu-t-sg-16-q10','ITU-T-SG-16-Q10','itu-t-sg-16','active'),
#('itu-t-sg-17-tsb','ITU-T-SG-17-TSB','itu-t-sg-17','active'),
('itu-t-sg-17-q2','ITU-T-SG-17-Q2','itu-t-sg-17','active'),
#('itu-t-sg-17-q4','ITU-T-SG-17-Q4','itu-t-sg-17','active'),
('itu-t-sg-20','ITU-T-SG-20','itu-t','active'),
('ieee','IEEE',None,'active'),
('ieee-802','IEEE 802','ieee','active'),
('ieee-802-ec','IEEE 802 Executive Committee','ieee','active'),
('ieee-802-21','IEEE 802.21','ieee-802','active'),
('ieee-sa-ngson','IEEE SA NGSON','ieee-sa','active'),
('iso-iec-jtc1','ISO/IEC JTC1',None,'active'),
('iso-iec-jtc1-sc29-wg1','ISO/IEC JTC1 SC29 WG1','iso-iec-jtc1-sc29','active'),
('iso-iec-jtc1-sc31','ISO/IEC JTC1 SC31','iso-iec-jtc1','active'),
('iso-iec-jtc1-sc31-wg4','ISO/IEC JTC1 SC31 WG4','iso-iec-jtc1-sc31','active'),
('iso-iec-jtc1-sgsn','ISO/IEC JTC1 SGSN','iso-iec-jtc1','active'),
('iso-iec-jtc1-wg7','ISO/IEC JTC1 WG7','iso-iec-jtc1','active'),
('mead','IETF MEAD Team','rtg','active'),
('mfa-forum','MFA Forum',None,'active'),
('mpeg','MPEG',None,'active'),
('mpls-forum','MPLS Forum',None,'active'),
('mfa','MPLS and Frame Relay Alliance',None,'active'),
('nanc-lnpa-wg','NANC LNPA WG',None,'active'),
('nmnro','National, Multi-National or Regional Organizations',None,'active'),
('oma','OMA',None,'active'),
('oma-bcast','OMA BCAST','oma','active'),
('oma-com-cab','OMA COM CAB','oma','active'),
('oma-com-cpm','OMA COM CPM','oma','active'),
('oma-mwg','OMA MWG','oma','active'),
('oma-mwg-mem','OMA MWG-MEM','oma-mwg','active'),
('oma-pag-wg','OMA PAG WG','oma','active'),
('oma-tp','OMA TP','oma','active'),
('opif','Open IPTV Forum',None,'active'),
('t1m1','T1M1',None,'active'),
('t1s1','T1S1',None,'active'),
('t1x1','T1X1',None,'active'),
('tia','TIA',None,'active'),
('tmoc','TMOC',None,'active'),
('w3c-geolocation-wg','W3C Geolocation WG','w3c','active'),
('w3c-mmi','W3C MMI','w3c','active'),
('wifi-alliance','Wifi Alliance',None,'active'),
('wig','WIG',None,'active'),
]
CHANGE_ACRONYM = [
('ieee-8021','ieee-802-1'),
('ieee-8023','ieee-802-3'),
('ieee-80211','ieee-802-11'),
('ieee-80216','ieee-802-16'),
('ieee-80223','ieee-802-23'),
('isoiec-jtc1-sc2','iso-iec-jtc1-sc2'),
('isoiec-jtc1-sc6','iso-iec-jtc1-sc6'),
('isoiec-jtc1-sc29','iso-iec-jtc1-sc29'),
('isoiec-jtc-1sc-29wg-11','iso-iec-jtc1-sc29-wg11'),
('itu-t-fgd','itu-t-fg-dist'),
('itu-t-sg17-q4','itu-t-sg-17-q4'),
('itu-t-sg17-tsb','itu-t-sg-17-tsb'),
('ITU-T-SG5','itu-t-sg-5'),
('3GPP-TSG-SA-WG4','3gpp-tsgsa-sa4'),
('IEEE-802-OmniRAN','ieee-802-ec-omniran'),
]
SET_PARENT = [
('itu-t','itu'),
('itu-r','itu'),
('itu-t-jca-cloud','itu-t'),
('itu-t-jca-cop','itu-t'),
('itu-t-jca-sdn','itu-t'),
('itu-t-mpls','itu-t'),
('itu-t-sg-2','itu-t'),
('itu-t-sg-3','itu-t'),
('itu-t-sg-11','itu-t'),
('itu-t-sg-12','itu-t'),
('itu-t-sg-13','itu-t'),
('itu-t-sg-15','itu-t'),
('itu-t-sg-16','itu-t'),
('itu-t-sg-17','itu-t'),
('itu-t-tsag','itu-t'),
('ieee-sa','ieee'),
('ieee-802-1','ieee-802'),
('ieee-802-3','ieee-802'),
('ieee-802-11','ieee-802'),
('ieee-802-16','ieee-802'),
('ieee-802-23','ieee-802'),
('ieee-802-ec-omniran','ieee-802-ec'),
('iso-iec-jtc1-sc2','iso-iec-jtc1'),
('iso-iec-jtc1-sc6','iso-iec-jtc1'),
('iso-iec-jtc1-sc7','iso-iec-jtc1'),
('iso-iec-jtc1-sc27','iso-iec-jtc1'),
('iso-iec-jtc1-sc29','iso-iec-jtc1'),
('iso-iec-jtc1-sc29-wg11','iso-iec-jtc1-sc29'),
]
MULTI_TO_GROUPS = [
('itu-t-sg15-q9-q10-q12-and-q14','ITU-T SG 15 Q9, Q10, Q12 and Q14'),
('itu-t-sg12-q-12-17','ITU-T SG 12, Q12, Q17'),
]
TO_NAME_MAPPING = {
u'(bwijnen@lucent.com) Bert Wijnen': [u'sming'],
u'(lyong@ciena.com)Lyndon Ong': [u'itu-t-sg-15'],
#u'(sob@harvard.edu) Scott Bradner': None, # this is a bunch (explicit)
u'(sob@harvard.edu)Scott Bradner': ['irtf'], # this is 833
u'3GPP SA WG4': [u'3gpp-tsgsa-sa4'],
u'3GPP SA2': [u'3gpp-tsgsa-sa2'],
u'3GPP TSG CT WG4': [u'3gpp-tsgct-ct4'],
u'3GPP TSG RAN WG2': [u'3gpp-tsgran-ran2'],
u'3GPP TSG SA WG4': [u'3gpp-tsgsa-sa4'],
u'3GPP, 3GPP2, ARIB, ATIS, CCSA, ETSI, ETSI-DECT, ETSI-BRAN, IEEE, IETF,': [u'ietf'],
u'3GPP/IETF and 3GPP/ITU-T Co-ordinator': ['3gpp-tsgct-ct1'],
u'ACIF, ARIB, ATIS, CCSA, ETSI, IEEE, IETF, ISACC, TIA, TTA, TTC': ['ietf'],
u'ASON-related Work': ['ccamp'],
u'ATIS': ['atis'],
u'American Society of Heating, Refrigerating, and Air-Conditioning Engineers': ['ashrae'],
u'BBF': ['broadband-forum'],
u'BMWG': [u'bmwg'],
u'Bert Wijnen and the IETF O & M Area': [u'ops'],
u'Bert Wijnen, Bernard Aboba and the IETF': [u'ietf'],
u'CCAMP WG co-chairs and IEEE-IETF': ['ccamp'],
u'CCAMP WG co-chairs and IEEE-IETF liaisons': ['ccamp'],
u'Completes action above Scott Bradner, Area co-Director (sob@harvard.edu)': ['tsv'],
u'DLNA': ['dlna'],
#u'DONE': None, # this one is explicitly mapped
u'DSL Forum': [u'dsl-forum'],
u'DSL Forum Architecture & Transport Working Group': ['dsl-forum-twg'],
u'DVB IPI': ['dvb-tm-ipi'],
u'DVB TM-IPI, ETSI TISPAN, ATIS IIF, IETF RMT, IETF FECFRAME': ['fecframe','rmt'],
u'EAP Method Update Working Group': ['emu'],
u'ETSI AT working group Digital': ['etsi-at-digital'],
u'ETSI TC HF': ['etsi-tc-hf'],
u'ETSI TISPAN': ['etsi-tispan'],
u'G.7712 Editor, ITU-T SG15Q14 Rapporteur, ITU-T SG15': ['itu-t-sg-15'],
u'Generic EAP Encapsulation': ['int'],
u'Harald Alvestrand': ['avt'], # placeholder for explicit (2)
u'IAB and IETF Routing Area Directors': [u'iab', 'rtg'],
u'IAB, IESG': [u'iab', 'iesg'],
u'IANA': [u'iana'],
u'ICANN, IETF/IAB, NRO and ACSIS': ['ietf','iab'],
u'IEEE 802': [u'ieee-802'],
u'IEEE NGSON Study Group': ['ieee-sa-ngson'],
u'IEEE802.1': [u'ieee-802-1'],
u'IESG members, IAB members': [u'iesg', u'iab'],
u'IESG, IAB, IETF MPLS WG': ['iesg','iab','mpls'],
u'IESG, IETF-RAI': [u'iesg', u'rai'],
u'IESG/IAB Chair': [u'iesg', u'iab'],
u'IETF PWE3 and TICTOC': [u'pwe3', u'tictoc'],
u'IETF (CCAMP, PCE and MPLS WGs)': [u'ccamp', u'pce', u'mpls'],
u'IETF (Management)': ['iesg'],
u'IETF (SAVI and V6OPS WGS, OPS Area and INT Area)': [u'savi', u'v6ops', u'ops', u'int'],
u'IETF (Sub-IP & Transport Areas)': [u'sub', u'tsv'],
u'IETF (and others)': [u'ietf'],
u'IETF (ccamp, pce and mpls WGs)': [u'ccamp', u'pce', u'mpls'],
u'IETF 6MAN WG, IETF Internet Area': [u'6man', u'int'],
u'IETF AVT WG, ITU-T SG11': [u'avt', u'itu-t-sg-11'],
#u'IETF CCAMP WG and Routing Area Directors': [u'ccamp', u'rtg'],
#u'IETF CCAMP WG and Sub IP Directors': [u'ccamp','sub'],
#u'IETF CCAMP WG and Sub-IP Area Directors': [u'ccamp','sub'],
u'IETF CCAMP WG, CC: IETF OSPF WG': [u'ospf','ccamp'],
u'IETF CCAMP WG, Routing Area Directors': [u'ccamp', u'rtg'],
u'IETF CCAMP and MPLS WGs': [u'ccamp', u'mpls'],
u'IETF CCAMP and MPLS WGs and the Routing Area Directors of the IETF': [u'ccamp', u'mpls', u'rtg'],
u'IETF CCAMP and PCE WGs': [u'ccamp', u'pce'],
u'IETF CCAMP, IETF Routing Area Directors': [u'ccamp', u'rtg'],
u'IETF CCAMP, PCE and MPLS WGs': [u'ccamp', u'pce', u'mpls'],
u'IETF Charter group on Authority to Citizen Alert (ATOCA)': [u'atoca'],
u'IETF DNSOP WG, SAAG, IAB': [u'dnsop', u'saag', u'iab'],
u'IETF IAB, IETF IESG': [u'iab', u'ietf', u'iesg'],
u'IETF IESG, IAB, PWE3 WG, MPLS WG, routing and internet Area Directors': [u'iesg', u'iab', u'pwe3', u'mpls', u'rtg', u'int'],
u'IETF IESG, IETF MPLS WG': [u'mpls','iesg'],
#u'IETF ISIS WG and Routing Area Directors': [u'isis','rtg'],
u'IETF IPPM, IETF AVT': [u'ippm', u'avt'],
u'IETF Internet Area; IETF MIF WG; IETF v6ops WG; IETF 6man WG; IETF softwire WG; IETF Operations and Management Area': [u'int', u'mif', u'v6ops', u'6man', u'softwire', u'ops'],
u'IETF Liaison to the ITU on MPLS and PWE3 WG Co-Chair': [u'itu-t-mpls', u'pwe3'],
u'IETF MEAD Team': [u'mead'],
u'IETF MEAD team': [u'mead'],
u'IETF MEXT WG': ['mext'],
u'IETF MIPSHOP-WG': [u'mipshop'],
u'IETF MMUSIC WG,IETF SIPPING WG': [u'sipping','mmusic'],
u'IETF MPLS & PWE3': [u'mpls', u'pwe3'],
u'IETF MPLS WG, CC: IETF CCAMP and PWE3 WGs': [u'mpls', u'ccamp', u'pwe3'],
u'IETF MPLS WG, CC: MFA Forum': ['mpls','mfa-forum'],
u'IETF MPLS WG, IAB, IESG': [u'mpls', u'iab', u'iesg'],
u'IETF MPLS WG, IETF IAB and IESG': [u'mpls', u'iab', u'iesg'],
u'IETF MPLS WG, IETF PWE3 WG, Broadband Forum': [u'mpls', u'pwe3', u'broadband-forum'],
u'IETF MPLS WG Co Chairs (Info: CCAMP WG Co Chairs, MEAD team)': [u'mpls','ccamp','mead'],
u'IETF MPLS WG Co-Chairs, CC: IETF MEAD team': [u'mpls','mead'],
u'IETF MPLS WG and OPSA WG': [u'mpls','opsawg'],
u'IETF MPLS WG and PEW3 WG': [u'mpls','pwe3'],
u'IETF MPLS WG, PWE3 WG': [u'pwe3','mpls'],
u'IETF MPLS and GMPLS': ['mpls'],
u'IETF MPLS and PWE3 WG, MFA Forum, ITU-T Q7/13': ['mpls','pwe3','mfa-forum','itu-t-sg-13-q7'],
u'IETF MPLS liaison representative': [u'mpls'],
u'IETF MPLS, CCAMP and PWE3 WGs': [u'mpls', u'ccamp', u'pwe3'],
u'IETF MPLS, CCAMP, PWE3 and L2VPN': [u'mpls', u'ccamp', u'pwe3', u'l2vpn'],
u'IETF MPLS, PWE WGs (Info: IETF MEAD team)': ['mpls','pwe3','mead'],
u'IETF Mead Team': [u'mead'],
u'IETF NSIS WG Chairs, IETF TSV Area Directors, IESG members, IAB members': [u'nsis', u'tsv', u'iesg', u'iab'],
u'IETF PWE3 and L2VPN': [u'pwe3', u'l2vpn'],
u'IETF PWE3 and L2VPN Working Groups': [u'pwe3', u'l2vpn'],
u'IETF PWE3 and MPLS WG': [u'mpls',u'pwe3'],
u'IETF PWE3 and MPLS WGs': [u'pwe3', u'mpls'],
u'IETF PWE3 and MPLS Working Groups': [u'pwe3', u'mpls'],
u'IETF PWE3, IETF L2VPN WG': ['pwe3',u'l2vpn'],
u'IETF PWE3, MPLS working groups': [u'pwe3', u'mpls'],
u'IETF RAI and IESG': [u'rai', 'iesg'],
u'IETF Real-time Applications and Infrastructure Area Director': [u'rai'],
u'IETF Routing Area, the MPLS and CCAMP working groups': [u'rtg', u'mpls', u'ccamp'],
u'IETF Routing Area (CCAMP WG) and Internet Area (L2VPN WG and L3VPN WG)': ['ccamp','l2vpn','l3vpn'],
u'IETF Routing Area Directors and IAB (CC: CCAMP WG)': ['ccamp',u'rtg','iab'],
u'IETF Routing Area Directors and IS-IS WG': ['isis'],
u'IETF Routing and Transport areas': [u'rtg', u'tsv'],
u'IETF Security Area Directors, CC: IETF CCAMP WG': [u'sec','ccamp'],
u'IETF SIP related Working Groups and IESG': ['iesg','rai'],
u'IETF Transport and Internat Areas': [u'tsv', u'int'],
u'IETF Transport Area Directors, PCN Working Group Chairs': [u'pcn'],
u'IETF WG MPLS': [u'mpls'],
u'IETF Working Groups IEPREP, TSV, NSIS': [u'ieprep', u'tsv', u'nsis'],
u'IETF and Harald Alvestrand': ['ietf'],
u'IETF and IAB': [u'ietf', u'iab'],
u'IETF avt and mmusic WG': [u'mmusic','avt'],
u'IETF ccamp and pce WG': ['ccamp',u'pce'],
u'IETF mobileip WG and mpls WG': [u'mobileip','mpls'],
u'IETF mpls WG, CC: IETF pwe3 WG': ['mpls',u'pwe3'],
u'IETF mpls and ccamp WG': ['mpls',u'ccamp'],
u'IETF pwe3 WG, CC: mpls WG': [u'mpls','pwe3'],
u'IETF pwe3, mpls WGs': [u'pwe3', u'mpls'],
u'IETF pwe3 and mpls WG': [u'mpls','pwe3'],
u'IETF re RoHC': [u'rohc'],
u'IETF \u2013 Internet Area Directors, Internet Area Working Groups': [u'int'],
u'IETF: Transport Area Directors, PCN Working Group Chairs': [u'pcn'],
u'IETF, IAB': [u'ietf', u'iab'],
u'IETF/IAB': [u'ietf', u'iab'],
u'IETF/IAB, NRO, ICANN and ACSIS': ['ietf','iab'],
u'IETF/IAB/IESG': [u'ietf', u'iab', u'iesg'],
u'IETF/PWE3 and L2VPN WGs': [u'pwe3', u'l2vpn'],
u'ISIS': [u'isis'],
u'ISMA': ['isma'],
u'ISO/IEC JTC': [u'iso-iec-jtc1'],
u'ISO/IEC JTC 1/SC 29/WG 1': [u'iso-iec-jtc1-sc29-wg1'],
u'ISOC': [u'isoc'],
u'ISOC/IAB Liaison': [u'isoc', 'iab'],
u'ITU': [u'itu'],
u'ITU IPv6 Group': [u'itu-t-ipv6-group'],
u'ITU Q12/15 and Q14/15': [u'itu-t-sg-15-q12',u'itu-t-sg-15-q14'],
u'ITU SG 16 Q8, 9, 10/16': [u'itu-t-sg-16-q8',u'itu-t-sg-16-q9',u'itu-t-sg-16-q10'],
u'ITU SG13': [u'itu-t-sg-13'],
u'ITU SG15': [u'itu-t-sg-15'],
u'ITU-R': [u'itu-r'],
u'ITU-R WP8F & IETF': [u'itu-r-wp8f',u'ietf'],
u'ITU-SG15': [u'itu-t-sg-15'],
u'ITU-SG2': [u'itu-t-sg-2'],
u'ITU-T JCA-IdM': [u'itu-t-jca-idm'],
u'ITU-T Q1/SG15': [u'itu-t-sg-15-q1'],
u'ITU-T Q10/15': [u'itu-t-sg-15-q10'],
u'ITU-T Q12/15 and Q14/15': [u'itu-t-sg-15-q12',u'itu-t-sg-15-q14'],
u'ITU-T Q14/15': [u'itu-t-sg-15-q14'],
u'ITU-T Q14/15 - Mr. Kam Lam, Rapporteur': [u'itu-t-sg-15-q14'],
u'ITU-T Q14/15, ITU-T Q11/15': [u'itu-t-sg-15-q11',u'itu-t-sg-15-q14'],
u'ITU-T Q3/15': [u'itu-t-sg-15-q3'],
u'ITU-T Q5/13 (recently renamed ITU-T Q7/13)': [u'itu-t-sg-13-q7'],
u'ITU-T Q7/SG13': [u'itu-t-sg-13-q7'],
u'ITU-T Question 14/15': [u'itu-t-sg-15-q14'],
u'ITU-T Question 3/15': [u'itu-t-sg-15-q3'],
u'ITU-T SG 11 and ITU-T TSAG': [u'itu-t-sg-11',u'itu-t-tsag'],
u'ITU-T SG 11, ITU-T Q.5/11, ITU-T WP 2/11': [u'itu-t-sg-11',u'itu-t-sg-11-q5',u'itu-t-sg-11-wp2'],
u'ITU-T SG 12, Q12, Q17': [u'itu-t-sg12-q-12-17'],
u'ITU-T SG 13 (ITU-T SG 11 and SG 12 for information)': [u'itu-t-sg-13',u'itu-t-sg-12',u'itu-t-sg-12'],
u'ITU-T SG 13 (ITU-T SG 11 for information)': [u'itu-t-sg-13',u'itu-t-sg-11'],
u'ITU-T SG 13, SG 15': [u'itu-t-sg-13', u'itu-t-sg-15'],
u'ITU-T SG 15 <tsbsg15@itu.int, greg.jones@itu.int>': [u'itu-t-sg-15'],
u'ITU-T SG 15 Q9, Q10, Q12 and Q14': [u'itu-t-sg-15-q9',u'itu-t-sg-15-q10',u'itu-t-sg-15-q12',u'itu-t-sg-15-q14'],
u'ITU-T SG 15, Q.14/15': [u'itu-t-sg-15-q14'],
u'ITU-T SG 15, Q9, Q11, Q12, Q14': [u'itu-t-sg-15-q9',u'itu-t-sg-15-q10',u'itu-t-sg-15-q12',u'itu-t-sg-15-q14'],
u'ITU-T SG 17 Q.2/17': [u'itu-t-sg-17-q2'],
u'ITU-T SG 4': [u'itu-t-sg-4'],
u'ITU-T SG 4, 9, 11, 13, 16 and IETF': [u'itu-t-sg-4',u'itu-t-sg-9',u'itu-t-sg-11',u'itu-t-sg-13',u'itu-t-sg-16',u'ietf'],
u'ITU-T SG-15': [u'itu-t-sg-15'],
u'ITU-T SG-2': [u'itu-t-sg-2'],
u'ITU-T SG11': [u'itu-t-sg-11'],
u'ITU-T SG12, SG13, ATIS, TIA, IEC, IETF ccamp WG, IEEE 802.1, 802.3, OIF, Metro Ethernet Forum, ATM Forum': ['ccamp'],
u'ITU-T SG13': [u'itu-t-sg-13'],
u'ITU-T SG13 and SG15': [u'itu-t-sg-13', u'itu-t-sg-15'],
u'ITU-T SG15': [u'itu-t-sg-15'],
u'ITU-T SG15 (Optical Control Plane)': [u'itu-t-sg-15'],
u'ITU-T SG15 Q10': [u'itu-t-sg-15-q10'],
u'ITU-T SG15 Q10, Q12': [u'itu-t-sg-15-q10',u'itu-t-sg-15-q12'],
u'ITU-T SG15 Q12': [u'itu-t-sg-15-q12'],
u'ITU-T SG15 Q14': [u'itu-t-sg-15-q14'],
u'ITU-T SG15 Q6': [u'itu-t-sg-15-q6'],
u'ITU-T SG15 Q9, Q10, Q12 and Q14': [u'itu-t-sg-15-q9',u'itu-t-sg-15-q10',u'itu-t-sg-15-q12',u'itu-t-sg-15-q14'],
u'ITU-T SG15 Q9, Q11, Q12 and Q14': [u'itu-t-sg-15-q9',u'itu-t-sg-15-q11',u'itu-t-sg-15-q12',u'itu-t-sg-15-q14'],
u'ITU-T SG15 Question 12': [u'itu-t-sg-15-q12'],
u'ITU-T SG15 Question 3': [u'itu-t-sg-15-q3'],
u'ITU-T SG15 Question 6': [u'itu-t-sg-15-q6'],
u'ITU-T SG15 Question 6, Question 12, and Question 14': [u'itu-t-sg-15-q6',u'itu-t-sg-15-q12',u'itu-t-sg-15-q14'],
u'ITU-T SG15 Question 9': [u'itu-t-sg-15-q9'],
u'ITU-T SG15 Questions 12 and 14': [u'itu-t-sg-15-q12',u'itu-t-sg-15-q14'],
u'ITU-T SG15 and Q14/15': [u'itu-t-sg-15-q14'],
u'ITU-T SG15, Q 9/15, Q 10/15, Q 12/15 and Q 14/15': [u'itu-t-sg-15-q9',u'itu-t-sg-15-q10',u'itu-t-sg-15-q12',u'itu-t-sg-15-q14'],
u'ITU-T SG15, Q 9/15, Q10/15, Q12/15 and Q14/15': [u'itu-t-sg-15-q9',u'itu-t-sg-15-q10',u'itu-t-sg-15-q12',u'itu-t-sg-15-q14'],
u'ITU-T SG15, Q9, Q11, Q12 and Q14': [u'itu-t-sg-15-q9',u'itu-t-sg-15-q11',u'itu-t-sg-15-q12',u'itu-t-sg-15-q14'],
u'ITU-T SG16': [u'itu-t-sg-16'],
u'ITU-T SG17': [u'itu-t-sg-17'],
u'ITU-T SG17 TSB': [u'itu-t-sg-17-tsb'],
u'ITU-T SG2': [u'itu-t-sg-2'],
u'ITU-T SG2 <tsbsg2@itu.int>': [u'itu-t-sg-2'],
u'ITU-T SG2 Q 1/2': [u'itu-t-sg-2-q1'],
u'ITU-T SG4': [u'itu-t-sg-4'],
u'ITU-T SG4, ITU-T SG15, ITU-T NGNM Focus group, 3GPP SA5, 3GPP2, ATIS/TMOC, TMF, IETF Management, ETSI BRAN': ['iesg'],
u'ITU-T SGs, ITU-R WGs, ITU-D SG2 and the IETF': ['ietf'],
u'ITU-T SGs: 2 (info), 4, 9, 11, 12, 13, 17, 19; ITU-R SGs: 1, 4, 5, 6; ITU-D SG 2; Focus Group on \u2018From/In/To Cars II\u2019 (ITU-T SG 12); ISO TC 22 SC3 and TC 204 ; IEEE 802, 802.11 (WiFi), 802.15.1 (Bluetooth); AUTOSAR WPII-1.1, OSGi VEG, IrDA and JSR298 Tele': ['ietf'],
u'ITU-T SQ15 Question 14': [u'itu-t-sg-15-q14'],
u'ITU-T Study Group 11': [u'itu-t-sg-11'],
u'ITU-T Study Group 11 <tsg11gen@itu.int>': [u'itu-t-sg-11'],
u'ITU-T Study Group 13': [u'itu-t-sg-13'],
u'ITU-T Study Group 15': [u'itu-t-sg-15'],
u'ITU-T Study Group 15 <greg.jones@itu.int>': [u'itu-t-sg-15'],
u'ITU-T Study Group 15 Q4 <rlstuart@ieee.org>': [u'itu-t-sg-15-q4'],
u'ITU-T Study Group 15 Question 14': [u'itu-t-sg-15-q14'],
u'ITU-T Study Group 15 Question 3': [u'itu-t-sg-15-q3'],
u'ITU-T Study Group 15 Question 6': [u'itu-t-sg-15-q6'],
u'ITU-T TSAG External Relations Group': [u'itu-t-tsag'],
u'ITU-T Working Party 3/13 and ITU-T Question 11/13': [u'itu-t-sg-13-wp3',u'itu-t-sg-13-q11'],
u'ITU-T and ITU-T Study Group 13': [u'itu-t', u'itu-t-sg-13'],
u'ITU-T, ITU SG13': [u'itu-t', u'itu-t-sg-13'],
u'ITU-T-SG13': [u'itu-t-sg-13'],
u'ITU-T/FG Cloud': ['itu-t-fg-cloud'],
u'ITU-T/SG11': [u'itu-t-sg-11'],
u'ITU-T/Study Group 11': [u'itu-t-sg-11'],
u'Kam Lam, Rapporteur for Question 14 of ITU-T SG15': [u'itu-t-sg-15-q14'],
u'Kam Lam, Rapporteur for Question 14 of ITU-T Study Group 15': [u'itu-t-sg-15-q14'],
u'Lyndon Ong (lyong@ciena.com)': [u'sigtran'],
u'MFA Forum': ['mfa-forum'],
u'MPLS and Frame Relay Alliance': ['mfa'],
u'Mr. Kam Lam, Rapporteur for Question 14 of ITU-T Study Group 15': [u'itu-t-sg-15-q14'],
u'National, Multi-National or Regional Organizations': ['nmnro'],
u'OMA': [u'oma'],
u'OMA MEM': [u'oma-mwg-mem'],
u'OMA MWG': [u'oma-mwg'],
u'OMA MWG MEM Sub Working Group': [u'oma-mwg-mem'],
u'OMA TP': [u'oma-tp'],
u'OPS ADs (Randy Bush and Bert Wijnen)': [u'ops'],
u'OPS Area Director Bert Wijnen': [u'ops'],
u'Open IPTV Forum': ['opif'],
u'Open Mobile Alliance Broadcasting Working Group': [u'oma-bcast'],
u'Open Mobile Alliance, PAG Working Group': [u'oma-pag-wg'],
u'PDNR ITU-R M.[IP CHAR]': ['ietf'], # pending robert
u'PWE WG': ['pwe3'],
u'Phase 1 report to SG 4': ['ops'],
u'Q7/13': [u'itu-t-sg-13-q7'],
u'Rao Cherukuri, Chair MPLS and Frame Relay Alliance Technical Committee': ['mfa'],
u'Rao Cherukuri, Chairman, MPLS and Frame Relay Alliance Technical Committee': ['mfa'],
u'SA2, T2, OMA TP, S3': ['3gpp-tsgsa-sa2','3gpp-tsgt-wg2',u'oma-tp','3gpp-tsgsa-sa3'],
u'SAVI WG, V6OPS WG, OPS AREA, INT AREA': [u'savi', u'v6ops', u'ops', u'int'],
u'SC 29/WG11': [u'iso-iec-jtc1-sc29-wg11'],
u'SC29/WG11': [u'iso-iec-jtc1-sc29-wg11'],
u'SG 15,Questions 3,9, 11,12, 14 and WP 3/15': [u'itu-t-sg-15-q3',u'itu-t-sg-15-q9',u'itu-t-sg-15-q11',u'itu-t-sg-15-q12',u'itu-t-sg-15-q14',u'itu-t-sg-15-wp3'],
u'SG-13, Q.3/13, Q.9/13 and TSAG': [u'itu-t-sg-13-q3',u'itu-t-sg-13-q9',u'itu-t-tsag'],
u'SG13, SG13 WP4': [u'itu-t-sg-13',u'itu-t-sg-13-wp4'],
u'SG15 Q9': [u'itu-t-sg-15-q9'],
u'SG15, Q9, Q10, Q12 and Q14': [u'itu-t-sg-15-q9',u'itu-t-sg-15-q10',u'itu-t-sg-15-q12',u'itu-t-sg-15-q14'],
u'SG15, Q9, Q10, Q12, Q14': [u'itu-t-sg-15-q9',u'itu-t-sg-15-q10',u'itu-t-sg-15-q12',u'itu-t-sg-15-q14'],
u'SG15, Q9, Q10, Q12, and Q14': [u'itu-t-sg-15-q9',u'itu-t-sg-15-q10',u'itu-t-sg-15-q12',u'itu-t-sg-15-q14'],
u'SG15, Q9, Q11, Q12 and Q14': [u'itu-t-sg-15-q9',u'itu-t-sg-15-q11',u'itu-t-sg-15-q12',u'itu-t-sg-15-q14'],
u'SG17, SG13, SG11, JCA-NID, ETSI TISPAN WG4, 3GPP TSG CT4, IESG': ['iesg'],
u'SG4': [u'itu-t-sg-4'],
u'SIP aand SIPPING WGs': [u'sip', u'sipping'],
u'SIP, SIPPING, SIMPLE WGs': [u'sip', u'sipping', u'simple'],
u'SUB-IP and Transport Areas': [u'sub', u'tsv'],
u'Scott Bradner': ['ccamp','isis','sigtran'],
u'Scott Bradner (sob@harvard.edu)': ['iesg'], # placeholder for explicit mappings
u'Scott Bradner (sob@harvard.edu) Done': ['mpls'],
u'SubIP ADs (sob@harvard.edu,bwijnen@lucent.com)': [u'sub'],
u'TEWG, MPLS, CCAMP WGs': [u'tewg', u'mpls', u'ccamp'],
u'TRILL WG co-chairs and IEEE-IETF liaisons': ['trill'],
u'TRILL WG co-chairs, ADs, and IEEE-IETF liaisons': ['trill'],
u'TSG-X Corr to IETF re MIP6 Bootstrapping': ['int'],
u'The IAB': [u'iab'],
u'The IESG': [u'iesg'],
u'The IESG and the IAB': [u'iesg', u'iab'],
u'The IETF': [u'ietf'],
u'Tom Taylor (taylor@nortelnetworks.com), Megaco WG Chair': [u'megaco'],
u'Transport ADs (Allison Mankin and Scott Bradner)': [u'tsv'],
u'Transport Area Directors': [u'tsv'],
u'Unicode Consortium': ['unicode'],
u'Unicode Technical Committee': ['unicode'],
u'Various IETF WGs': ['mobileip','pppext','avt'],
u'W3C Geolocation WG': ['w3c-geolocation-wg'],
u'W3C Geolocation Working Group': ['w3c-geolocation-wg'],
u'W3C Multimedia Interaction Work Group': ['w3c-mmi'],
u'WiFi Alliance and Wireless Broadband Alliance': ['wifi-alliance','wba'],
u'chair@ietf.org': [u'ietf'],
u'gonzalo.camarillo@ericsson.com': ['ietf'],
u'tsbdir@itu.int': ['itu-t']
}
FROM_NAME_MAPPING = {
u'3GPP TSG RAN WG2': ['3gpp-tsgran-ran2'],
u'<unknown body 0>': ['itu-t-sg-13'],
u'ATIS': ['atis'],
u'ATM Forum': [u'atm-forum'],
u'ATM Forum AIC WG': [u'afic'],
u'BBF': [u'broadband-forum'],
u'DSL Forum': [u'dsl-forum'],
u'EPCGlobal': [u'epcglobal'],
u'ETSI': ['etsi'],
u'ETSI EMTEL': ['etsi-emtel'],
u'ETSI TC HF': ['etsi-tc-hf'],
u'ETSI TISPAN': ['etsi-tispan'],
u'ETSI TISPAN WG5': ['etsi-tispan-wg5'],
u'Femto Forum': ['femto-forum'],
u'GSMA WLAN': ['gsma-wlan'],
u'IEEE 802': [u'ieee-802'],
u'IEEE 802.11': [u'ieee-802-11'],
u'IEEE 802.21': [u'ieee-802-21'],
u'IETF ADSL MIB': [u'adslmib'],
u'IETF MEAD Team': [u'mead'],
u'IETF Mead Team': [u'mead'],
u'IETF liaison on MPLS': [u'mpls'],
u'INCITS T11.5': ['incits-t11-5'],
u'ISO/IEC JTC 1 SC 29/WG 11': [u'iso-iec-jtc1-sc29-wg11'],
u'ISO/IEC JTC 1 SGSN': ['iso-iec-jtc1-sgsn'],
u'ISO/IEC JTC 1/SC31/WG 4/SG 1': ['iso-iec-jtc1-sc31-wg4'],
u'ISO/IEC JTC 1/WG 7': [u'iso-iec-jtc1-wg7'],
u'ISO/IEC JTC SC 29/WG1': [u'iso-iec-jtc1-sc29-wg1'],
u'ISO/IEC JTC SC 29/WG11': [u'iso-iec-jtc1-sc29-wg11'],
u'ISO/IEC JTC1/SC29/WG11': [u'iso-iec-jtc1-sc29-wg11'],
u'ISO/IEC JTC1/SC6': [u'iso-iec-jtc1-sc6'],
u'ITU': [u'itu'],
u'ITU IPv6 Group': [u'itu-t-ipv6-group'],
u'ITU-Q.14/15': [u'itu-t-sg-15-q14'],
u'ITU-R WP 5A': [u'itu-r-wp5a'],
u'ITU-R WP 5D': [u'itu-r-wp5d'],
u'ITU-R WP8A': [u'itu-r-wp8a'],
u'ITU-R WP8F': [u'itu-r-wp8f'],
u'ITU-SC29': ['iso-iec-jtc1-sc29-wg1'],
u'ITU-SG 15': [u'itu-t-sg-15'],
u'ITU-SG 7': [u'itu-t-sg-7'],
u'ITU-SG 8': [u'itu-t-sg-8'],
u'ITU-T FG Cloud': [u'itu-t-fg-cloud'],
u'ITU-T FG IPTV': [u'itu-t-fg-iptv'],
u'ITU-T Q.5/13': [u'itu-t-sg-13-q5'],
u'ITU-T SG 15 Q14/15': [u'itu-t-sg-15-q14'],
u'ITU-T SG 15 WP 1': [u'itu-t-sg-15-wp1'],
u'ITU-T SG 15, Q.11': [u'itu-t-sg-15-q11'],
u'ITU-T SG 15, Q.14/15': [u'itu-t-sg-15-q14'],
u'ITU-T SG 4': [u'itu-t-sg-4'],
u'ITU-T SG 6': [u'itu-t-sg-6'],
u'ITU-T SG 7': [u'itu-t-sg-7'],
u'ITU-T SG 9': [u'itu-t-sg-9'],
u'ITUT-T SG 16': [u'itu-t-sg-16'],
u'JCA-IdM': [u'itu-t-jca-idm'],
u'MFA Forum': ['mfa-forum'],
u'MPEG': ['mpeg'],
u'MPLS Forum': ['mpls-forum'],
u'MPLS and FR Alliance': ['mfa'],
u'MPLS and Frame Relay Alliance': ['mfa'],
u'NANP LNPA WG': ['nanc-lnpa-wg'],
u'NGN Management Focus Group': ['itu-t-ngnmfg'],
u'OMA': [u'oma'],
u'OMA COM-CAB SWG': [u'oma-com-cab'],
u'OMA COM-CPM Group': [u'oma-com-cpm'],
u'Open IPTV Forum': ['opif'],
u'SC 29/WG 1': [u'iso-iec-jtc1-sc29-wg1'],
u'SC 29/WG 11': [u'iso-iec-jtc1-sc29-wg11'],
u'SC29 4559': [u'iso-iec-jtc1-sc29-wg11'],
u'SC29 4561': [u'iso-iec-jtc1-sc29-wg11'],
u'SIP, SIPPING, SIMPLE WGs': [u'sip', u'sipping', u'simple'],
u'T1M1': ['t1m1'],
u'T1S1': ['t1s1'],
u'T1X1 cc: ITU-T Q. 14/15 (for info)': ['t1x1','itu-t-sg-15-q14'],
u'TIA': ['tia'],
u'TMOC': ['tmoc'],
u'The IAB': [u'iab'],
u'The IESG': [u'iesg'],
u'The IESG and the IAB': [u'iesg', u'iab'],
u'The IETF': [u'ietf'],
u'W3C Geolocation WG': ['w3c-geolocation-wg'],
u'WIG': ['wig']
}
DEFAULT_POC = {
'3gpp':'georg.mayer.huawei@gmx.com,3GPPLiaison@etsi.org',
'3gpp-tsgct':'georg.mayer.huawei@gmx.com,3GPPLiaison@etsi.org',
'3gpp-tsgct-ct1':'georg.mayer.huawei@gmx.com,3GPPLiaison@etsi.org',
'3gpp-tsgct-ct4':'georg.mayer.huawei@gmx.com,3GPPLiaison@etsi.org',
'3gpp-tsgran':'georg.mayer.huawei@gmx.com,3GPPLiaison@etsi.org',
'3gpp-tsgran-ran2':'georg.mayer.huawei@gmx.com,3GPPLiaison@etsi.org',
'3gpp-tsgsa':'georg.mayer.huawei@gmx.com,3GPPLiaison@etsi.org',
'3gpp-tsgsa-sa2':'georg.mayer.huawei@gmx.com,3GPPLiaison@etsi.org',
'3gpp-tsgsa-sa3':'georg.mayer.huawei@gmx.com,3GPPLiaison@etsi.org',
'3gpp-tsgsa-sa4':'georg.mayer.huawei@gmx.com,3GPPLiaison@etsi.org',
'3gpp-tsgt-wg2':'georg.mayer.huawei@gmx.com,3GPPLiaison@etsi.org',
'ieee-802':'Paul Nikolich <p.nikolich@ieee.org>,Pat Thaler <pthaler@broadcom.com>',
'ieee-802-1':'Paul Nikolich <p.nikolich@ieee.org>,Glen Parsons <glenn.parsons@ericsson.com>,John Messenger <jmessenger@advaoptical.com>',
'ieee-802-11':'Dorothy Stanley <dstanley@agere.com>, Adrian Stephens <adrian.p.stephens@intel.com>',
'cablelabs':'Greg White <g.white@CableLabs.com>',
'iso-iec-jtc1-sc29':'Watanabe Shinji <watanabe@itscj.ipsj.or.jp>',
'iso-iec-jtc1-sc29-wg1':'Watanabe Shinji <watanabe@itscj.ipsj.or.jp>',
'iso-iec-jtc1-sc29-wg11':'Watanabe Shinji <watanabe@itscj.ipsj.or.jp>',
'unicode':'Richard McGowan <rick@unicode.org>',
'isotc46':'sabine.donnardcusse@afnor.org',
'w3c':u'Wendy Seltzer <wseltzer@w3.org>,Philippe Le Hégaret <plh@w3.org>',
# change to m3aawg
'maawg':'Mike Adkins <madkins@fb.com>,technical-chair@mailman.m3aawg.org',
'ecma-tc39':'John Neuman <johnneumann.openstrat@gmail.com>,Istvan Sebestyen <istvan@ecma-interational.org>',
}

View file

@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('liaisons', '0005_migrate_groups'),
]
operations = [
migrations.RemoveField(
model_name='liaisonstatement',
name='action_taken',
),
migrations.RemoveField(
model_name='liaisonstatement',
name='approved',
),
migrations.RemoveField(
model_name='liaisonstatement',
name='from_group',
),
migrations.RemoveField(
model_name='liaisonstatement',
name='modified',
),
migrations.RemoveField(
model_name='liaisonstatement',
name='related_to',
),
migrations.RemoveField(
model_name='liaisonstatement',
name='reply_to',
),
migrations.RemoveField(
model_name='liaisonstatement',
name='submitted',
),
migrations.RemoveField(
model_name='liaisonstatement',
name='to_group',
),
#migrations.RemoveField(
# model_name='liaisonstatement',
# name='from_contact',
#),
]

View file

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('liaisons', '0006_remove_fields'),
]
operations = [
migrations.AlterField(
model_name='liaisonstatement',
name='from_groups',
field=models.ManyToManyField(related_name='liaisonstatement_from_set', to='group.Group', blank=True),
preserve_default=True,
),
migrations.AlterField(
model_name='liaisonstatement',
name='to_groups',
field=models.ManyToManyField(related_name='liaisonstatement_to_set', to='group.Group', blank=True),
preserve_default=True,
),
]

View file

@ -1,52 +1,207 @@
# Copyright The IETF Trust 2007, All Rights Reserved
from django.conf import settings
from django.core.urlresolvers import reverse as urlreverse
from django.db import models
from django.utils.text import slugify
from ietf.name.models import LiaisonStatementPurposeName
from ietf.person.models import Email, Person
from ietf.name.models import (LiaisonStatementPurposeName, LiaisonStatementState,
LiaisonStatementEventTypeName, LiaisonStatementTagName,
DocRelationshipName)
from ietf.doc.models import Document
from ietf.person.models import Email
from ietf.group.models import Group
# maps (previous state id, new state id) to event type id
STATE_EVENT_MAPPING = {
(u'pending','approved'):'approved',
(u'pending','dead'):'killed',
(u'pending','posted'):'posted',
(u'approved','posted'):'posted',
(u'dead','pending'):'resurrected',
(u'pending','pending'):'submitted'
}
class LiaisonStatement(models.Model):
title = models.CharField(blank=True, max_length=255)
purpose = models.ForeignKey(LiaisonStatementPurposeName)
body = models.TextField(blank=True)
deadline = models.DateField(null=True, blank=True)
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_groups = models.ManyToManyField(Group, blank=True, related_name='liaisonstatement_from_set')
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_groups = models.ManyToManyField(Group, blank=True, related_name='liaisonstatement_to_set')
to_contacts = models.CharField(blank=True, max_length=255, help_text="Contacts at recipient body")
reply_to = models.CharField(blank=True, max_length=255)
response_contacts = models.CharField(blank=True, max_length=255, help_text="Where to send a response") # RFC4053
technical_contacts = models.CharField(blank=True, max_length=255, help_text="Who to contact for clarification") # RFC4053
action_holder_contacts = models.CharField(blank=True, max_length=255, help_text="Who makes sure action is completed") # incoming only?
cc_contacts = models.TextField(blank=True)
response_contact = models.CharField(blank=True, max_length=255)
technical_contact = models.CharField(blank=True, max_length=255)
cc = models.TextField(blank=True)
purpose = models.ForeignKey(LiaisonStatementPurposeName)
deadline = models.DateField(null=True, blank=True)
other_identifiers = models.TextField(blank=True, null=True) # Identifiers from other bodies
body = models.TextField(blank=True)
submitted = models.DateTimeField(null=True, blank=True)
modified = models.DateTimeField(null=True, blank=True)
approved = models.DateTimeField(null=True, blank=True)
tags = models.ManyToManyField(LiaisonStatementTagName, blank=True, null=True)
attachments = models.ManyToManyField(Document, through='LiaisonStatementAttachment', blank=True)
state = models.ForeignKey(LiaisonStatementState, default='pending')
action_taken = models.BooleanField(default=False)
# remove these fields post upgrade
from_name = models.CharField(max_length=255, help_text="Name of the sender body")
to_name = models.CharField(max_length=255, help_text="Name of the recipient body")
attachments = models.ManyToManyField(Document, blank=True)
def __unicode__(self):
return self.title or u"<no title>"
def change_state(self,state_id=None,person=None):
'''Helper function to change state of liaison statement and create appropriate
event'''
previous_state_id = self.state_id
self.set_state(state_id)
event_type_id = STATE_EVENT_MAPPING[(previous_state_id,state_id)]
LiaisonStatementEvent.objects.create(
type_id=event_type_id,
by=person,
statement=self,
desc='Statement {}'.format(event_type_id.capitalize())
)
def get_absolute_url(self):
return settings.IDTRACKER_BASE_URL + urlreverse('ietf.liaisons.views.liaison_detail',kwargs={'object_id':self.id})
def is_outgoing(self):
return self.to_groups.first().type_id == 'sdo'
def latest_event(self, *args, **filter_args):
"""Get latest event of optional Python type and with filter
arguments, e.g. d.latest_event(type="xyz") returns an LiaisonStatementEvent
while d.latest_event(WriteupDocEvent, type="xyz") returns a
WriteupDocEvent event."""
model = args[0] if args else LiaisonStatementEvent
e = model.objects.filter(statement=self).filter(**filter_args).order_by('-time', '-id')[:1]
return e[0] if e else None
def name(self):
if self.from_group:
frm = self.from_group.acronym or self.from_group.name
if self.from_groups.count():
frm = ', '.join([i.acronym or i.name for i in self.from_groups.all()])
else:
frm = self.from_name
if self.to_group:
to = self.to_group.acronym or self.to_group.name
if self.to_groups.count():
to = ', '.join([i.acronym or i.name for i in self.to_groups.all()])
else:
to = self.to_name
return slugify("liaison" + " " + self.submitted.strftime("%Y-%m-%d") + " " + frm[:50] + " " + to[:50] + " " + self.title[:115])
@property
def posted(self):
event = self.latest_event(type='posted')
if event:
return event.time
return None
@property
def submitted(self):
event = self.latest_event(type='submitted')
if event:
return event.time
return None
@property
def sort_date(self):
"""Returns the date to use for sorting, for posted statements this is post date,
for pending statements this is submitted date"""
if self.state_id == 'posted':
return self.posted
elif self.state_id == 'pending':
return self.submitted
@property
def modified(self):
event = self.liaisonstatementevent_set.all().order_by('-time').first()
if event:
return event.time
return None
@property
def approved(self):
return self.state_id in ('approved','posted')
@property
def action_taken(self):
return self.tags.filter(slug='taken').exists()
def active_attachments(self):
'''Returns attachments with removed ones filtered out'''
return self.attachments.exclude(liaisonstatementattachment__removed=True)
@property
def awaiting_action(self):
if getattr(self, '_awaiting_action', None) != None:
return bool(self._awaiting_action)
return self.tags.filter(slug='awaiting').exists()
@property
def from_groups_display(self):
'''Returns comma separated list of from_group names'''
groups = self.from_groups.order_by('name').values_list('name',flat=True)
return ', '.join(groups)
@property
def to_groups_display(self):
'''Returns comma separated list of to_group names'''
groups = self.to_groups.order_by('name').values_list('name',flat=True)
return ', '.join(groups)
def from_groups_short_display(self):
'''Returns comma separated list of from_group acronyms. For use in admin
interface'''
groups = self.to_groups.order_by('acronym').values_list('acronym',flat=True)
return ', '.join(groups)
from_groups_short_display.short_description = 'From Groups'
def set_state(self,slug):
try:
state = LiaisonStatementState.objects.get(slug=slug)
except LiaisonStatementState.DoesNotExist:
return
self.state = state
self.save()
class LiaisonStatementAttachment(models.Model):
statement = models.ForeignKey(LiaisonStatement)
document = models.ForeignKey(Document)
removed = models.BooleanField(default=False)
def __unicode__(self):
return self.title
return self.document.name
class RelatedLiaisonStatement(models.Model):
source = models.ForeignKey(LiaisonStatement, related_name='source_of_set')
target = models.ForeignKey(LiaisonStatement, related_name='target_of_set')
relationship = models.ForeignKey(DocRelationshipName)
def __unicode__(self):
return u"%s %s %s" % (self.source.title, self.relationship.name.lower(), self.target.title)
class LiaisonStatementGroupContacts(models.Model):
group = models.ForeignKey(Group, unique=True)
contacts = models.CharField(max_length=255,blank=True)
cc_contacts = models.CharField(max_length=255,blank=True)
def __unicode__(self):
return u"%s" % self.group.name
class LiaisonStatementEvent(models.Model):
time = models.DateTimeField(auto_now_add=True)
type = models.ForeignKey(LiaisonStatementEventTypeName)
by = models.ForeignKey(Person)
statement = models.ForeignKey(LiaisonStatement)
desc = models.TextField()
def __unicode__(self):
return u"%s %s by %s at %s" % (self.statement.title, self.type.slug, self.by.plain_name(), self.time)
class Meta:
ordering = ['-time', '-id']

View file

@ -1,23 +1,24 @@
# Autogenerated by the mkresources management command 2014-11-13 23:53
# Autogenerated by the makeresources management command 2015-08-27 10:22 PDT
from tastypie.resources import ModelResource
from tastypie.fields import ToOneField, ToManyField
from tastypie.constants import ALL, ALL_WITH_RELATIONS
from tastypie.fields import ToOneField, ToManyField # pyflakes:ignore
from tastypie.constants import ALL, ALL_WITH_RELATIONS # pyflakes:ignore
from ietf import api
from ietf.liaisons.models import * # pyflakes:ignore
from ietf.liaisons.models import * # pyflakes:ignore
from ietf.group.resources import GroupResource
from ietf.doc.resources import DocumentResource
from ietf.name.resources import LiaisonStatementPurposeNameResource
from ietf.person.resources import EmailResource
from ietf.group.resources import GroupResource
from ietf.name.resources import LiaisonStatementPurposeNameResource, LiaisonStatementTagNameResource, LiaisonStatementStateResource
from ietf.doc.resources import DocumentResource
class LiaisonStatementResource(ModelResource):
purpose = ToOneField(LiaisonStatementPurposeNameResource, 'purpose')
related_to = ToOneField('ietf.liaisons.resources.LiaisonStatementResource', 'related_to', null=True)
from_group = ToOneField(GroupResource, 'from_group', null=True)
from_contact = ToOneField(EmailResource, 'from_contact', null=True)
to_group = ToOneField(GroupResource, 'to_group', null=True)
purpose = ToOneField(LiaisonStatementPurposeNameResource, 'purpose')
state = ToOneField(LiaisonStatementStateResource, 'state')
from_groups = ToManyField(GroupResource, 'from_groups', null=True)
to_groups = ToManyField(GroupResource, 'to_groups', null=True)
tags = ToManyField(LiaisonStatementTagNameResource, 'tags', null=True)
attachments = ToManyField(DocumentResource, 'attachments', null=True)
class Meta:
queryset = LiaisonStatement.objects.all()
@ -26,25 +27,86 @@ class LiaisonStatementResource(ModelResource):
filtering = {
"id": ALL,
"title": ALL,
"body": ALL,
"to_contacts": ALL,
"response_contacts": ALL,
"technical_contacts": ALL,
"action_holder_contacts": ALL,
"cc_contacts": ALL,
"deadline": ALL,
"other_identifiers": ALL,
"body": ALL,
"from_name": ALL,
"to_name": ALL,
"to_contact": ALL,
"reply_to": ALL,
"response_contact": ALL,
"technical_contact": ALL,
"cc": ALL,
"submitted": ALL,
"modified": ALL,
"approved": ALL,
"action_taken": ALL,
"purpose": ALL_WITH_RELATIONS,
"related_to": ALL_WITH_RELATIONS,
"from_group": ALL_WITH_RELATIONS,
"from_contact": ALL_WITH_RELATIONS,
"to_group": ALL_WITH_RELATIONS,
"purpose": ALL_WITH_RELATIONS,
"state": ALL_WITH_RELATIONS,
"from_groups": ALL_WITH_RELATIONS,
"to_groups": ALL_WITH_RELATIONS,
"tags": ALL_WITH_RELATIONS,
"attachments": ALL_WITH_RELATIONS,
}
api.liaisons.register(LiaisonStatementResource())
from ietf.group.resources import GroupResource
class LiaisonStatementGroupContactsResource(ModelResource):
group = ToOneField(GroupResource, 'group')
class Meta:
queryset = LiaisonStatementGroupContacts.objects.all()
#resource_name = 'liaisonstatementgroupcontacts'
filtering = {
"id": ALL,
"contacts": ALL,
"group": ALL_WITH_RELATIONS,
}
api.liaisons.register(LiaisonStatementGroupContactsResource())
from ietf.person.resources import PersonResource
from ietf.name.resources import LiaisonStatementEventTypeNameResource
class LiaisonStatementEventResource(ModelResource):
type = ToOneField(LiaisonStatementEventTypeNameResource, 'type')
by = ToOneField(PersonResource, 'by')
statement = ToOneField(LiaisonStatementResource, 'statement')
class Meta:
queryset = LiaisonStatementEvent.objects.all()
#resource_name = 'liaisonstatementevent'
filtering = {
"id": ALL,
"time": ALL,
"desc": ALL,
"type": ALL_WITH_RELATIONS,
"by": ALL_WITH_RELATIONS,
"statement": ALL_WITH_RELATIONS,
}
api.liaisons.register(LiaisonStatementEventResource())
from ietf.doc.resources import DocumentResource
class LiaisonStatementAttachmentResource(ModelResource):
statement = ToOneField(LiaisonStatementResource, 'statement')
document = ToOneField(DocumentResource, 'document')
class Meta:
queryset = LiaisonStatementAttachment.objects.all()
#resource_name = 'liaisonstatementattachment'
filtering = {
"id": ALL,
"removed": ALL,
"statement": ALL_WITH_RELATIONS,
"document": ALL_WITH_RELATIONS,
}
api.liaisons.register(LiaisonStatementAttachmentResource())
from ietf.name.resources import DocRelationshipNameResource
class RelatedLiaisonStatementResource(ModelResource):
source = ToOneField(LiaisonStatementResource, 'source')
target = ToOneField(LiaisonStatementResource, 'target')
relationship = ToOneField(DocRelationshipNameResource, 'relationship')
class Meta:
queryset = RelatedLiaisonStatement.objects.all()
#resource_name = 'relatedliaisonstatement'
filtering = {
"id": ALL,
"source": ALL_WITH_RELATIONS,
"target": ALL_WITH_RELATIONS,
"relationship": ALL_WITH_RELATIONS,
}
api.liaisons.register(RelatedLiaisonStatementResource())

File diff suppressed because it is too large Load diff

View file

@ -4,20 +4,34 @@ from django.conf.urls import patterns, url
from django.views.generic import RedirectView, TemplateView
urlpatterns = patterns('',
(r'^help/$', TemplateView.as_view(template_name='liaisons/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='https://www.ietf.org/liaison/managers.html')),
(r'^help/$', TemplateView.as_view(template_name='liaisons/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='https://www.ietf.org/liaison/managers.html')),
)
# AJAX views
urlpatterns += patterns('ietf.liaisons.views',
url(r'^$', 'liaison_list', name='liaison_list'),
url(r'^(?P<object_id>\d+)/$', 'liaison_detail', name='liaison_detail'),
url(r'^(?P<object_id>\d+)/edit/$', 'liaison_edit', name='liaison_edit'),
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/$', 'ajax_get_liaison_info'),
url(r'^ajax/select2search/$', 'ajax_select2_search_liaison_statements', name='ajax_select2_search_liaison_statements'),
(r'^ajax/get_info/$', 'ajax_get_liaison_info'),
(r'^ajax/select2search/$', 'ajax_select2_search_liaison_statements'),
)
# Views
urlpatterns += patterns('ietf.liaisons.views',
(r'^$', 'liaison_list'),
(r'^(?P<state>(posted|pending|dead))/$', 'liaison_list'),
(r'^(?P<object_id>\d+)/$', 'liaison_detail'),
(r'^(?P<object_id>\d+)/edit/$', 'liaison_edit'),
(r'^(?P<object_id>\d+)/edit-attachment/(?P<doc_id>[A-Za-z0-9._+-]+)$', 'liaison_edit_attachment'),
(r'^(?P<object_id>\d+)/delete-attachment/(?P<attach_id>[A-Za-z0-9._+-]+)$', 'liaison_delete_attachment'),
(r'^(?P<object_id>\d+)/history/$', 'liaison_history'),
(r'^(?P<object_id>\d+)/reply/$', 'liaison_reply'),
(r'^(?P<object_id>\d+)/resend/$', 'liaison_resend'),
(r'^add/(?P<type>(incoming|outgoing))/$', 'liaison_add'),
# Redirects for backwards compatibility
(r'^add/$', 'redirect_add'),
(r'^for_approval/$', 'redirect_for_approval'),
(r'^for_approval/(?P<object_id>\d+)/$', 'redirect_for_approval'),
)

View file

@ -1,548 +1,82 @@
from django.db.models import Q
from itertools import chain
from ietf.group.models import Group, Role
from ietf.liaisons.models import LiaisonStatement
from ietf.ietfauth.utils import has_role, passes_test_decorator
from ietf.liaisons.accounts import (is_ietfchair, is_iabchair, is_iab_executive_director, is_irtfchair,
get_ietf_chair, get_iab_chair, get_iab_executive_director, get_irtf_chair,
is_secretariat, can_add_liaison, get_person_for_user, proxy_personify_role)
# a list of tuples, group query kwargs, role query kwargs
GROUP_APPROVAL_MAPPING = [
({'acronym':'ietf'},{'name':'chair'}),
({'acronym':'iab'},{'name':'chair'}),
({'type':'area'},{'name':'ad'}),
({'type':'wg'},{'name':'ad'})]
can_submit_liaison_required = passes_test_decorator(
lambda u, *args, **kwargs: can_add_liaison(u),
"Restricted to participants who are authorized to submit liaison statements on behalf of the various IETF entities")
def approval_roles(group):
'''Returns roles that have approval authority for group'''
for group_kwargs,role_kwargs in GROUP_APPROVAL_MAPPING:
if group in Group.objects.filter(**group_kwargs):
# TODO is there a cleaner way?
if group.type == 'wg':
return Role.objects.filter(group=group.parent,**role_kwargs)
else:
return Role.objects.filter(group=group,**role_kwargs)
def approvable_liaison_statements(user):
liaisons = LiaisonStatement.objects.filter(approved=None)
'''Returns a queryset of Liaison Statements in pending state that user has authority
to approve'''
liaisons = LiaisonStatement.objects.filter(state__slug='pending')
person = get_person_for_user(user)
if has_role(user, "Secretariat"):
return liaisons
# this is a bit complicated because IETFHM encodes the
# groups, it should just give us a list of ids or acronyms
group_codes = IETFHM.get_all_can_approve_codes(get_person_for_user(user))
group_acronyms = []
group_ids = []
for x in group_codes:
if "_" in x:
group_ids.append(x.split("_")[1])
approvable_liaisons = []
for liaison in liaisons:
for group in liaison.from_groups.all():
if person not in [ r.person for r in approval_roles(group) ]:
break
else:
group_acronyms.append(x)
approvable_liaisons.append(liaison.pk)
return liaisons.filter(Q(from_group__acronym__in=group_acronyms) | Q(from_group__pk__in=group_ids))
return liaisons.filter(id__in=approvable_liaisons)
# the following is a biggish object hierarchy abstracting the entity
# names and auth rules for posting liaison statements in a sort of
# semi-declarational (and perhaps overengineered given the revamped
# schema) way - unfortunately, it's never been strong enough to do so
# fine-grained enough so the form code also has some rules
IETFCHAIR = {'name': u'The IETF Chair', 'address': u'chair@ietf.org'}
IESG = {'name': u'The IESG', 'address': u'iesg@ietf.org'}
IAB = {'name': u'The IAB', 'address': u'iab@iab.org'}
IABCHAIR = {'name': u'The IAB Chair', 'address': u'iab-chair@iab.org'}
IABEXECUTIVEDIRECTOR = {'name': u'The IAB Executive Director', 'address': u'execd@iab.org'}
IRTFCHAIR = {'name': u'The IRTF Chair', 'address': u'irtf-chair@irtf.org'}
IESGANDIAB = {'name': u'The IESG and IAB', 'address': u'iesg-iab@ietf.org'}
class FakePerson(object):
def __init__(self, name, address):
self.name = name
self.address = address
def email(self):
return (self.name, self.address)
def all_sdo_managers():
return [proxy_personify_role(r) for r in Role.objects.filter(group__type="sdo", name="liaiman").select_related("person").distinct()]
def role_persons_with_fixed_email(group, role_name):
return [proxy_personify_role(r) for r in Role.objects.filter(group=group, name=role_name).select_related("person").distinct()]
class Entity(object):
poc = []
cc = []
def __init__(self, name, obj=None):
self.name = name
self.obj = obj
def get_poc(self):
if not isinstance(self.poc, list):
return [self.poc]
return self.poc
def get_cc(self, person=None):
if not isinstance(self.cc, list):
return [self.cc]
return self.cc
def get_from_cc(self, person=None):
return []
def needs_approval(self, person=None):
return False
def can_approve(self):
return []
def post_only(self, person, user):
return False
def full_user_list(self):
return False
class IETFEntity(Entity):
poc = FakePerson(**IETFCHAIR)
cc = FakePerson(**IESG)
def get_from_cc(self, person):
result = []
if not is_ietfchair(person):
result.append(self.poc)
result.append(self.cc)
return result
def needs_approval(self, person=None):
if is_ietfchair(person):
return False
def can_edit_liaison(user, liaison):
'''Return True if user is Secretariat or Liaison Manager of all SDO groups involved'''
if has_role(user, "Secretariat"):
return True
def can_approve(self):
return [self.poc]
def full_user_list(self):
result = all_sdo_managers()
result.append(get_ietf_chair())
return result
class IABEntity(Entity):
chair = FakePerson(**IABCHAIR)
director = FakePerson(**IABEXECUTIVEDIRECTOR)
poc = [chair, director]
cc = FakePerson(**IAB)
def get_from_cc(self, person):
result = []
if not is_iabchair(person):
result.append(self.chair)
result.append(self.cc)
if not is_iab_executive_director(person):
result.append(self.director)
return result
def needs_approval(self, person=None):
if is_iabchair(person) or is_iab_executive_director(person):
return False
return True
def can_approve(self):
return [self.chair]
def full_user_list(self):
result = all_sdo_managers()
result += [get_iab_chair(), get_iab_executive_director()]
return result
class IRTFEntity(Entity):
chair = FakePerson(**IRTFCHAIR)
poc = [chair,]
def get_from_cc(self, person):
result = []
return result
def needs_approval(self, person=None):
if is_irtfchair(person):
return False
return True
def can_approve(self):
return [self.chair]
def full_user_list(self):
result = [get_irtf_chair()]
return result
class IAB_IESG_Entity(Entity):
poc = [IABEntity.chair, IABEntity.director, FakePerson(**IETFCHAIR), FakePerson(**IESGANDIAB), ]
cc = [FakePerson(**IAB), FakePerson(**IESG), FakePerson(**IESGANDIAB)]
def __init__(self, name, obj=None):
self.name = name
self.obj = obj
self.iab = IABEntity(name, obj)
self.iesg = IETFEntity(name, obj)
def get_from_cc(self, person):
return list(set(self.iab.get_from_cc(person) + self.iesg.get_from_cc(person)))
def needs_approval(self, person=None):
if not self.iab.needs_approval(person):
return False
if not self.iesg.needs_approval(person):
return False
return True
def can_approve(self):
return list(set(self.iab.can_approve() + self.iesg.can_approve()))
def full_user_list(self):
return [get_ietf_chair(), get_iab_chair(), get_iab_executive_director()]
class AreaEntity(Entity):
def get_poc(self):
return role_persons_with_fixed_email(self.obj, "ad")
def get_cc(self, person=None):
return [FakePerson(**IETFCHAIR)]
def get_from_cc(self, person):
result = [p for p in role_persons_with_fixed_email(self.obj, "ad") if p != person]
result.append(FakePerson(**IETFCHAIR))
return result
def needs_approval(self, person=None):
# Check if person is an area director
if self.obj.role_set.filter(person=person, name="ad"):
return False
return True
def can_approve(self):
return self.get_poc()
def full_user_list(self):
result = all_sdo_managers()
result += self.get_poc()
return result
class WGEntity(Entity):
def get_poc(self):
return role_persons_with_fixed_email(self.obj, "chair")
def get_cc(self, person=None):
if self.obj.parent:
result = [p for p in role_persons_with_fixed_email(self.obj.parent, "ad") if p != person]
if has_role(user, "Liaison Manager"):
person = get_person_for_user(user)
for group in chain(liaison.from_groups.filter(type_id='sdo'),liaison.to_groups.filter(type_id='sdo')):
if not person.role_set.filter(group=group,name='liaiman'):
return False
else:
result = []
if self.obj.list_email:
result.append(FakePerson(name ='%s Discussion List' % self.obj.name,
address = self.obj.list_email))
return result
return True
def get_from_cc(self, person):
result = [p for p in role_persons_with_fixed_email(self.obj, "chair") if p != person]
if self.obj.parent:
result += role_persons_with_fixed_email(self.obj.parent, "ad")
if self.obj.list_email:
result.append(FakePerson(name ='%s Discussion List' % self.obj.name,
address = self.obj.list_email))
return result
return False
def needs_approval(self, person=None):
# Check if person is AD of area
if self.obj.parent and self.obj.parent.role_set.filter(person=person, name="ad"):
def get_person_for_user(user):
try:
return user.person
except:
return None
def can_add_outgoing_liaison(user):
return has_role(user, ["Area Director","WG Chair","WG Secretary","IETF Chair","IAB Chair",
"IAB Executive Director","Liaison Manager","Secretariat"])
def can_add_incoming_liaison(user):
return has_role(user, ["Liaison Manager","Authorized Individual","Secretariat"])
def can_add_liaison(user):
return can_add_incoming_liaison(user) or can_add_outgoing_liaison(user)
def is_authorized_individual(user, groups):
'''Returns True if the user has authorized_individual role for each of the groups'''
for group in groups:
if not Role.objects.filter(person=user.person, group=group, name="auth"):
return False
return True
return True
def can_approve(self):
return role_persons_with_fixed_email(self.obj.parent, "ad") if self.obj.parent else []
def full_user_list(self):
result = all_sdo_managers()
result += self.get_poc()
return result
class SDOEntity(Entity):
def get_poc(self):
return []
def get_cc(self, person=None):
return role_persons_with_fixed_email(self.obj, "liaiman")
def get_from_cc(self, person=None):
return [p for p in role_persons_with_fixed_email(self.obj, "liaiman") if p != person]
def post_only(self, person, user):
if is_secretariat(user) or self.obj.role_set.filter(person=person, name="auth"):
return False
return True
def full_user_list(self):
result = role_persons_with_fixed_email(self.obj, "liaiman")
result += role_persons_with_fixed_email(self.obj, "auth")
return result
class EntityManager(object):
def __init__(self, pk=None, name=None, queryset=None):
self.pk = pk
self.name = name
self.queryset = queryset
def get_entity(self, pk=None):
return Entity(name=self.name)
def get_managed_list(self):
return [(self.pk, self.name)]
def can_send_on_behalf(self, person):
return []
def can_approve_list(self, person):
return []
class IETFEntityManager(EntityManager):
def __init__(self, *args, **kwargs):
super(IETFEntityManager, self).__init__(*args, **kwargs)
self.entity = IETFEntity(name=self.name)
def get_entity(self, pk=None):
return self.entity
def can_send_on_behalf(self, person):
if is_ietfchair(person):
return self.get_managed_list()
return []
def can_approve_list(self, person):
if is_ietfchair(person):
return self.get_managed_list()
return []
class IABEntityManager(EntityManager):
def __init__(self, *args, **kwargs):
super(IABEntityManager, self).__init__(*args, **kwargs)
self.entity = IABEntity(name=self.name)
def get_entity(self, pk=None):
return self.entity
def can_send_on_behalf(self, person):
if (is_iabchair(person) or
is_iab_executive_director(person)):
return self.get_managed_list()
return []
def can_approve_list(self, person):
if (is_iabchair(person) or
is_iab_executive_director(person)):
return self.get_managed_list()
return []
class IRTFEntityManager(EntityManager):
def __init__(self, *args, **kwargs):
super(IRTFEntityManager, self).__init__(*args, **kwargs)
self.entity = IRTFEntity(name=self.name)
def get_entity(self, pk=None):
return self.entity
def can_send_on_behalf(self, person):
if is_irtfchair(person):
return self.get_managed_list()
return []
def can_approve_list(self, person):
if is_irtfchair(person):
return self.get_managed_list()
return []
class IAB_IESG_EntityManager(EntityManager):
def __init__(self, *args, **kwargs):
super(IAB_IESG_EntityManager, self).__init__(*args, **kwargs)
self.entity = IAB_IESG_Entity(name=self.name)
def get_entity(self, pk=None):
return self.entity
def can_send_on_behalf(self, person):
if (is_iabchair(person) or
is_iab_executive_director(person) or
is_ietfchair(person)):
return self.get_managed_list()
return []
def can_approve_list(self, person):
if (is_iabchair(person) or
is_iab_executive_director(person) or
is_ietfchair(person)):
return self.get_managed_list()
return []
class AreaEntityManager(EntityManager):
def __init__(self, pk=None, name=None, queryset=None):
super(AreaEntityManager, self).__init__(pk, name, queryset)
if self.queryset == None:
self.queryset = Group.objects.filter(type="area", state="active")
def get_managed_list(self, query_filter=None):
if not query_filter:
query_filter = {}
return [(u'%s_%s' % (self.pk, i.pk), i.name) for i in self.queryset.filter(**query_filter).order_by('name')]
def get_entity(self, pk=None):
if not pk:
return None
try:
obj = self.queryset.get(pk=pk)
except self.queryset.model.DoesNotExist:
return None
return AreaEntity(name=obj.name, obj=obj)
def can_send_on_behalf(self, person):
query_filter = dict(role__person=person, role__name="ad")
return self.get_managed_list(query_filter)
def can_approve_list(self, person):
query_filter = dict(role__person=person, role__name="ad")
return self.get_managed_list(query_filter)
class WGEntityManager(EntityManager):
def __init__(self, pk=None, name=None, queryset=None):
super(WGEntityManager, self).__init__(pk, name, queryset)
if self.queryset == None:
self.queryset = Group.objects.filter(type="wg", state="active", parent__state="active").select_related("parent")
def get_managed_list(self, query_filter=None):
if not query_filter:
query_filter = {}
return [(u'%s_%s' % (self.pk, i.pk), '%s - %s' % (i.acronym, i.name)) for i in self.queryset.filter(**query_filter).order_by('acronym')]
def get_entity(self, pk=None):
if not pk:
return None
try:
obj = self.queryset.get(pk=pk)
except self.queryset.model.DoesNotExist:
return None
return WGEntity(name=obj.name, obj=obj)
def can_send_on_behalf(self, person):
wgs = Group.objects.filter(role__person=person, role__name__in=("chair", "secretary")).values_list('pk', flat=True)
query_filter = {'pk__in': wgs}
return self.get_managed_list(query_filter)
def can_approve_list(self, person):
query_filter = dict(parent__role__person=person, parent__role__name="ad")
return self.get_managed_list(query_filter)
class SDOEntityManager(EntityManager):
def __init__(self, pk=None, name=None, queryset=None):
super(SDOEntityManager, self).__init__(pk, name, queryset)
if self.queryset == None:
self.queryset = Group.objects.filter(type="sdo")
def get_managed_list(self):
return [(u'%s_%s' % (self.pk, i.pk), i.name) for i in self.queryset.order_by('name')]
def get_entity(self, pk=None):
if not pk:
return None
try:
obj = self.queryset.get(pk=pk)
except self.queryset.model.DoesNotExist:
return None
return SDOEntity(name=obj.name, obj=obj)
class IETFHierarchyManager(object):
def __init__(self):
self.managers = {'ietf': IETFEntityManager(pk='ietf', name=u'The IETF'),
'iesg': IETFEntityManager(pk='iesg', name=u'The IESG'),
'iab': IABEntityManager(pk='iab', name=u'The IAB'),
'iabiesg': IAB_IESG_EntityManager(pk='iabiesg', name=u'The IESG and the IAB'),
'area': AreaEntityManager(pk='area', name=u'IETF Areas'),
'wg': WGEntityManager(pk='wg', name=u'IETF Working Groups'),
'sdo': SDOEntityManager(pk='sdo', name=u'Standards Development Organizations'),
'othersdo': EntityManager(pk='othersdo', name=u'Other SDOs'),
}
def get_entity_by_key(self, entity_id):
if not entity_id:
return None
id_list = entity_id.split('_', 1)
key = id_list[0]
pk = None
if len(id_list)==2:
pk = id_list[1]
if key not in self.managers.keys():
return None
return self.managers[key].get_entity(pk)
def get_all_entities(self):
entities = []
for manager in self.managers.values():
entities += manager.get_managed_list()
return entities
def get_all_incoming_entities(self):
entities = []
results = []
for key in ['ietf', 'iesg', 'iab', 'iabiesg']:
results += self.managers[key].get_managed_list()
entities.append(('Main IETF Entities', results))
entities.append(('IETF Areas', self.managers['area'].get_managed_list()))
entities.append(('IETF Working Groups', self.managers['wg'].get_managed_list()))
return entities
def get_all_outgoing_entities(self):
entities = [(self.managers['sdo'].name, self.managers['sdo'].get_managed_list())]
entities += [(self.managers['othersdo'].name, self.managers['othersdo'].get_managed_list())]
return entities
def get_entities_for_person(self, person):
entities = []
results = []
for key in ['ietf', 'iesg', 'iab', 'iabiesg']:
results += self.managers[key].can_send_on_behalf(person)
if results:
entities.append(('Main IETF Entities', results))
areas = self.managers['area'].can_send_on_behalf(person)
if areas:
entities.append(('IETF Areas', areas))
wgs = self.managers['wg'].can_send_on_behalf(person)
if wgs:
entities.append(('IETF Working Groups', wgs))
return entities
def get_all_can_approve_codes(self, person):
entities = []
for key in ['ietf', 'iesg', 'iab', 'iabiesg']:
entities += self.managers[key].can_approve_list(person)
entities += self.managers['area'].can_approve_list(person)
entities += self.managers['wg'].can_approve_list(person)
return [i[0] for i in entities]
IETFHM = IETFHierarchyManager()

View file

@ -1,179 +1,69 @@
# Copyright The IETF Trust 2007, All Rights Reserved
import datetime
import json
from email.utils import parseaddr
from django.contrib import messages
from django.core.urlresolvers import reverse as urlreverse
from django.core.validators import validate_email, ValidationError
from django.db.models import Q
from django.http import HttpResponse, HttpResponseForbidden
from django.shortcuts import render_to_response, get_object_or_404, redirect
from django.shortcuts import render, render_to_response, get_object_or_404, redirect
from django.template import RequestContext
from ietf.liaisons.models import LiaisonStatement
from ietf.liaisons.accounts import (get_person_for_user, can_add_outgoing_liaison,
can_add_incoming_liaison,
is_ietfchair, is_iabchair, is_iab_executive_director,
can_edit_liaison, is_secretariat)
from ietf.liaisons.forms import liaison_form_factory
from ietf.liaisons.utils import IETFHM, can_submit_liaison_required, approvable_liaison_statements
from ietf.doc.models import Document
from ietf.ietfauth.utils import role_required, has_role
from ietf.group.models import Group, Role
from ietf.liaisons.models import (LiaisonStatement,LiaisonStatementEvent,
LiaisonStatementAttachment)
from ietf.liaisons.utils import (get_person_for_user, can_add_outgoing_liaison,
can_add_incoming_liaison, can_edit_liaison,can_submit_liaison_required,
can_add_liaison)
from ietf.liaisons.forms import liaison_form_factory, SearchLiaisonForm, EditAttachmentForm
from ietf.liaisons.mails import notify_pending_by_email, send_liaison_by_email
from ietf.liaisons.fields import select2_id_liaison_json
EMAIL_ALIASES = {
'IETFCHAIR':'The IETF Chair <chair@ietf.org>',
'IESG':'The IESG <iesg@ietf.org>',
'IAB':'The IAB <iab@iab.org>',
'IABCHAIR':'The IAB Chair <iab-chair@iab.org>',
'IABEXECUTIVEDIRECTOR':'The IAB Executive Director <execd@iab.org>'}
@can_submit_liaison_required
def add_liaison(request, liaison=None):
if 'incoming' in request.GET.keys() and not can_add_incoming_liaison(request.user):
return HttpResponseForbidden("Restricted to users who are authorized to submit incoming liaison statements")
if request.method == 'POST':
form = liaison_form_factory(request, data=request.POST.copy(),
files = request.FILES, liaison=liaison)
if form.is_valid():
liaison = form.save()
if request.POST.get('send', False):
if not liaison.approved:
notify_pending_by_email(request, liaison)
else:
send_liaison_by_email(request, liaison)
return redirect('liaison_list')
else:
form = liaison_form_factory(request, liaison=liaison)
return render_to_response(
'liaisons/edit.html',
{'form': form,
'liaison': liaison},
context_instance=RequestContext(request),
)
@can_submit_liaison_required
def ajax_get_liaison_info(request):
person = get_person_for_user(request.user)
to_entity_id = request.GET.get('to_entity_id', None)
from_entity_id = request.GET.get('from_entity_id', None)
result = {'poc': [], 'cc': [], 'needs_approval': False, 'post_only': False, 'full_list': []}
to_error = 'Invalid TO entity id'
if to_entity_id:
to_entity = IETFHM.get_entity_by_key(to_entity_id)
if to_entity:
to_error = ''
from_error = 'Invalid FROM entity id'
if from_entity_id:
from_entity = IETFHM.get_entity_by_key(from_entity_id)
if from_entity:
from_error = ''
if to_error or from_error:
result.update({'error': '\n'.join([to_error, from_error])})
else:
result.update({'error': False,
'cc': ([i.email() for i in to_entity.get_cc(person=person)] +
[i.email() for i in from_entity.get_from_cc(person=person)]),
'poc': [i.email() for i in to_entity.get_poc()],
'needs_approval': from_entity.needs_approval(person=person),
'post_only': from_entity.post_only(person=person, user=request.user)})
if is_secretariat(request.user):
full_list = [(i.pk, i.email()) for i in set(from_entity.full_user_list())]
full_list.sort(key=lambda x: x[1])
full_list = [(person.pk, person.email())] + full_list
result.update({'full_list': full_list})
json_result = json.dumps(result)
return HttpResponse(json_result, content_type='text/javascript')
def normalize_sort(request):
sort = request.GET.get('sort', "")
if sort not in ('submitted', 'deadline', 'title', 'to_name', 'from_name'):
sort = "submitted"
# reverse dates
order_by = "-" + sort if sort in ("submitted", "deadline") else sort
return sort, order_by
def liaison_list(request):
sort, order_by = normalize_sort(request)
liaisons = LiaisonStatement.objects.exclude(approved=None).order_by(order_by).prefetch_related("attachments")
can_send_outgoing = can_add_outgoing_liaison(request.user)
can_send_incoming = can_add_incoming_liaison(request.user)
approvable = approvable_liaison_statements(request.user).count()
return render_to_response('liaisons/overview.html', {
"liaisons": liaisons,
"can_manage": approvable or can_send_incoming or can_send_outgoing,
"approvable": approvable,
"can_send_incoming": can_send_incoming,
"can_send_outgoing": can_send_outgoing,
"sort": sort,
}, context_instance=RequestContext(request))
def ajax_select2_search_liaison_statements(request):
q = [w.strip() for w in request.GET.get('q', '').split() if w.strip()]
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):
liaisons = approvable_liaison_statements(request.user).order_by("-submitted")
return render_to_response('liaisons/approval_list.html', {
"liaisons": liaisons,
}, context_instance=RequestContext(request))
@can_submit_liaison_required
def liaison_approval_detail(request, object_id):
liaison = get_object_or_404(approvable_liaison_statements(request.user), pk=object_id)
if request.method == 'POST' and request.POST.get('do_approval', False):
liaison.approved = datetime.datetime.now()
liaison.save()
send_liaison_by_email(request, liaison)
return redirect('liaison_list')
return render_to_response('liaisons/approval_detail.html', {
"liaison": liaison,
"is_approving": True,
}, context_instance=RequestContext(request))
# -------------------------------------------------
# Helper Functions
# -------------------------------------------------
def _can_reply(liaison, user):
'''Returns true if the user can send a reply to this liaison'''
if user.is_authenticated():
person = get_person_for_user(user)
if has_role(user, "Secretariat"):
return True
if liaison.is_outgoing() and Role.objects.filter(group__in=liaison.to_groups.all(),person=person,name='auth'):
return True
if not liaison.is_outgoing() and Role.objects.filter(group__in=liaison.from_groups.all(),person=person,name='liaiman'):
return True
return False
def _can_take_care(liaison, user):
'''Returns True if user can take care of awaiting actions associated with this liaison'''
if not liaison.deadline or liaison.action_taken:
return False
if user.is_authenticated():
if is_secretariat(user):
if has_role(user, "Secretariat"):
return True
else:
return _find_person_in_emails(liaison, get_person_for_user(user))
return False
def _find_person_in_emails(liaison, person):
'''Returns true if person corresponds with any of the email addresses associated
with this liaison statement'''
if not person:
return False
emails = ','.join(e for e in [liaison.cc, liaison.to_contact, liaison.to_name,
liaison.reply_to, liaison.response_contact,
liaison.technical_contact] if e)
emails = ','.join(e for e in [liaison.response_contacts, liaison.cc_contacts,
liaison.to_contacts,liaison.technical_contacts] if e)
for email in emails.split(','):
name, addr = parseaddr(email)
try:
@ -183,37 +73,445 @@ def _find_person_in_emails(liaison, person):
if person.email_set.filter(address=addr):
return True
elif addr in ('chair@ietf.org', 'iesg@ietf.org') and is_ietfchair(person):
elif addr in ('chair@ietf.org', 'iesg@ietf.org') and has_role(person.user, "IETF Chair"):
return True
elif addr in ('iab@iab.org', 'iab-chair@iab.org') and is_iabchair(person):
elif addr in ('iab@iab.org', 'iab-chair@iab.org') and has_role(person.user, "IAB Chair"):
return True
elif addr in ('execd@iab.org', ) and is_iab_executive_director(person):
elif addr in ('execd@iab.org', ) and has_role(person.user, "IAB Executive Director"):
return True
return False
def contacts_from_roles(roles):
'''Returns contact string for given roles'''
emails = [ '{} <{}>'.format(r.person.plain_name(),r.email.address) for r in roles ]
return ','.join(emails)
def get_cc(group):
'''Returns list of emails to use as CC for group. Simplified refactor of IETFHierarchy
get_cc() and get_from_cc()
'''
emails = []
# role based CCs
if group.acronym in ('ietf','iesg'):
emails.append(EMAIL_ALIASES['IESG'])
emails.append(EMAIL_ALIASES['IETFCHAIR'])
elif group.acronym in ('iab'):
emails.append(EMAIL_ALIASES['IAB'])
emails.append(EMAIL_ALIASES['IABCHAIR'])
emails.append(EMAIL_ALIASES['IABEXECUTIVEDIRECTOR'])
elif group.type_id == 'area':
emails.append(EMAIL_ALIASES['IETFCHAIR'])
ad_roles = group.role_set.filter(name='ad')
emails.extend([ '{} <{}>'.format(r.person.plain_name(),r.email.address) for r in ad_roles ])
elif group.type_id == 'wg':
ad_roles = group.parent.role_set.filter(name='ad')
emails.extend([ '{} <{}>'.format(r.person.plain_name(),r.email.address) for r in ad_roles ])
chair_roles = group.role_set.filter(name='chair')
emails.extend([ '{} <{}>'.format(r.person.plain_name(),r.email.address) for r in chair_roles ])
if group.list_email:
emails.append('{} Discussion List <{}>'.format(group.name,group.list_email))
elif group.type_id == 'sdo':
liaiman_roles = group.role_set.filter(name='liaiman')
emails.extend([ '{} <{}>'.format(r.person.plain_name(),r.email.address) for r in liaiman_roles ])
# explicit CCs
if group.liaisonstatementgroupcontacts_set.exists() and group.liaisonstatementgroupcontacts_set.first().cc_contacts:
emails = emails + group.liaisonstatementgroupcontacts_set.first().cc_contacts.split(',')
return emails
def get_contacts_for_group(group):
'''Returns default contacts for groups as a comma separated string'''
contacts = []
# use explicit default contacts if defined
if group.liaisonstatementgroupcontacts_set.first():
contacts.append(group.liaisonstatementgroupcontacts_set.first().contacts)
# otherwise construct based on group type
elif group.type_id == 'area':
roles = group.role_set.filter(name='ad')
contacts.append(contacts_from_roles(roles))
elif group.type_id == 'wg':
roles = group.role_set.filter(name='chair')
contacts.append(contacts_from_roles(roles))
elif group.acronym == 'ietf':
contacts.append(EMAIL_ALIASES['IETFCHAIR'])
elif group.acronym == 'iab':
contacts.append(EMAIL_ALIASES['IABCHAIR'])
contacts.append(EMAIL_ALIASES['IABEXECUTIVEDIRECTOR'])
elif group.acronym == 'iesg':
contacts.append(EMAIL_ALIASES['IESG'])
return ','.join(contacts)
def get_details_tabs(stmt, selected):
return [
t + (t[0].lower() == selected.lower(),)
for t in [
('Statement', urlreverse('ietf.liaisons.views.liaison_detail', kwargs={ 'object_id': stmt.pk })),
('History', urlreverse('ietf.liaisons.views.liaison_history', kwargs={ 'object_id': stmt.pk }))
]]
def needs_approval(group,person):
'''Returns True if the person does not have authority to send a Liaison Statement
from group. For outgoing Liaison Statements only'''
user = person.user
if group.acronym in ('ietf','iesg') and has_role(user, 'IETF Chair'):
return False
if group.acronym == 'iab' and (has_role(user,'IAB Chair') or has_role(user,'IAB Executive Director')):
return False
if group.type_id == 'area' and group.role_set.filter(name='ad',person=person):
return False
if group.type_id == 'wg' and group.parent and group.parent.role_set.filter(name='ad',person=person):
return False
return True
def normalize_sort(request):
sort = request.GET.get('sort', "")
if sort not in ('date', 'deadline', 'title', 'to_groups', 'from_groups'):
sort = "date"
# reverse dates
order_by = "-" + sort if sort in ("date", "deadline") else sort
return sort, order_by
def post_only(group,person):
'''Returns true if the user is restricted to post_only (vs. post_and_send) for this
group. This is for incoming liaison statements.
- Secretariat have full access.
- Authorized Individuals have full access for the group they are associated with
- Liaison Managers can post only
'''
if group.type_id == 'sdo' and ( not(has_role(person.user,"Secretariat") or group.role_set.filter(name='auth',person=person)) ):
return True
else:
return False
# -------------------------------------------------
# Ajax Functions
# -------------------------------------------------
@can_submit_liaison_required
def ajax_get_liaison_info(request):
'''Returns dictionary of info to update entry form given the groups
that have been selected
'''
person = get_person_for_user(request.user)
from_groups = request.GET.getlist('from_groups', None)
if not any(from_groups):
from_groups = []
to_groups = request.GET.getlist('to_groups', None)
if not any(to_groups):
to_groups = []
from_groups = [ Group.objects.get(id=id) for id in from_groups ]
to_groups = [ Group.objects.get(id=id) for id in to_groups ]
cc = []
does_need_approval = []
can_post_only = []
to_contacts = []
response_contacts = []
result = {'response_contacts':[],'to_contacts': [], 'cc': [], 'needs_approval': False, 'post_only': False, 'full_list': []}
for group in from_groups:
cc.extend(get_cc(group))
does_need_approval.append(needs_approval(group,person))
can_post_only.append(post_only(group,person))
response_contacts.append(get_contacts_for_group(group))
for group in to_groups:
cc.extend(get_cc(group))
to_contacts.append(get_contacts_for_group(group))
# if there are from_groups and any need approval
if does_need_approval:
if any(does_need_approval):
does_need_approval = True
else:
does_need_approval = False
else:
does_need_approval = True
result.update({'error': False,
'cc': list(set(cc)),
'response_contacts':list(set(response_contacts)),
'to_contacts': list(set(to_contacts)),
'needs_approval': does_need_approval,
'post_only': any(can_post_only)})
json_result = json.dumps(result)
return HttpResponse(json_result, content_type='text/javascript')
def ajax_select2_search_liaison_statements(request):
query = [w.strip() for w in request.GET.get('q', '').split() if w.strip()]
if not query:
objs = LiaisonStatement.objects.none()
else:
qs = LiaisonStatement.objects.filter(state='posted')
for term in query:
if term.isdigit():
q = Q(title__icontains=term)|Q(pk=term)
else:
q = Q(title__icontains=term)
qs = qs.filter(q)
objs = qs.distinct().order_by("-id")[:20]
return HttpResponse(select2_id_liaison_json(objs), content_type='application/json')
# -------------------------------------------------
# Redirects for backwards compatibility
# -------------------------------------------------
def redirect_add(request):
"""Redirects old add urls"""
if 'incoming' in request.GET.keys():
return redirect('ietf.liaisons.views.liaison_add', type='incoming')
else:
return redirect('ietf.liaisons.views.liaison_add', type='outgoing')
def redirect_for_approval(request, object_id=None):
"""Redirects old approval urls"""
if object_id:
return redirect('ietf.liaisons.views.liaison_detail', object_id=object_id)
else:
return redirect('ietf.liaisons.views.liaison_list', state='pending')
# -------------------------------------------------
# View Functions
# -------------------------------------------------
@can_submit_liaison_required
def liaison_add(request, type=None, **kwargs):
if type == 'incoming' and not can_add_incoming_liaison(request.user):
return HttpResponseForbidden("Restricted to users who are authorized to submit incoming liaison statements")
if type == 'outgoing' and not can_add_outgoing_liaison(request.user):
return HttpResponseForbidden("Restricted to users who are authorized to submit outgoing liaison statements")
if request.method == 'POST':
form = liaison_form_factory(request, data=request.POST.copy(),
files=request.FILES, type=type, **kwargs)
if form.is_valid():
liaison = form.save()
# notifications
if 'send' in request.POST and liaison.state.slug == 'posted':
send_liaison_by_email(request, liaison)
messages.success(request, 'The statement has been sent and posted')
elif liaison.state.slug == 'pending':
notify_pending_by_email(request, liaison)
messages.success(request, 'The statement has been submitted and is awaiting approval')
return redirect('ietf.liaisons.views.liaison_detail', object_id=liaison.pk)
else:
form = liaison_form_factory(request,type=type,**kwargs)
return render_to_response(
'liaisons/edit.html',
{'form': form,
'liaison': kwargs.get('instance')},
context_instance=RequestContext(request),
)
def liaison_history(request, object_id):
"""Show the history for a specific liaison statement"""
liaison = get_object_or_404(LiaisonStatement, id=object_id)
events = liaison.liaisonstatementevent_set.all().order_by("-time", "-id").select_related("by")
return render(request, "liaisons/detail_history.html", {
'events':events,
'liaison': liaison,
'tabs': get_details_tabs(liaison, 'History'),
'selected_tab_entry':'history'
})
def liaison_delete_attachment(request, object_id, attach_id):
liaison = get_object_or_404(LiaisonStatement, pk=object_id)
attach = get_object_or_404(LiaisonStatementAttachment, pk=attach_id)
if not ( request.user.is_authenticated() and can_edit_liaison(request.user, liaison) ):
return HttpResponseForbidden("You are not authorized for this action")
attach.removed = True
attach.save()
# create event
LiaisonStatementEvent.objects.create(
type_id='modified',
by=get_person_for_user(request.user),
statement=liaison,
desc='Attachment Removed: {}'.format(attach.document.title)
)
messages.success(request, 'Attachment Deleted')
return redirect('ietf.liaisons.views.liaison_detail', object_id=liaison.pk)
def liaison_detail(request, object_id):
liaison = get_object_or_404(LiaisonStatement.objects.exclude(approved=None), pk=object_id)
liaison = get_object_or_404(LiaisonStatement, pk=object_id)
can_edit = request.user.is_authenticated() and can_edit_liaison(request.user, liaison)
can_take_care = _can_take_care(liaison, request.user)
can_reply = _can_reply(liaison, request.user)
person = get_person_for_user(request.user)
if request.method == 'POST' and request.POST.get('do_action_taken', None) and can_take_care:
liaison.action_taken = True
liaison.save()
can_take_care = False
relations = liaison.liaisonstatement_set.exclude(approved=None)
if request.method == 'POST':
if request.POST.get('approved'):
liaison.change_state(state_id='approved',person=person)
liaison.change_state(state_id='posted',person=person)
send_liaison_by_email(request, liaison)
messages.success(request,'Liaison Statement Approved and Posted')
elif request.POST.get('dead'):
liaison.change_state(state_id='dead',person=person)
messages.success(request,'Liaison Statement Killed')
elif request.POST.get('resurrect'):
liaison.change_state(state_id='pending',person=person)
messages.success(request,'Liaison Statement Resurrected')
elif request.POST.get('do_action_taken') and can_take_care:
liaison.tags.remove('required')
liaison.tags.add('taken')
can_take_care = False
messages.success(request,'Action handled')
relations_by = [i.target for i in liaison.source_of_set.filter(target__state__slug='posted')]
relations_to = [i.source for i in liaison.target_of_set.filter(source__state__slug='posted')]
return render_to_response("liaisons/detail.html", {
"liaison": liaison,
'tabs': get_details_tabs(liaison, 'Statement'),
"can_edit": can_edit,
"can_take_care": can_take_care,
"relations": relations,
"can_reply": can_reply,
"relations_to": relations_to,
"relations_by": relations_by,
}, context_instance=RequestContext(request))
def liaison_edit(request, object_id):
liaison = get_object_or_404(LiaisonStatement, pk=object_id)
if not (request.user.is_authenticated() and can_edit_liaison(request.user, liaison)):
return HttpResponseForbidden('You do not have permission to edit this liaison statement')
return add_liaison(request, liaison=liaison)
return liaison_add(request, instance=liaison)
def liaison_edit_attachment(request, object_id, doc_id):
'''Edit the Liaison Statement attachment title'''
liaison = get_object_or_404(LiaisonStatement, pk=object_id)
doc = get_object_or_404(Document, pk=doc_id)
if not ( request.user.is_authenticated() and can_edit_liaison(request.user, liaison) ):
return HttpResponseForbidden("You are not authorized for this action")
if request.method == 'POST':
form = EditAttachmentForm(request.POST)
if form.is_valid():
title = form.cleaned_data.get('title')
doc.title = title
doc.save()
# create event
LiaisonStatementEvent.objects.create(
type_id='modified',
by=get_person_for_user(request.user),
statement=liaison,
desc='Attachment Title changed to {}'.format(title)
)
messages.success(request,'Attachment title changed')
return redirect('ietf.liaisons.views.liaison_detail', object_id=liaison.pk)
else:
form = EditAttachmentForm(initial={'title':doc.title})
return render_to_response(
'liaisons/edit_attachment.html',
{'form': form,
'liaison': liaison},
context_instance=RequestContext(request),
)
def liaison_list(request, state='posted'):
"""A generic list view with tabs for different states: posted, pending, dead"""
liaisons = LiaisonStatement.objects.filter(state=state)
# check authorization for pending and dead tabs
if state in ('pending','dead') and not can_add_liaison(request.user):
msg = "Restricted to participants who are authorized to submit liaison statements on behalf of the various IETF entities"
return HttpResponseForbidden(msg)
# perform search / filter
if 'text' in request.GET:
form = SearchLiaisonForm(data=request.GET)
search_conducted = True
if form.is_valid():
results = form.get_results()
liaisons = results
else:
form = SearchLiaisonForm()
search_conducted = False
# perform sort
sort, order_by = normalize_sort(request)
if sort == 'date':
liaisons = sorted(liaisons, key=lambda a: a.sort_date, reverse=True)
if sort == 'from_groups':
liaisons = sorted(liaisons, key=lambda a: a.from_groups_display)
if sort == 'to_groups':
liaisons = sorted(liaisons, key=lambda a: a.to_groups_display)
if sort == 'deadline':
liaisons = liaisons.order_by('-deadline')
if sort == 'title':
liaisons = liaisons.order_by('title')
# add menu entries
entries = []
entries.append(("Posted", urlreverse("ietf.liaisons.views.liaison_list", kwargs={'state':'posted'})))
if can_add_liaison(request.user):
entries.append(("Pending", urlreverse("ietf.liaisons.views.liaison_list", kwargs={'state':'pending'})))
entries.append(("Dead", urlreverse("ietf.liaisons.views.liaison_list", kwargs={'state':'dead'})))
# add menu actions
actions = []
if can_add_incoming_liaison(request.user):
actions.append(("New incoming liaison", urlreverse("ietf.liaisons.views.liaison_add", kwargs={'type':'incoming'})))
if can_add_outgoing_liaison(request.user):
actions.append(("New outgoing liaison", urlreverse("ietf.liaisons.views.liaison_add", kwargs={'type':'outgoing'})))
return render(request, 'liaisons/liaison_base.html', {
'liaisons':liaisons,
'selected_menu_entry':state,
'menu_entries':entries,
'menu_actions':actions,
'sort':sort,
'form':form,
'with_search':True,
'search_conducted':search_conducted,
'state':state,
})
def liaison_reply(request,object_id):
'''Returns a new liaison form with initial data to reply to the given liaison'''
liaison = get_object_or_404(LiaisonStatement, pk=object_id)
if liaison.is_outgoing():
reply_type = 'incoming'
else:
reply_type = 'outgoing'
initial = dict(
to_groups=[ x.pk for x in liaison.from_groups.all() ],
from_groups=[ x.pk for x in liaison.to_groups.all() ],
related_to=str(liaison.pk))
return liaison_add(request,type=reply_type,initial=initial)
@role_required('Secretariat',)
def liaison_resend(request, object_id):
'''Resend the liaison statement notification email'''
liaison = get_object_or_404(LiaisonStatement, pk=object_id)
person = get_person_for_user(request.user)
send_liaison_by_email(request,liaison)
LiaisonStatementEvent.objects.create(type_id='resent',by=person,statement=liaison,desc='Statement Resent')
messages.success(request,'Liaison Statement resent')
return redirect('ietf.liaisons.views.liaison_list')

View file

@ -1,49 +1,12 @@
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
from django.forms.widgets import Widget
from django.utils.safestring import mark_safe
from django.utils.html import conditional_escape
class FromWidget(Select):
def __init__(self, *args, **kwargs):
super(FromWidget, self).__init__(*args, **kwargs)
self.full_power_on = []
self.reduced_to_set = []
def render(self, name, value, attrs=None, choices=()):
all_choices = list(self.choices) + list(choices)
if (len(all_choices) != 1 or
(isinstance(all_choices[0][1], (list, tuple)) and
len(all_choices[0][1]) != 1)):
base = super(FromWidget, self).render(name, value, attrs, choices)
else:
option = all_choices[0]
if isinstance(option[1], (list, tuple)):
option = option[1][0]
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 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:
base += '<span class="full_power_on_%s"></span>' % conditional_escape(from_code)
for to_code in self.reduced_to_set:
base += '<span class="reduced_to_set_%s"></span>' % conditional_escape(to_code)
base += '</div>'
return mark_safe(base)
class ReadOnlyWidget(Widget):
def render(self, name, value, attrs=None):
html = u'<div id="id_%s" class="form-control widget">%s</div>' % (conditional_escape(name), conditional_escape(value or ''))
return mark_safe(html)
class ButtonWidget(Widget):
def __init__(self, *args, **kwargs):
self.label = kwargs.pop('label', None)
self.show_on = kwargs.pop('show_on', None)
@ -64,15 +27,17 @@ class ButtonWidget(Widget):
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 form-control widget">No files attached</span>'
html += u'<div class="attachedFiles form-control widget">'
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))
html += u'<a class="initialAttach" href="%s%s">%s</a>&nbsp' % (settings.LIAISON_ATTACH_URL, conditional_escape(attachment.document.external_url), conditional_escape(attachment.document.title))
html += u'<a class="btn btn-default btn-xs" href="{}">Edit</a>&nbsp'.format(urlreverse("ietf.liaisons.views.liaison_edit_attachment", kwargs={'object_id':attachment.statement.pk,'doc_id':attachment.document.pk}))
html += u'<a class="btn btn-default btn-xs" href="{}">Delete</a>&nbsp'.format(urlreverse("ietf.liaisons.views.liaison_delete_attachment", kwargs={'object_id':attachment.statement.pk,'attach_id':attachment.pk}))
html += u'<br />'
else:
html += u'No files attached'
html += u'</div></div>'
return mark_safe(html)
return mark_safe(html)

View file

@ -1530,6 +1530,66 @@
"model": "name.liaisonstatementpurposename",
"pk": "response"
},
{
"fields": {
"order": 1,
"used": true,
"name": "Required",
"desc": ""
},
"model": "name.liaisonstatementtagname",
"pk": "required"
},
{
"fields": {
"order": 2,
"used": true,
"name": "Taken",
"desc": ""
},
"model": "name.liaisonstatementtagname",
"pk": "taken"
},
{
"fields": {
"order": 1,
"used": true,
"name": "Pending",
"desc": ""
},
"model": "name.liaisonstatementstate",
"pk": "pending"
},
{
"fields": {
"order": 2,
"used": true,
"name": "Approved",
"desc": ""
},
"model": "name.liaisonstatementstate",
"pk": "approved"
},
{
"fields": {
"order": 3,
"used": true,
"name": "Posted",
"desc": ""
},
"model": "name.liaisonstatementstate",
"pk": "posted"
},
{
"fields": {
"order": 4,
"used": true,
"name": "Dead",
"desc": ""
},
"model": "name.liaisonstatementstate",
"pk": "dead"
},
{
"fields": {
"order": 0,

View file

@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('name', '0005_add_sug_replaces'),
]
operations = [
migrations.CreateModel(
name='LiaisonStatementEventTypeName',
fields=[
('slug', models.CharField(max_length=32, serialize=False, primary_key=True)),
('name', models.CharField(max_length=255)),
('desc', models.TextField(blank=True)),
('used', models.BooleanField(default=True)),
('order', models.IntegerField(default=0)),
],
options={
'ordering': ['order'],
'abstract': False,
},
bases=(models.Model,),
),
migrations.CreateModel(
name='LiaisonStatementState',
fields=[
('slug', models.CharField(max_length=32, serialize=False, primary_key=True)),
('name', models.CharField(max_length=255)),
('desc', models.TextField(blank=True)),
('used', models.BooleanField(default=True)),
('order', models.IntegerField(default=0)),
],
options={
'ordering': ['order'],
'abstract': False,
},
bases=(models.Model,),
),
migrations.CreateModel(
name='LiaisonStatementTagName',
fields=[
('slug', models.CharField(max_length=32, serialize=False, primary_key=True)),
('name', models.CharField(max_length=255)),
('desc', models.TextField(blank=True)),
('used', models.BooleanField(default=True)),
('order', models.IntegerField(default=0)),
],
options={
'ordering': ['order'],
'abstract': False,
},
bases=(models.Model,),
),
]

View file

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
def populate_names(apps, schema_editor):
# LiaisonStatementState: Pending, Approved, Dead
LiaisonStatementState = apps.get_model("name", "LiaisonStatementState")
LiaisonStatementState.objects.create(slug="pending", order=1, name="Pending")
LiaisonStatementState.objects.create(slug="approved", order=2, name="Approved")
LiaisonStatementState.objects.create(slug="posted", order=3, name="Posted")
LiaisonStatementState.objects.create(slug="dead", order=4, name="Dead")
# LiaisonStatementEventTypeName: Submitted, Modified, Approved, Posted, Killed, Resurrected, MsgIn, MsgOut, Comment
LiaisonStatementEventTypeName = apps.get_model("name", "LiaisonStatementEventTypeName")
LiaisonStatementEventTypeName.objects.create(slug="submitted", order=1, name="Submitted")
LiaisonStatementEventTypeName.objects.create(slug="modified", order=2, name="Modified")
LiaisonStatementEventTypeName.objects.create(slug="approved", order=3, name="Approved")
LiaisonStatementEventTypeName.objects.create(slug="posted", order=4, name="Posted")
LiaisonStatementEventTypeName.objects.create(slug="killed", order=5, name="Killed")
LiaisonStatementEventTypeName.objects.create(slug="resurrected", order=6, name="Resurrected")
LiaisonStatementEventTypeName.objects.create(slug="msgin", order=7, name="MsgIn")
LiaisonStatementEventTypeName.objects.create(slug="msgout", order=8, name="MsgOut")
LiaisonStatementEventTypeName.objects.create(slug="comment", order=9, name="Comment")
#LiaisonStatementTagName: Action Required, Action Taken
LiaisonStatementTagName = apps.get_model("name", "LiaisonStatementTagName")
LiaisonStatementTagName.objects.create(slug="required", order=1, name="Action Required")
LiaisonStatementTagName.objects.create(slug="taken", order=2, name="Action Taken")
class Migration(migrations.Migration):
dependencies = [
('name', '0006_add_liaison_names'),
]
operations = [
migrations.RunPython(populate_names),
]

View file

@ -80,4 +80,9 @@ class IprLicenseTypeName(NameModel):
class IprEventTypeName(NameModel):
"""submitted,posted,parked,removed,rejected,msgin,msgoutcomment,private_comment,
legacy,update_notify,change_disclosure"""
class LiaisonStatementState(NameModel):
"Pending, Approved, Dead"
class LiaisonStatementEventTypeName(NameModel):
"Submitted, Modified, Approved, Posted, Killed, Resurrected, MsgIn, MsgOut, Comment"
class LiaisonStatementTagName(NameModel):
"Action Required, Action Taken"

View file

@ -1,17 +1,16 @@
# Autogenerated by the mkresources management command 2014-11-13 23:53
# Autogenerated by the makeresources management command 2015-08-27 11:01 PDT
from tastypie.resources import ModelResource
from tastypie.fields import ToManyField
from tastypie.constants import ALL, ALL_WITH_RELATIONS
from tastypie.fields import ToOneField, ToManyField # pyflakes:ignore
from tastypie.constants import ALL, ALL_WITH_RELATIONS # pyflakes:ignore
from ietf import api
from ietf.name.models import * # pyflakes:ignore
from ietf.name.models import * # pyflakes:ignore
class TimeSlotTypeNameResource(ModelResource):
class Meta:
queryset = TimeSlotTypeName.objects.all()
serializer = api.Serializer()
#resource_name = 'timeslottypename'
filtering = {
"slug": ALL,
@ -25,7 +24,6 @@ api.name.register(TimeSlotTypeNameResource())
class GroupStateNameResource(ModelResource):
class Meta:
queryset = GroupStateName.objects.all()
serializer = api.Serializer()
#resource_name = 'groupstatename'
filtering = {
"slug": ALL,
@ -39,7 +37,6 @@ api.name.register(GroupStateNameResource())
class DocTagNameResource(ModelResource):
class Meta:
queryset = DocTagName.objects.all()
serializer = api.Serializer()
#resource_name = 'doctagname'
filtering = {
"slug": ALL,
@ -53,7 +50,6 @@ api.name.register(DocTagNameResource())
class IntendedStdLevelNameResource(ModelResource):
class Meta:
queryset = IntendedStdLevelName.objects.all()
serializer = api.Serializer()
#resource_name = 'intendedstdlevelname'
filtering = {
"slug": ALL,
@ -67,7 +63,6 @@ api.name.register(IntendedStdLevelNameResource())
class LiaisonStatementPurposeNameResource(ModelResource):
class Meta:
queryset = LiaisonStatementPurposeName.objects.all()
serializer = api.Serializer()
#resource_name = 'liaisonstatementpurposename'
filtering = {
"slug": ALL,
@ -82,7 +77,6 @@ class DraftSubmissionStateNameResource(ModelResource):
next_states = ToManyField('ietf.name.resources.DraftSubmissionStateNameResource', 'next_states', null=True)
class Meta:
queryset = DraftSubmissionStateName.objects.all()
serializer = api.Serializer()
#resource_name = 'draftsubmissionstatename'
filtering = {
"slug": ALL,
@ -97,7 +91,6 @@ api.name.register(DraftSubmissionStateNameResource())
class DocTypeNameResource(ModelResource):
class Meta:
queryset = DocTypeName.objects.all()
serializer = api.Serializer()
#resource_name = 'doctypename'
filtering = {
"slug": ALL,
@ -111,7 +104,6 @@ api.name.register(DocTypeNameResource())
class RoleNameResource(ModelResource):
class Meta:
queryset = RoleName.objects.all()
serializer = api.Serializer()
#resource_name = 'rolename'
filtering = {
"slug": ALL,
@ -122,10 +114,22 @@ class RoleNameResource(ModelResource):
}
api.name.register(RoleNameResource())
class IprDisclosureStateNameResource(ModelResource):
class Meta:
queryset = IprDisclosureStateName.objects.all()
#resource_name = 'iprdisclosurestatename'
filtering = {
"slug": ALL,
"name": ALL,
"desc": ALL,
"used": ALL,
"order": ALL,
}
api.name.register(IprDisclosureStateNameResource())
class StdLevelNameResource(ModelResource):
class Meta:
queryset = StdLevelName.objects.all()
serializer = api.Serializer()
#resource_name = 'stdlevelname'
filtering = {
"slug": ALL,
@ -136,10 +140,22 @@ class StdLevelNameResource(ModelResource):
}
api.name.register(StdLevelNameResource())
class LiaisonStatementEventTypeNameResource(ModelResource):
class Meta:
queryset = LiaisonStatementEventTypeName.objects.all()
#resource_name = 'liaisonstatementeventtypename'
filtering = {
"slug": ALL,
"name": ALL,
"desc": ALL,
"used": ALL,
"order": ALL,
}
api.name.register(LiaisonStatementEventTypeNameResource())
class GroupTypeNameResource(ModelResource):
class Meta:
queryset = GroupTypeName.objects.all()
serializer = api.Serializer()
#resource_name = 'grouptypename'
filtering = {
"slug": ALL,
@ -150,10 +166,22 @@ class GroupTypeNameResource(ModelResource):
}
api.name.register(GroupTypeNameResource())
class IprEventTypeNameResource(ModelResource):
class Meta:
queryset = IprEventTypeName.objects.all()
#resource_name = 'ipreventtypename'
filtering = {
"slug": ALL,
"name": ALL,
"desc": ALL,
"used": ALL,
"order": ALL,
}
api.name.register(IprEventTypeNameResource())
class GroupMilestoneStateNameResource(ModelResource):
class Meta:
queryset = GroupMilestoneStateName.objects.all()
serializer = api.Serializer()
#resource_name = 'groupmilestonestatename'
filtering = {
"slug": ALL,
@ -167,7 +195,6 @@ api.name.register(GroupMilestoneStateNameResource())
class SessionStatusNameResource(ModelResource):
class Meta:
queryset = SessionStatusName.objects.all()
serializer = api.Serializer()
#resource_name = 'sessionstatusname'
filtering = {
"slug": ALL,
@ -181,7 +208,6 @@ api.name.register(SessionStatusNameResource())
class DocReminderTypeNameResource(ModelResource):
class Meta:
queryset = DocReminderTypeName.objects.all()
serializer = api.Serializer()
#resource_name = 'docremindertypename'
filtering = {
"slug": ALL,
@ -195,7 +221,6 @@ api.name.register(DocReminderTypeNameResource())
class ConstraintNameResource(ModelResource):
class Meta:
queryset = ConstraintName.objects.all()
serializer = api.Serializer()
#resource_name = 'constraintname'
filtering = {
"slug": ALL,
@ -210,7 +235,6 @@ api.name.register(ConstraintNameResource())
class MeetingTypeNameResource(ModelResource):
class Meta:
queryset = MeetingTypeName.objects.all()
serializer = api.Serializer()
#resource_name = 'meetingtypename'
filtering = {
"slug": ALL,
@ -224,7 +248,6 @@ api.name.register(MeetingTypeNameResource())
class DocRelationshipNameResource(ModelResource):
class Meta:
queryset = DocRelationshipName.objects.all()
serializer = api.Serializer()
#resource_name = 'docrelationshipname'
filtering = {
"slug": ALL,
@ -239,7 +262,6 @@ api.name.register(DocRelationshipNameResource())
class RoomResourceNameResource(ModelResource):
class Meta:
queryset = RoomResourceName.objects.all()
serializer = api.Serializer()
resource_name = 'roomresourcename' # Needed because tastypie otherwise removes 'resource' from the name
filtering = {
"slug": ALL,
@ -250,10 +272,35 @@ class RoomResourceNameResource(ModelResource):
}
api.name.register(RoomResourceNameResource())
class IprLicenseTypeNameResource(ModelResource):
class Meta:
queryset = IprLicenseTypeName.objects.all()
#resource_name = 'iprlicensetypename'
filtering = {
"slug": ALL,
"name": ALL,
"desc": ALL,
"used": ALL,
"order": ALL,
}
api.name.register(IprLicenseTypeNameResource())
class LiaisonStatementTagNameResource(ModelResource):
class Meta:
queryset = LiaisonStatementTagName.objects.all()
#resource_name = 'liaisonstatementtagname'
filtering = {
"slug": ALL,
"name": ALL,
"desc": ALL,
"used": ALL,
"order": ALL,
}
api.name.register(LiaisonStatementTagNameResource())
class FeedbackTypeNameResource(ModelResource):
class Meta:
queryset = FeedbackTypeName.objects.all()
serializer = api.Serializer()
#resource_name = 'feedbacktypename'
filtering = {
"slug": ALL,
@ -264,10 +311,22 @@ class FeedbackTypeNameResource(ModelResource):
}
api.name.register(FeedbackTypeNameResource())
class LiaisonStatementStateResource(ModelResource):
class Meta:
queryset = LiaisonStatementState.objects.all()
#resource_name = 'liaisonstatementstate'
filtering = {
"slug": ALL,
"name": ALL,
"desc": ALL,
"used": ALL,
"order": ALL,
}
api.name.register(LiaisonStatementStateResource())
class StreamNameResource(ModelResource):
class Meta:
queryset = StreamName.objects.all()
serializer = api.Serializer()
#resource_name = 'streamname'
filtering = {
"slug": ALL,
@ -281,7 +340,6 @@ api.name.register(StreamNameResource())
class BallotPositionNameResource(ModelResource):
class Meta:
queryset = BallotPositionName.objects.all()
serializer = api.Serializer()
#resource_name = 'ballotpositionname'
filtering = {
"slug": ALL,
@ -296,7 +354,6 @@ api.name.register(BallotPositionNameResource())
class DBTemplateTypeNameResource(ModelResource):
class Meta:
queryset = DBTemplateTypeName.objects.all()
serializer = api.Serializer()
#resource_name = 'dbtemplatetypename'
filtering = {
"slug": ALL,
@ -310,7 +367,6 @@ api.name.register(DBTemplateTypeNameResource())
class NomineePositionStateNameResource(ModelResource):
class Meta:
queryset = NomineePositionStateName.objects.all()
serializer = api.Serializer()
#resource_name = 'nomineepositionstatename'
filtering = {
"slug": ALL,
@ -321,47 +377,3 @@ class NomineePositionStateNameResource(ModelResource):
}
api.name.register(NomineePositionStateNameResource())
class IprDisclosureStateNameResource(ModelResource):
class Meta:
queryset = IprDisclosureStateName.objects.all()
serializer = api.Serializer()
#resource_name = 'iprdisclosurestatename'
filtering = {
"slug": ALL,
"name": ALL,
"desc": ALL,
"used": ALL,
"order": ALL,
}
api.name.register(IprDisclosureStateNameResource())
class IprEventTypeNameResource(ModelResource):
class Meta:
queryset = IprEventTypeName.objects.all()
serializer = api.Serializer()
#resource_name = 'ipreventtypename'
filtering = {
"slug": ALL,
"name": ALL,
"desc": ALL,
"used": ALL,
"order": ALL,
}
api.name.register(IprEventTypeNameResource())
class IprLicenseTypeNameResource(ModelResource):
class Meta:
queryset = IprLicenseTypeName.objects.all()
serializer = api.Serializer()
#resource_name = 'iprlicensetypename'
filtering = {
"slug": ALL,
"name": ALL,
"desc": ALL,
"used": ALL,
"order": ALL,
}
api.name.register(IprLicenseTypeNameResource())

View file

@ -6,7 +6,7 @@ from django.db.models import Q
from ietf.group.models import Group, GroupMilestone, Role
from ietf.name.models import GroupStateName, GroupTypeName, RoleName
from ietf.person.models import Person, Email
from ietf.liaisons.models import LiaisonStatementGroupContacts
# ---------------------------------------------
@ -69,6 +69,7 @@ class GroupModelForm(forms.ModelForm):
parent = forms.ModelChoiceField(queryset=Group.objects.filter(Q(type='area',state='active')|Q(acronym='irtf')),required=False)
ad = forms.ModelChoiceField(queryset=Person.objects.filter(role__name='ad',role__group__state='active',role__group__type='area'),required=False)
state = forms.ModelChoiceField(queryset=GroupStateName.objects.exclude(slug__in=('dormant','unknown')),empty_label=None)
liaison_contacts = forms.CharField(max_length=255,required=False,label='Default Liaison Contacts')
class Meta:
model = Group
@ -82,7 +83,12 @@ class GroupModelForm(forms.ModelForm):
self.fields['ad'].label = 'Area Director'
self.fields['comments'].widget.attrs['rows'] = 3
self.fields['parent'].label = 'Area'
if self.instance.pk:
lsgc = self.instance.liaisonstatementgroupcontacts_set.first() # there can only be one
if lsgc:
self.fields['liaison_contacts'].initial = lsgc.contacts
def clean_parent(self):
parent = self.cleaned_data['parent']
type = self.cleaned_data['type']
@ -111,7 +117,24 @@ class GroupModelForm(forms.ModelForm):
raise forms.ValidationError('You must choose "active" or "concluded" for research group state')
return self.cleaned_data
def save(self, force_insert=False, force_update=False, commit=True):
obj = super(GroupModelForm, self).save(commit=False)
if commit:
obj.save()
contacts = self.cleaned_data.get('liaison_contacts')
if contacts:
try:
lsgc = LiaisonStatementGroupContacts.objects.get(group=self.instance)
lsgc.contacts = contacts
lsgc.save()
except LiaisonStatementGroupContacts.DoesNotExist:
LiaisonStatementGroupContacts.objects.create(group=self.instance,contacts=contacts)
elif LiaisonStatementGroupContacts.objects.filter(group=self.instance):
LiaisonStatementGroupContacts.objects.filter(group=self.instance).delete()
return obj
class RoleForm(forms.Form):
name = forms.ModelChoiceField(RoleName.objects.filter(slug__in=('chair','editor','secr','techadv')),empty_label=None)
person = forms.CharField(max_length=50,widget=forms.TextInput(attrs={'class':'name-autocomplete'}),help_text="To see a list of people type the first name, or last name, or both.")

View file

@ -45,6 +45,7 @@ def add_legacy_fields(group):
group.techadvisors = group.role_set.filter(name="techadv")
group.editors = group.role_set.filter(name="editor")
group.secretaries = group.role_set.filter(name="secr")
group.liaison_contacts = group.liaisonstatementgroupcontacts_set.first()
#fill_in_charter_info(group)

View file

@ -46,6 +46,9 @@
<tr><td>Email Address:</td><td>{{ group.list_email }}</td></tr>
<tr><td>Email Subscription:</td><td>{{ group.list_subscribe }}</td></tr>
<tr><td>Email Archive:</td><td>{{ group.list_archive }}</td></tr>
{% if group.liaison_contacts %}
<tr><td>Default Liaison Contacts:</td><td>{{ group.liaison_contacts.contacts }}</td></tr>
{% endif %}
{% if group.features.has_chartering_process %}
<tr><td>Charter:</td><td><a href="{% url "groups_charter" acronym=group.acronym %}">View Charter</a></td></tr>
{% else %}

View file

@ -399,6 +399,8 @@ DOC_HREFS = {
"slides": "https://www.ietf.org/slides/{doc.name}-{doc.rev}",
"conflrev": "https://www.ietf.org/cr/{doc.name}-{doc.rev}.txt",
"statchg": "https://www.ietf.org/sc/{doc.name}-{doc.rev}.txt",
"liaison": "/documents/LIAISON/{doc.external_url}",
"liai-att": "/documents/LIAISON/{doc.external_url}",
}
MEETING_DOC_HREFS = {

View file

@ -0,0 +1,131 @@
.baseform {
font-size: 12px;
}
.baseform .fieldset {
margin: 1em 0px;
border: none;
border: 1px solid #8899dd;
background-color: #edf5ff;
}
.baseform .fieldset h2 {
background-color: #2647a0;
color: white;
font-size: 14px;
padding: 5px 10px;
margin: 0px;
}
.baseform .field {
padding: 0.5em 10px;
}
.baseform .field label {
display: block;
width: 150px;
float: left;
clear: left;
}
.baseform .field .endfield {
clear: left;
}
.baseform .fieldWidget {
margin-left: 150px;
}
.baseform #baseform-fieldname-purpose_text,
.baseform #baseform-fieldname-deadline_date {
display: none;
}
.baseform select,
.baseform textarea,
.baseform input {
border: 1px solid #cccccc;
}
.baseform input {
font-size: 12px;
}
#id_title,
.baseformedit #id_from_field,
.baseform #id_organization,
.baseform #id_to_poc,
.baseform #id_response_contacts,
.baseform #id_technical_contact,
.baseform #id_cc1,
.attach_titleField input,
.baseform textarea {
width: 80%;
}
#id_purpose_text {
height: 100px;
}
#id_body {
height: 300px;
}
.baseform input.disabledAddAttachment {
border: none;
padding: none;
background: none;
padding: 0px;
margin: 0px;
color: black;
font-weight: bold;
}
span.fieldRequired {
color: red;
}
.fieldError {
background-color: #ffcc66;
}
th.sort {
background-image: url(/images/sort-header-clear.png);
background-repeat: no-repeat;
background-position: right center;
cursor: pointer;
}
th.headerSortUp {
background-image: url(/images/sort-header-up-filled.png);
}
th.headerSortDown {
background-image: url(/images/sort-header-filled.png);
}
td span.awaiting {
background-color: #ffcc33;
border-radius: 3px;
float: right;
width: 35px;
padding: 4px 8px;
text-align: center;
font-size: 10px;
}
.noActionTaken, .actionTaken { padding: 2px 5px; }
.actionTaken { border: 1px solid green; background-color: #ccffbb; }
.noActionTaken { border: 1px solid red; background-color: #ffccbb; }
input[id$='DELETE'] {
display: none;
}
#id_from_groups + span {
display: none;
}
#id_to_groups + span {
display: none;
}

View file

@ -1,319 +1,311 @@
$(document).ready(function () {
function setupAttachmentWidget() {
var button = $(this);
var config = {};
var count = 0;
var readConfig = function() {
var buttonFormGroup = button.parents('.form-group');
var disabledLabel = buttonFormGroup.find('.attachDisabledLabel');
var attachmentWidget = {
button : null,
config : {},
count : 0,
if (disabledLabel.length) {
config.disabledLabel = disabledLabel.html();
var required = [];
buttonFormGroup.find('.attachRequiredField').each(function(index, field) {
required.push('#' + $(field).text());
});
config.basefields = $(required.join(","));
}
readConfig : function() {
var buttonFormGroup = attachmentWidget.button.parents('.form-group');
var disabledLabel = buttonFormGroup.find('.attachDisabledLabel');
config.showOn = $('#' + buttonFormGroup.find('.showAttachsOn').html());
config.showOnDisplay = config.showOn.find('.attachedFiles');
count = config.showOnDisplay.find('.initialAttach').length;
config.showOnEmpty = config.showOn.find('.showAttachmentsEmpty').html();
config.enabledLabel = buttonFormGroup.find('.attachEnabledLabel').html();
};
var setState = function() {
var enabled = true;
config.fields.each(function() {
if (!$(this).val()) {
enabled = false;
return;
}
if (disabledLabel.length) {
attachmentWidget.config.disabledLabel = disabledLabel.html();
var required = [];
buttonFormGroup.find('.attachRequiredField').each(function(index, field) {
required.push('#' + $(field).text());
});
if (enabled) {
button.removeAttr('disabled').removeClass('disabledAddAttachment');
button.val(config.enabledLabel);
attachmentWidget.config.basefields = $(required.join(","));
}
attachmentWidget.config.showOn = $('#' + buttonFormGroup.find('.showAttachsOn').html());
attachmentWidget.config.showOnDisplay = attachmentWidget.config.showOn.find('.attachedFiles');
attachmentWidget.count = attachmentWidget.config.showOnDisplay.find('.initialAttach').length;
attachmentWidget.config.showOnEmpty = attachmentWidget.config.showOn.find('.showAttachmentsEmpty').html();
attachmentWidget.config.enabledLabel = buttonFormGroup.find('.attachEnabledLabel').html();
},
setState : function() {
var enabled = true;
attachmentWidget.config.fields.each(function() {
if (!$(this).val()) {
enabled = false;
return;
}
});
if (enabled) {
attachmentWidget.button.removeAttr('disabled').removeClass('disabledAddAttachment');
attachmentWidget.button.val(attachmentWidget.config.enabledLabel);
} else {
attachmentWidget.button.attr('disabled', 'disabled').addClass('disabledAddAttachment');
attachmentWidget.button.val(attachmentWidget.config.disabledLabel);
}
},
cloneFields : function() {
var html = '<div class="attachedFileInfo">';
if (attachmentWidget.count) {
html = attachmentWidget.config.showOnDisplay.html() + html;
}
attachmentWidget.config.fields.each(function() {
var field = $(this);
var container= $(this).parents('.form-group');
if (container.find(':file').length) {
html += ' (' + field.val() + ')';
} else {
button.attr('disabled', 'disabled').addClass('disabledAddAttachment');
button.val(config.disabledLabel);
html += ' ' + field.val();
}
};
html += '<span style="display: none;" class="removeField">';
html += container.attr('id');
html += '</span>';
container.hide();
});
//html += ' <a href="" class="removeAttach glyphicon glyphicon-remove text-danger"></a>';
html += ' <a href="" class="removeAttach btn btn-default btn-xs">Delete</a>';
html += '</div>';
attachmentWidget.config.showOnDisplay.html(html);
attachmentWidget.count += 1;
attachmentWidget.initFileInput();
},
var cloneFields = function() {
var html = '<div class="attachedFileInfo">';
if (count) {
html = config.showOnDisplay.html() + html;
doAttach : function() {
attachmentWidget.cloneFields();
attachmentWidget.setState();
},
removeAttachment : function() {
var link = $(this);
var attach = $(this).parent('.attachedFileInfo');
var fields = attach.find('.removeField');
fields.each(function() {
$('#' + $(this).html()).remove();
});
attach.remove();
if (!attachmentWidget.config.showOnDisplay.html()) {
attachmentWidget.config.showOnDisplay.html(attachmentWidget.config.showOnEmpty);
attachmentWidget.count = 0;
}
return false;
},
initTriggers : function() {
attachmentWidget.config.showOnDisplay.on('click', 'a.removeAttach', attachmentWidget.removeAttachment);
attachmentWidget.button.click(attachmentWidget.doAttach);
},
initFileInput : function() {
var fieldids = [];
attachmentWidget.config.basefields.each(function(i) {
var field = $(this);
var oldcontainer= $(this).parents('.form-group');
var newcontainer= oldcontainer.clone();
var newfield = newcontainer.find('#' + field.attr('id'));
newfield.attr('name', newfield.attr('name') + '_' + attachmentWidget.count);
newfield.attr('id', newfield.attr('id') + '_' + attachmentWidget.count);
newcontainer.attr('id', 'container_id_' + newfield.attr('name'));
oldcontainer.after(newcontainer);
oldcontainer.hide();
newcontainer.show();
fieldids.push('#' + newfield.attr('id'));
});
attachmentWidget.config.fields = $(fieldids.join(","));
attachmentWidget.config.fields.change(attachmentWidget.setState);
attachmentWidget.config.fields.keyup(attachmentWidget.setState);
},
initWidget : function() {
attachmentWidget.button = $(this);
attachmentWidget.readConfig();
attachmentWidget.initFileInput();
attachmentWidget.initTriggers();
attachmentWidget.setState();
},
}
var liaisonForm = {
initVariables : function() {
liaisonForm.from_groups = liaisonForm.form.find('#id_from_groups');
liaisonForm.from_contact = liaisonForm.form.find('#id_from_contact');
liaisonForm.response_contacts = liaisonForm.form.find('#id_response_contacts');
liaisonForm.to_groups = liaisonForm.form.find('#id_to_groups');
liaisonForm.to_contacts = liaisonForm.form.find('#id_to_contacts');
liaisonForm.cc = liaisonForm.form.find('#id_cc_contacts');
liaisonForm.purpose = liaisonForm.form.find('#id_purpose');
liaisonForm.deadline = liaisonForm.form.find('#id_deadline');
liaisonForm.submission_date = liaisonForm.form.find('#id_submitted_date');
liaisonForm.approval = liaisonForm.form.find('#id_approved');
liaisonForm.initial_approval_label = liaisonForm.form.find("label[for='id_approved']").text();
liaisonForm.cancel = liaisonForm.form.find('#id_cancel');
liaisonForm.cancel_dialog = liaisonForm.form.find('#cancel-dialog');
liaisonForm.config = {};
liaisonForm.related_trigger = liaisonForm.form.find('.id_related_to');
liaisonForm.related_url = liaisonForm.form.find('#id_related_to').parent().find('.listURL').text();
liaisonForm.related_dialog = liaisonForm.form.find('#related-dialog');
liaisonForm.unrelate_trigger = liaisonForm.form.find('.id_no_related_to');
},
render_mails_into : function(container, person_list, as_html) {
var html='';
$.each(person_list, function(index, person) {
if (as_html) {
html += person[0] + ' &lt;<a href="mailto:'+person[1]+'">'+person[1]+'</a>&gt;<br />';
} else {
//html += person[0] + ' &lt;'+person[1]+'&gt;\n';
html += person + '\n';
}
config.fields.each(function() {
var field = $(this);
var container= $(this).parents('.form-group');
if (container.find(':file').length) {
html += ' (' + field.val() + ')';
} else {
html += ' ' + field.val();
});
container.html(html);
},
toggleApproval : function(needed) {
if (!liaisonForm.approval.length) {
return;
}
if (!needed) {
liaisonForm.approval.prop('checked',true);
liaisonForm.approval.hide();
//$("label[for='id_approved']").text("Approval not required");
var nodes = $("label[for='id_approved']:not(.control-label)")[0].childNodes;
nodes[nodes.length-1].nodeValue= 'Approval not required';
return;
}
if ( needed && !$('#id_approved').is(':visible') ) {
liaisonForm.approval.prop('checked',false);
liaisonForm.approval.show();
//$("label[for='id_approved']").text(initial_approval_label);
var nodes = $("label[for='id_approved']:not(.control-label)")[0].childNodes;
nodes[nodes.length-1].nodeValue=initial_approval_label;
return;
}
},
checkPostOnly : function(post_only) {
if (post_only) {
$("button[name=send]").hide();
} else {
$("button[name=send]").show();
}
},
updateInfo : function(first_time, sender) {
var from_ids = liaisonForm.from_groups.val();
var to_ids = liaisonForm.to_groups.val();
var url = liaisonForm.form.data("ajaxInfoUrl");
$.ajax({
url: url,
type: 'GET',
cache: false,
async: true,
dataType: 'json',
data: {from_groups: from_ids,
to_groups: to_ids},
success: function(response){
if (!response.error) {
if (!first_time || !liaisonForm.cc.text()) {
liaisonForm.render_mails_into(liaisonForm.cc, response.cc, false);
}
//render_mails_into(poc, response.poc, false);
if ( sender.attr('id') == 'id_to_groups' ) {
liaisonForm.to_contacts.val(response.to_contacts);
}
if ( sender.attr('id') == 'id_from_groups' ) {
liaisonForm.toggleApproval(response.needs_approval);
liaisonForm.response_contacts.val(response.response_contacts);
}
liaisonForm.checkPostOnly(response.post_only);
}
html += '<span style="display: none;" class="removeField">';
html += container.attr('id');
html += '</span>';
container.hide();
});
html += ' <a href="" class="removeAttach glyphicon glyphicon-remove text-danger"></a>';
html += '</div>';
config.showOnDisplay.html(html);
count += 1;
initFileInput();
};
var doAttach = function() {
cloneFields();
setState();
};
var removeAttachment = function() {
var link = $(this);
var attach = $(this).parent('.attachedFileInfo');
var fields = attach.find('.removeField');
fields.each(function() {
$('#' + $(this).html()).remove();
});
attach.remove();
if (!config.showOnDisplay.html()) {
config.showOnDisplay.html(config.showOnEmpty);
count = 0;
}
});
return false;
},
updatePurpose : function() {
var deadlinecontainer = liaisonForm.deadline.closest('.form-group');
var value = liaisonForm.purpose.val();
if (value == 'action' || value == 'comment') {
liaisonForm.deadline.prop('required',true);
deadlinecontainer.show();
} else {
liaisonForm.deadline.prop('required',false);
deadlinecontainer.hide();
liaisonForm.deadline.val('');
}
},
cancelForm : function() {
liaisonForm.cancel_dialog.dialog("open");
},
checkSubmissionDate : function() {
var date_str = liaisonForm.submission_date.val();
if (date_str) {
var sdate = new Date(date_str);
var today = new Date();
if (Math.abs(today-sdate) > 2592000000) { // 2592000000 = 30 days in milliseconds
return confirm('Submission date ' + date_str + ' differ more than 30 days.\n\nDo you want to continue and post this liaison using that submission date?\n');
}
return true;
}
else
return false;
};
},
var initTriggers = function() {
config.showOnDisplay.on('click', 'a.removeAttach', removeAttachment);
button.click(doAttach);
};
init : function() {
liaisonForm.form = $(this);
liaisonForm.initVariables();
$('#id_from_groups').select2();
$('#id_to_groups').select2();
liaisonForm.to_groups.change(function() { liaisonForm.updateInfo(false,$(this)); });
liaisonForm.from_groups.change(function() { liaisonForm.updateInfo(false,$(this)); });
liaisonForm.purpose.change(liaisonForm.updatePurpose);
liaisonForm.form.submit(liaisonForm.checkSubmissionDate);
$('.addAttachmentWidget').each(attachmentWidget.initWidget);
liaisonForm.updatePurpose();
if($('#id_to_groups').val()) {
$('#id_to_groups').trigger('change');
}
if($('#id_from_groups').val()) {
$('#id_from_groups').trigger('change');
}
},
}
var initFileInput = function() {
var fieldids = [];
config.basefields.each(function(i) {
var field = $(this);
var oldcontainer= $(this).parents('.form-group');
var newcontainer= oldcontainer.clone();
var newfield = newcontainer.find('#' + field.attr('id'));
newfield.attr('name', newfield.attr('name') + '_' + count);
newfield.attr('id', newfield.attr('id') + '_' + count);
newcontainer.attr('id', 'container_id_' + newfield.attr('name'));
oldcontainer.after(newcontainer);
oldcontainer.hide();
newcontainer.show();
fieldids.push('#' + newfield.attr('id'));
var searchForm = {
// search form, based on doc search feature
init : function() {
searchForm.form = $(this);
searchForm.form.find(".search_field input,select").change(searchForm.toggleSubmit).click(searchForm.toggleSubmit).keyup(searchForm.toggleSubmit);
},
anyAdvancedActive : function() {
var advanced = false;
var by = searchForm.form.find("input[name=by]:checked");
if (by.length > 0) {
by.closest(".search_field").find("input,select").not("input[name=by]").each(function () {
if ($.trim(this.value)) {
advanced = true;
}
});
config.fields = $(fieldids.join(","));
config.fields.change(setState);
config.fields.keyup(setState);
};
var initWidget = function() {
readConfig();
initFileInput();
initTriggers();
setState();
};
initWidget();
}
return advanced;
},
toggleSubmit : function() {
var textSearch = $.trim($("#id_text").val());
searchForm.form.find("button[type=submit]").get(0).disabled = !textSearch && !searchForm.anyAdvancedActive();
}
$('form.liaisons').each(function() {
var form = $(this);
var organization = form.find('#id_organization');
var from = form.find('#id_from_field');
var poc = form.find('#id_to_poc');
var cc = form.find('#id_cc1');
var reply = form.find('#id_replyto');
var purpose = form.find('#id_purpose');
var other_purpose = form.find('#id_purpose_text');
var deadline = form.find('#id_deadline_date');
var submission_date = form.find('#id_submitted_date');
var other_organization = form.find('#id_other_organization');
var approval = form.find('#id_approved');
var render_mails_into = function(container, person_list, as_html) {
var html='';
$.each(person_list, function(index, person) {
if (as_html) {
html += person[0] + ' &lt;<a href="mailto:'+person[1]+'">'+person[1]+'</a>&gt;<br />';
} else {
html += person[0] + ' &lt;'+person[1]+'&gt;\n';
}
});
container.html(html);
};
var toggleApproval = function(needed) {
if (!approval.length) {
return;
}
if (needed) {
approval.removeAttr('disabled');
approval.removeAttr('checked');
} else {
approval.attr('checked','checked');
approval.attr('disabled','disabled');
}
};
var checkPostOnly = function(post_only) {
if (post_only) {
$("input[name=send]").hide();
} else {
$("input[name=send]").show();
}
};
var updateReplyTo = function() {
var select = form.find('select[name=from_fake_user]');
var option = select.find('option:selected');
reply.val(option.attr('title'));
updateFrom();
};
var userSelect = function(user_list) {
if (!user_list || !user_list.length) {
return;
}
var link = form.find('a.from_mailto');
var select = form.find('select[name=from_fake_user]');
var options = '';
link.hide();
$.each(user_list, function(index, person) {
options += '<option value="' + person[0] + '" title="' + person[1][1] + '">'+ person[1][0] + ' &lt;' + person[1][1] + '&gt;</option>';
});
select.remove();
link.after('<select name="from_fake_user" class="form-control" style="margin-top: 0.5em;">' + options +'</select>');
form.find('select[name=from_fake_user]').change(updateReplyTo);
updateReplyTo();
};
var updateInfo = function(first_time, sender) {
var entity = organization;
var to_entity = from;
if (!entity.is('select') || !to_entity.is('select')) {
return false;
}
var url = form.data("ajaxInfoUrl");
$.ajax({
url: url,
type: 'GET',
cache: false,
async: true,
dataType: 'json',
data: {to_entity_id: organization.val(),
from_entity_id: to_entity.val()},
success: function(response){
if (!response.error) {
if (!first_time || !cc.text()) {
render_mails_into(cc, response.cc, false);
}
render_mails_into(poc, response.poc, true);
toggleApproval(response.needs_approval);
checkPostOnly(response.post_only);
if (sender == 'from') {
userSelect(response.full_list);
}
}
}
});
return false;
};
var updateFrom = function() {
var reply_to = reply.val();
form.find('a.from_mailto').attr('href', 'mailto:' + reply_to);
};
var updatePurpose = function() {
var deadlinecontainer = deadline.closest('.form-group');
var othercontainer = other_purpose.closest('.form-group');
var selected_id = purpose.val();
if (selected_id == '1' || selected_id == '2') {
deadline.prop('required',true);
deadlinecontainer.show();
} else {
deadline.prop('required',false);
deadlinecontainer.hide();
deadline.val('');
}
};
var checkOtherSDO = function() {
var entity = organization.val();
if (entity=='othersdo') {
other_organization.closest('.form-group').show();
other_organization.prop("required", true);
} else {
other_organization.closest('.form-group').hide();
other_organization.prop("required", false);
}
};
var checkFrom = function(first_time) {
var reduce_options = form.find('.reducedToOptions');
if (!reduce_options.length) {
updateInfo(first_time, 'from');
return;
}
var to_select = organization;
var from_entity = from.val();
if (!reduce_options.find('.full_power_on_' + from_entity).length) {
to_select.find('optgroup').eq(1).hide();
to_select.find('option').each(function() {
if (!reduce_options.find('.reduced_to_set_' + $(this).val()).length) {
$(this).hide();
} else {
$(this).show();
}
});
if (!to_select.find('option:selected').is(':visible')) {
to_select.find('option:selected').removeAttr('selected');
}
} else {
to_select.find('optgroup').show();
to_select.find('option').show();
}
updateInfo(first_time, 'from');
};
var checkSubmissionDate = function() {
var date_str = submission_date.val();
if (date_str) {
var sdate = new Date(date_str);
var today = new Date();
if (Math.abs(today-sdate) > 2592000000) { // 2592000000 = 30 days in milliseconds
return confirm('Submission date ' + date_str + ' differ more than 30 days.\n\nDo you want to continue and post this liaison using that submission date?\n');
}
return true;
}
else
return false;
};
}
// init form
organization.change(function() { updateInfo(false, 'to'); });
organization.change(checkOtherSDO);
from.change(function() { checkFrom(false); });
reply.keyup(updateFrom);
purpose.change(updatePurpose);
form.submit(checkSubmissionDate);
updateFrom();
checkFrom(true);
updatePurpose();
checkOtherSDO();
form.find('.addAttachmentWidget').each(setupAttachmentWidget);
});
$(document).ready(function () {
// use traditional style URL parameters
$.ajaxSetup({ traditional: true });
$('form.liaisons-form').each(liaisonForm.init);
$('#search_form').each(searchForm.init);
});

View file

@ -1,13 +0,0 @@
{% extends "liaisons/detail.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% block content %}
{% origin %}
{{ block.super }}
<form method="post">
{% csrf_token %}
<input class="btn btn-primary" type="submit" value="Approve" name='do_approval' />
</form>
{% endblock %}

View file

@ -1,15 +0,0 @@
{% extends "liaisons/overview.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% block title %}Pending liaison statements{% endblock %}
{% block content-title %}
<h1>Pending liaison statements</h1>
{% endblock %}
{% block management-links %}
<p>
<a class="btn btn-default" href="{% url "liaison_list" %}">Return to liaison list</a>
</p>
{% endblock %}

View file

@ -13,65 +13,68 @@
Liaison statement<br><small>{% include 'liaisons/liaison_title.html' %}</small>
</h1>
<table class="table table-condensed table-striped">
<tr>
<th class="text-nowrap">Submission date</th>
<td>{{ liaison.submitted|date:"Y-m-d" }}</td>
</tr>
<tr>
<th class="text-nowrap">From</th>
<td>
{{ liaison.from_name }}
{% if liaison.from_contact %}(<a href="mailto:{{ liaison.from_contact.address }}">{{ liaison.from_contact.person }}</a>)
{% endif %}
</td>
</tr>
<tr>
<th class="text-nowrap">To</th>
<td>
{% if liaison.from_contact %}
{{ liaison.to_name }} ({{ liaison.to_contact|parse_email_list }})
{% else %}
{{ liaison.to_name|urlize }}
{% endif %}
</td>
</tr>
{% include "liaisons/detail_tabs.html" %}
<table class="table table-condensed table-striped">
<tr>
<th>State</th>
<td>{{ liaison.state }}</td>
</tr>
<tr>
<th class="text-nowrap">Submission Date</th>
<td>{{ liaison.submitted|date:"Y-m-d" }}</td></tr>
{% if liaison.from_contact %}
<tr>
<th class="text-nowrap">Cc</th>
<td>
{{ liaison.cc|parse_email_list }}
</td>
</tr>
{% endif %}
{% if liaison.from_contact %}
<tr>
<th class="text-nowrap">Sender</th>
<td>
<a href="mailto:{{ liaison.from_contact.address }}">{{ liaison.from_contact.person }}</a>
</td>
</tr>
{% endif %}
{% if liaison.response_contact %}
<tr>
<th class="text-nowrap">Response contact</th>
<td>
{{ liaison.response_contact|parse_email_list }}
</td>
</tr>
{% endif %}
<tr>
<th class="text-nowrap">From</th>
<td>{{ liaison.from_groups_display }}</td>
</tr>
{% if liaison.technical_contact %}
<tr>
<th class="text-nowrap">Technical contact</th>
<td>
{{ liaison.technical_contact|parse_email_list }}
</td>
</tr>
{% endif %}
<tr>
<th class="text-nowrap">To</th>
<td>{{ liaison.to_groups_display }}</td>
</tr>
{% if liaison.purpose %}
<tr>
<th class="text-nowrap">Purpose</th>
<td>{{ liaison.purpose.name }}</td>
</tr>
{% endif %}
{% if liaison.cc_contacts %}
<tr>
<th class="text-nowrap">Cc</th><td>{{ liaison.cc_contacts|parse_email_list|make_one_per_line|safe|linebreaksbr }}</td>
</tr>
{% endif %}
{% if liaison.response_contacts %}
<tr>
<th class="text-nowrap">Response Contact</th>
<td>{{ liaison.response_contacts|parse_email_list|make_one_per_line|safe|linebreaksbr }}</td>
</tr>
{% endif %}
{% if liaison.technical_contacts %}
<tr>
<th class="text-nowrap">Technical Contact</th>
<td>{{ liaison.technical_contacts|parse_email_list|make_one_per_line|safe|linebreaksbr }}</td>
</tr>
{% endif %}
{% if liaison.action_holder_contacts %}
<tr>
<th class="text-nowrap">Action Holder Contacts</th>
<td>{{ liaison.action_holder_contacts|parse_email_list|make_one_per_line|safe|linebreaksbr }}</td>
</tr>
{% endif %}
<tr>
<th class="text-nowrap">Purpose</th>
<td>{{ liaison.purpose.name }}</td>
</tr>
{% if liaison.deadline %}
<tr>
<th class="text-nowrap">Deadline</th>
@ -97,7 +100,7 @@
<th class="text-nowrap">Liaisons referring to this</th>
<td>
{% for rel in relations %}
<a href="{% url "liaison_detail" rel.pk %}">
<a href="{% url "ietf.liaisons.views.liaison_detail" rel.pk %}">
{% if rel.title %}{{ rel.title }}{% else %}Liaison #{{ rel.pk }}{% endif %}
</a>
<br>
@ -111,7 +114,7 @@
<tr>
<th class="text-nowrap">Referenced liaison</th>
<td>
<a href="{% url "liaison_detail" liaison.related_to.pk %}">
<a href="{% url "ietf.liaisons.views.liaison_detail" liaison.related_to.pk %}">
{% if liaison.related_to.title %}{{ liaison.related_to.title }}{% else %}Liaison #{{ liaison.related_to.pk }}{% endif %}
</a>
</td>
@ -119,11 +122,18 @@
{% endif %}
{% endif %}
{% if liaison.other_identifiers %}
<tr>
<th class="text-nowrap">Other Identifiers</th>
<td>{{ liaison.other_identifiers }}</td>
</tr>
{% endif %}
<tr>
<th class="text-nowrap">Attachments</th>
<td>
{% for doc in liaison.attachments.all %}
<a href="https://datatracker.ietf.org/documents/LIAISON/{{ doc.external_url }}">{{ doc.title }}</a>
{% for doc in liaison.active_attachments.all %}
<a href="{{ doc.href }}">{{ doc.title }}</a>
{% if not forloop.last %}<br>{% endif %}
{% empty %}
(None)
@ -131,6 +141,28 @@
</td>
</tr>
{% if relations_by %}
<tr>
<th class="text-nowrap">Liaisons referred by this one</th>
<td>
{% for rel in relations_by %}
<a href="{% url "ietf.liaisons.views.liaison_detail" rel.pk %}">{% if rel.title %}{{ rel.title }}{% else %}Liaison #{{ rel.pk }}{% endif %}</a><br />
{% endfor %}
</td>
</tr>
{% endif %}
{% if relations_to %}
<tr>
<th class="text-nowrap">Liaisons referring to this one</th>
<td>
{% for rel in relations_to %}
<a href="{% url "ietf.liaisons.views.liaison_detail" rel.pk %}">{% if rel.title %}{{ rel.title }}{% else %}Liaison #{{ rel.pk }}{% endif %}</a><br />
{% endfor %}
</td>
</tr>
{% endif %}
{% if liaison.from_contact and liaison.body %}
<tr>
<th class="text-nowrap">Body</th>
@ -139,12 +171,29 @@
</td>
</tr>
{% endif %}
</table>
</table>
{% if can_edit %}
<p>
<a class="btn btn-default" href="{% url "liaison_edit" object_id=liaison.pk %}">Edit liaison</a>
</p>
<p>
<form method="post">
{% csrf_token %}
{% if liaison.state.slug != 'dead' and can_edit %}
<a class="btn btn-default" href="{% url "ietf.liaisons.views.liaison_edit" object_id=liaison.pk %}">Edit liaison</a>
{% endif %}
{% if liaison.state.slug != 'dead' and can_reply %}
<a class="btn btn-default" href="{% url "ietf.liaisons.views.liaison_reply" object_id=liaison.pk %}">Reply to liaison</a>
{% endif %}
{% if liaison.state.slug == 'pending' and can_edit %}
<input class="btn btn-default" type="submit" value="Approve" name='approved' />
<input class="btn btn-default" type="submit" value="Mark as Dead" name='dead' />
{% endif %}
{% if liaison.state.slug == 'posted' and user|has_role:"Secretariat" %}
<a class="btn btn-default" href="{% url "ietf.liaisons.views.liaison_resend" object_id=liaison.pk %}">Resend statement</a>
{% endif %}
{% if liaison.state.slug == 'dead' and can_edit %}
<input class="btn btn-default" type="submit" value="Resurrect" name='resurrect' />
{% endif %}
</form>
</p>
{% endblock %}

View file

@ -0,0 +1,47 @@
{% extends "base.html" %}
{% load future %}
{% load ietf_filters %}
{% block title %}History for Liaison Statement - {{ liaison.title }}{% endblock %}
{% block content %}
<h1>History for Liaison Statement<br><small>{{ liaison.title }}</small></h1>
{% include "liaisons/detail_tabs.html" %}
{% comment %}
{% if user|has_role:"Area Director,Secretariat,IANA,RFC Editor" %}
<p class="buttonlist">
<a class="btn btn-default" href="{% url "ipr_add_comment" id=ipr.id %}" title="Add comment to history">Add comment</a>
<a class="btn btn-default" href="{% url "ipr_add_email" id=ipr.id %}" title="Add email to history">Add email</a>
</p>
{% endif %}
{% endcomment %}
<table class="table table-condensed table-striped history">
<thead>
<tr>
<th>Date</th>
<th>Type</th>
<th>By</th>
<th>Text</th>
</tr>
</thead>
<tbody>
{% for e in events %}
<tr>
<td class="text-nowrap">{{ e.time|date:"Y-m-d" }}</td>
<td>{{ e.type }}
{% if e.response_due and e.response_past_due %}
<span class="glyphicon glyphicon-exclamation-sign" title="Response overdue"></span>
{% endif %}
</td>
<td>{{ e.by }}</td>
<td>{{ e.desc|format_history_text }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock content %}

View file

@ -0,0 +1,5 @@
<ul class="nav nav-tabs" role="tablist">
{% for name, link, selected in tabs %}
<li {% if selected %}class="active"{% endif %}><a href="{{ link }}">{{ name }}</a></li>
{% endfor %}
</ul>

View file

@ -2,7 +2,8 @@
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load staticfiles %}
{% load bootstrap3 %}
{% load ietf_filters %}
{% load bootstrap3 widget_tweaks %}
{% block title %}{% if liaison %}Edit liaison: {{ liaison }}{% else %}Send Liaison Statement{% endif %}{% endblock %}
@ -10,6 +11,7 @@
<link rel="stylesheet" href="{% static 'bootstrap-datepicker/css/datepicker3.css' %}">
<link rel="stylesheet" href="{% static 'select2/select2.css' %}">
<link rel="stylesheet" href="{% static 'select2-bootstrap-css/select2-bootstrap.min.css' %}">
<link rel="stylesheet" href="{% static 'ietf/css/liaisons.css' %}">
{% endblock %}
{% block morecss %}
@ -41,18 +43,22 @@
<p class="help-block">Fields marked with <label class="required"></label> are required. For detailed descriptions of the fields see the <a href="{% url "liaisons_field_help" %}">field help</a>.</p>
{% endif %}
<form class="liaisons form-horizontal show-required" method="post" enctype="multipart/form-data" data-ajax-info-url="{% url "ietf.liaisons.views.ajax_get_liaison_info" %}">{% csrf_token %}
<form role="form" class="liaisons-form form-horizontal show-required" method="post" enctype="multipart/form-data" data-ajax-info-url="{% url "ietf.liaisons.views.ajax_get_liaison_info" %}">{% csrf_token %}
{% for fieldset in form.get_fieldsets %}
<h2>{{ fieldset.name }}</h2>
{% for field in fieldset.fields %}
{% for fieldset in form.fieldsets %}
{% if forloop.first and user|has_role:"Secretariat" %}
<h2><div class="col-md-2">{{ fieldset.name }}</div><div class="col-md-10"><a class="small" target="_blank" href="{% url "groups_add" %}">Add new group &gt&gt</a></div></h2>
{% else %}
<h2>{{ fieldset.name }}</h2>
{% endif %}
{% for field in fieldset %}
{% bootstrap_field field layout="horizontal" %}
{% endfor %}
{% endfor %}
{% buttons %}
<a class="btn btn-danger pull-right" href="{% url "liaison_list" %}">Cancel</a>
<a class="btn btn-danger pull-right" href="{% if liaison %}{% url "ietf.liaisons.views.liaison_detail" object_id=liaison.pk %}{% else %}{% url "ietf.liaisons.views.liaison_list" %}{% endif %}">Cancel</a>
{% if not liaison %}
<button name="send" type="submit" class="btn btn-primary">Send and post</button>

View file

@ -0,0 +1,44 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load bootstrap3 widget_tweaks %}
{% load staticfiles %}
{% block title %}Edit liaison attachment{% endblock %}
{% block pagehead %}
<link rel="stylesheet" href="{% static 'ietf/css/liaisons.css' %}">
{% endblock %}
{% block morecss %}
.widget { height: auto; min-height: 34px; }
{% endblock %}
{% block content %}
{% origin %}
<h1>Edit liaison attachment</h1>
{% bootstrap_messages %}
{% if form.errors %}
<div class="alert alert-danger">
<p>There were errors in the submitted form -- see below. Please correct these and resubmit.</p>
</div>
{% endif %}
{% bootstrap_form_errors form %}
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<a class="btn btn-danger pull-right" href="{% url "ietf.liaisons.views.liaison_edit" object_id=liaison.pk %}">Cancel</a>
<button name="save" type="submit" class="btn btn-primary">Save</button>
{% endbuttons %}
</form>
{% endblock %}

View file

@ -5,7 +5,7 @@
{% if attachments %}
<ul>
{% for doc in attachments %}
<li><a href="https://datatracker.ietf.org/documents/LIAISON/{{ doc.external_url }}">{{ doc.title }}</a><br>
<li><a href="{{ doc.href }}">{{ doc.title }}</a><br>
{% endfor %}
</ul>
{% else %}

View file

@ -20,12 +20,12 @@
<ul>
<li>
<a href="/liaison/help/to_ietf/">
Liaison statements to the IETF: guidelines for completing the "To:" and "Cc:" fields
Liaison statements to the IETF: guidelines for completing the "To:" and "Cc:" fields
</a>
</li>
<li>
<a href="/liaison/help/from_ietf/">
Liaison statements from the IETF: guidelines for completing the "Cc:" field
Liaison statements from the IETF: guidelines for completing the "Cc:" field
</a>
</li>
<li>
@ -47,65 +47,65 @@
<table class="table table-condensed table-striped">
<thead>
<tr>
<th>Field</th>
<th>Sub-field</th>
<th>Description</th>
<th>Comments</th>
<th>Fieldset</th>
<th>Field</th>
<th>Description</th>
<th>Comments</th>
</tr>
</thead>
<tr>
<th>From</th>
<th>Organization</th>
<td>The organization submitting the liaison statement.</td>
<td>The field is filled in automatically.</td>
<th>Groups</th>
<td>The organization(s) submitting the liaison statement.</td>
<td>Use arrows to select or type name to search</td>
</tr>
<tr>
<td></td>
<th>Submitter</th>
<th>From Contact</th>
<td>
The name and e-mail address of the person submitting the liaison statement.
The e-mail address of the person submitting the liaison statement.
</td>
<td>The field is filled in automatically.</td>
</tr>
<tr>
<td></td>
<th>Reply to</th>
<th>Response Contacts</th>
<td>
The e-mail address(es) that will be inserted in the "To" field when a recipient hits the "reply" button on the message.
The e-mail address(es) to which any response should be sent, separated by commas.
</td>
<td>Mandatory format: Name &lt;e-mail address&gt</td>
</tr
<tr>
<th>To</th>
<th>Organization</th>
<th>Groups</th>
<td>
The name of the organization (and sub-group, if applicable) to which the liaison statement is being sent.
The organization(s) (and sub-group, if applicable) to which the liaison statement is being sent.
</td>
<td>
Drop-down menu with available choices. If an SDO is not listed, please contact statements@ietf.org
Drop-down menu with available choices. If an SDO is not listed, please contact statements@ietf.org
</td>
</tr>
<tr>
<td></td>
<th>POC(s)</th>
<th>Contacts</th>
<td>
The e-mail address(es) of the recipient(s) of the liaison statement, separated by commas.
The e-mail address(es) of the recipient(s) of the liaison statement, separated by commas.
</td>
<td>The field is filled in automatically.</td>
<td>The field may be filled in automatically.</td>
</tr>
<tr>
<th>Other email addresses</th>
<th>Response contact</th>
<th>Technical contact</th>
<td>
The e-mail address(es) to which any response should be sent, separated by commas.
The e-mail address(es) of individuals or lists that may be contacted for clarification of the liaison statement, separated by commas.
</td>
<td>Optional. Suggested format: Name &lt;e-mail address&gt</td>
</tr>
<tr>
<td></td>
<th>Technical contact</th>
<th>Action Holder Contacts</th>
<td>
The e-mail address(es) of individuals or lists that may be contacted for clarification of the liaison statement, separated by commas.
The e-mail address(es) of the persons responsible for acting on the statement.
</td>
<td>Optional. Suggested format: Name &lt;e-mail address&gt</td>
</tr>
@ -113,7 +113,7 @@
<td></td>
<th>Cc</th>
<td>
The e-mail address(es) of the copy recipient(s) of the liaison statement, one on each line.
The e-mail address(es) of the copy recipient(s) of the liaison statement, one on each line.
</td>
<td>Optional. Suggested format: Name &lt;e-mail address&gt</td>
</tr>
@ -121,10 +121,10 @@
<th>Purpose</th>
<th>Purpose</th>
<td>
The intent of the liaison statement. Normally, one of four choices: (a) For action, (b) For comment, (c) For information, (d) In Response.
The intent of the liaison statement. One of four choices: (a) For action, (b) For comment, (c) For information, (d) In Response.
</td>
<td>
The submitter selects one of the four choices, or selects "other" and indicates the intent.
The submitter selects one of the four choices.
</td>
</tr>
<tr>
@ -132,7 +132,7 @@
<th>Deadline</th>
<td>The date by which a comment or action is required.</td>
<td>
Mandatory if the purpose is "For comment" or "For action." Otherwise, optional.
Mandatory if the purpose is "For comment" or "For action."
</td>
</tr>
@ -153,7 +153,7 @@
<th>Body</th>
<td>The text of the liaison statement.</td>
<td>
Mandatory if there are no attachments. Optional if the text of the liaison statement is provided in an attachment.
Mandatory if there are no attachments. Optional if the text of the liaison statement is provided in an attachment.
</td>
</tr>
@ -162,7 +162,7 @@
<th>Title</th>
<td>The title of the attachment.</td>
<td>
Optional if there is text in the body, mandatory if there is not.
Optional if there is text in the body, mandatory if there is not.
</td>
</tr>
<tr>
@ -170,7 +170,7 @@
<th>File</th>
<td>Browse to find the attachment.</td>
<td>
Optional if there is text in the body, mandatory if there is not.
Optional if there is text in the body, mandatory if there is not.
</td>
</tr>
</table>

View file

@ -0,0 +1,55 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load ietf_filters %}
{% load staticfiles %}
{% block pagehead %}
<link rel="stylesheet" href="{% static 'bootstrap-datepicker/css/datepicker3.css' %}">
{% endblock %}
{% block title %}Liaison Statements - {{ selected_menu_entry|capfirst }}{% endblock %}
{% block content %}
{% origin %}
<h1>Liaison Statements</h1>
{% if with_search %}
<div class="ietf-box search-form-box">
{% include "liaisons/search_form.html" %}
</div>
{% endif %}
<ul class="nav nav-tabs" role="tablist">
{% for name, url in menu_entries %}
<li {% if selected_menu_entry == name.lower %}class="active"{% endif %}>
<a href="{{ url }}">{{ name }}</a>
</li>
{% endfor %}
</ul>
{% if menu_actions %}
<div class="buttonlist">
{% for name, url in menu_actions %}
<a class="btn btn-default" href="{{ url }}">{{ name }}</a>
{% endfor %}
</div>
{% endif %}
{% block group_content %}
{% if search_conducted and not liaisons %}
<div class="alert alert-info">No statements match your query.</div>
{% else %}
{% include "liaisons/liaison_table.html" %}
{% endif %}
{% endblock group_content %}
{% endblock content %}
{% block js %}
<script src="{% static 'bootstrap-datepicker/js/bootstrap-datepicker.js' %}"></script>
<script src="{% static 'ietf/js/liaisons.js' %}"></script>
{% endblock %}

View file

@ -1,19 +1,21 @@
{% load ietf_filters %}{% autoescape off %}Title: {{ liaison.title|clean_whitespace }}
Submission Date: {{ liaison.submitted|date:"Y-m-d" }}
URL of the IETF Web page: {{ url }}
URL of the IETF Web page: {{ liaison.get_absolute_url }}
{% if liaison.deadline %}Please reply by {{ liaison.deadline }}{% endif %}
From: {{ liaison.from_name }} ({{ liaison.from_contact.person }} <{{ liaison.reply_to|default:liaison.from_contact.address }}>)
To: {{ liaison.to_name }} ({{ liaison.to_contact }})
From: {{ liaison.from_name }} ({{ liaison.from_contact.person }} <{% if liaison.from_contact %}{{ liaison.from_contact.address }}{% endif %}>)
To: {{ liaison.to_name }} ({{ liaison.to_contacts }})
Cc: {{ liaison.cc }}
Response Contact: {{ liaison.response_contact }}
Technical Contact: {{ liaison.technical_contact }}
Response Contacts: {{ liaison.response_contacts }}
Technical Contacts: {{ liaison.technical_contacts }}
Purpose: {{ liaison.purpose.name }}
{% if liaison.related_to %}Referenced liaison: {% if liaison.related_to.title %}{{ liaison.related_to.title }}{% else %}Liaison #{{ liaison.related_to.pk }}{% endif %} ({{ referenced_url }}){% endif %}
{% for related in liaison.source_of_set.all %}
Referenced liaison: {% if related.target.title %}{{ related.target.title }}{% else %}Liaison #{{ related.target.pk }}{% endif %} ({{ related.target.get_absolute_url }})
{% endfor %}
Body: {{ liaison.body }}
Attachments:
{% for doc in liaison.attachments.all %}
{{ doc.title }}
https://datatracker.ietf.org/documents/LIAISON/{{ doc.external_url }}
{{ doc.href }}
{% empty %}
No document has been attached
{% endfor %}{% endautoescape %}

View file

@ -0,0 +1,46 @@
{# Copyright The IETF Trust 2007, All Rights Reserved #}
{% load ietf_filters %}
{% load future %}
<table class="table table-condensed table-striped">
<thead>
<tr>
<th>
<a href="?sort=date">Date {% if sort == "date" %}<span class="fa fa-caret-down"></span>{% endif %}</a>
</th>
<th>
<a href="?sort=from_groups">From {% if sort == "from_groups" %}<span class="fa fa-caret-down"></span>{% endif %}</a>
</th>
<th>
<a href="?sort=to_groups">To {% if sort == "to_groups" %}<span class="fa fa-caret-down"></span>{% endif %}</a>
</th>
<th>
<a href="?sort=deadline">Deadline {% if sort == "deadline" %}<span class="fa fa-caret-down"></span>{% endif %}</a>
</th>
<th>
<a href="?sort=title">Title {% if sort == "title" %}<span class="fa fa-caret-down"></span>{% endif %}</a>
</th>
</tr>
</thead>
<tbody>
{% for liaison in liaisons %}
<tr>
<td class="text-nowrap">{{ liaison.sort_date|date:"Y-m-d" }}</td>
<td>{{ liaison.from_groups_display }}</td>
<td>{{ liaison.to_groups_display }}</td>
<td class="text-nowrap">{{ liaison.deadline|default:"-"|date:"Y-m-d" }}{% if liaison.deadline and not liaison.action_taken %}<br><span class="label label-warning">Action Needed</span>{% endif %}</td>
<td>
{% if not liaison.from_contact_id %}
{% for doc in liaison.attachments.all %}
<a href="{{ doc.href }}">{{ doc.title }}</a>
<br>
{% endfor %}
{% else %}
<a href="{% url "ietf.liaisons.views.liaison_detail" object_id=liaison.pk %}">{{ liaison.title }}</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>

View file

@ -2,7 +2,7 @@
{% load ietf_filters %}
{% if not liaison.from_contact %}
Liaison statement submitted by email from {{ liaison.from_name }} to {{ liaison.to_name|strip_email }} on {{ liaison.submitted|date:"Y-m-d" }}
Liaison statement submitted by email from {{ liaison.from_groups.first.name }} to {{ liaison.to_groups.first.name }} on {{ liaison.submitted|date:"Y-m-d" }}
{% else %}
{{ liaison.title }}
{% endif %}

View file

@ -1,82 +0,0 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% block title %}Liaison statements{% endblock %}
{% block content %}
{% origin %}
{% block content-title %}
<h1>Liaison statements</h1>
{% endblock %}
{% block management-links %}
{% if can_manage %}
<p class="buttonlist">
{% if can_send_incoming %}
<a class="btn btn-default" href="{% url "add_liaison" %}?incoming">New incoming liaison</a>
{% endif %}
{% if can_send_outgoing %}
<a class="btn btn-default" href="{% url "add_liaison" %}">New outgoing liaison</a>
{% endif %}
{% if approvable %}
<a class="btn btn-default" href="{% url "liaison_approval_list" %}">Approve pending liaison statement{{ approvable|pluralize }} <span class="badge">{{ approvable }}</span></a>
{% endif %}
</p>
{% endif %}
{% endblock %}
{% load ietf_filters %}
<table class="table table-condensed table-striped">
<thead>
<tr>
<th>
<a href="?sort=submitted">Date {% if sort == "submitted" %}<span class="fa fa-caret-down"></span>{% endif %}</a>
</th>
<th>
<a href="?sort=from_name">From {% if sort == "from_name" %}<span class="fa fa-caret-down"></span>{% endif %}</a>
</th>
<th>
<a href="?sort=to_name">To {% if sort == "to_name" %}<span class="fa fa-caret-down"></span>{% endif %}</a>
</th>
<th>
<a href="?sort=deadline">Deadline {% if sort == "deadline" %}<span class="fa fa-caret-down"></span>{% endif %}</a>
</th>
<th>
<a href="?sort=title">Title {% if sort == "title" %}<span class="fa fa-caret-down"></span>{% endif %}</a>
</th>
</tr>
</thead>
<tbody>
{% for liaison in liaisons %}
<tr>
<td class="text-nowrap">{{ liaison.submitted|date:"Y-m-d" }}</td>
<td>{{ liaison.from_name }}</td>
<td>
{% if liaison.from_contact_id %}
{{ liaison.to_name }}
{% else %}
{{ liaison.to_name|strip_email }}
{% endif %}
</td>
<td class="text-nowrap">{{ liaison.deadline|default:"-"|date:"Y-m-d" }}</td>
<td>
{% if not liaison.from_contact_id %}
{% for doc in liaison.attachments.all %}
<a href="https://datatracker.ietf.org/documents/LIAISON/{{ doc.external_url }}">{{ doc.title }}</a>
<br>
{% endfor %}
{% else %}
<a href="{% if liaison.approved %}{% url "liaison_detail" object_id=liaison.pk %}{% else %}{% url "liaison_approval_detail" object_id=liaison.pk %}{% endif %}">{{ liaison.title }}</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View file

@ -2,4 +2,4 @@ The following liaison statement will remain pending (and not public available) i
{% include "liaisons/liaison_mail.txt" %}
Please visit {{ url }} in order to approve the liaison statement.
Please visit {{ liaison.get_absolute_url }} in order to approve the liaison statement.

View file

@ -8,8 +8,8 @@ As liaison manager of {{ sdo_name }} you have to provide an updated list of pers
Current list in our system for {{ sdo_name }} is:
------
{% for person in individuals %}
{{ person.email.0 }} <{{ person.email.1 }}>
{% for role in individuals %}
{{ role.person.plain_name }} <{{ role.email.address }}>
{% endfor %}
------

View file

@ -0,0 +1,87 @@
{# Copyright The IETF Trust 2015, All Rights Reserved #}{% load origin %}{% origin %}
{% load widget_tweaks %}
{% load ietf_filters %}
{% load bootstrap3 %}
<form id="search_form" class="form-horizontal" action="{% url "ietf.liaisons.views.liaison_list" state=state %}" method="get">
<div class="input-group search_field">
{{ form.text|add_class:"form-control"|attr:"placeholder:Title, body, identifiers, etc." }}
<span class="input-group-btn">
<button class="btn btn-primary" type="submit">
<span class="fa fa-search"></span>
Search
</button>
</span>
</div> <!-- search_field -->
{{ form.sort }} {# hidden field #}
<div class="panel-group" id="accordion1">
<div class="panel panel-default">
<div class="panel-heading">
<p class="panel-title toggle_advanced">
<a data-toggle="collapse" data-parent="#accordion1" href="#searchcollapse">
<span class="fa fa-caret-down"></span> Additional search criteria
</a>
</p>
</div>
<div id="searchcollapse" class="panel-collapse collapse visible-nojs">
<div class="panel-body">
<div class="form-group search_field">
<div class="col-sm-4">
<label for="id_source" class="control-label">Source</label>
</div>
<div class="col-sm-8">
{{ form.source|add_class:"form-control" }}
</div>
</div>
<div class="form-group search_field">
<div class="col-sm-4">
<label for="id_destination" class="control-label">Destination</label>
</div>
<div class="col-sm-8">
{{ form.destination|add_class:"form-control" }}
</div>
</div>
<div class="form-group search_field">
<div class="col-sm-4">
<label for="id_start_date" class="control-label">Start Date</label>
</div>
<div class="col-sm-8">
{{ form.start_date|add_class:"form-control" }}
</div>
</div>
<div class="form-group search_field">
<div class="col-sm-4">
<label for="id_end_date" class="control-label">End Date</label>
</div>
<div class="col-sm-8">
{{ form.end_date|add_class:"form-control" }}
</div>
</div>
<div class="form-group search_field">
<div class="col-md-offset-4 col-sm-4">
<button class="btn btn-default btn-block" type="reset">Clear</button>
</div>
<div class="col-sm-4">
<button class="btn btn-primary btn-block" type="submit">
<span class="fa fa-search"></span>
Search
</button>
</div>
</div>
</div>
</div>
</div>
</div> <!-- accordian1 -->
</form>