diff --git a/ietf/ietfauth/utils.py b/ietf/ietfauth/utils.py
index fc9647093..845162392 100644
--- a/ietf/ietfauth/utils.py
+++ b/ietf/ietfauth/utils.py
@@ -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()
diff --git a/ietf/liaisons/accounts.py b/ietf/liaisons/accounts.py
deleted file mode 100644
index aa28a2723..000000000
--- a/ietf/liaisons/accounts.py
+++ /dev/null
@@ -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
diff --git a/ietf/liaisons/admin.py b/ietf/liaisons/admin.py
index d0626aa54..acb09ebaf 100644
--- a/ietf/liaisons/admin.py
+++ b/ietf/liaisons/admin.py
@@ -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 ' '.join(['%s ' % (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)
diff --git a/ietf/liaisons/feeds.py b/ietf/liaisons/feeds.py
index 44649fba8..f9434d23b 100644
--- a/ietf/liaisons/feeds.py
+++ b/ietf/liaisons/feeds.py
@@ -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
diff --git a/ietf/liaisons/fields.py b/ietf/liaisons/fields.py
index 0671764c8..e83b1063b 100644
--- a/ietf/liaisons/fields.py
+++ b/ietf/liaisons/fields.py
@@ -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
diff --git a/ietf/liaisons/forms.py b/ietf/liaisons/forms.py
index 54f1d2ab3..f97a64c8a 100644
--- a/ietf/liaisons/forms.py
+++ b/ietf/liaisons/forms.py
@@ -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)
diff --git a/ietf/liaisons/mails.py b/ietf/liaisons/mails.py
index f2d9b9a89..956802c13 100644
--- a/ietf/liaisons/mails.py
+++ b/ietf/liaisons/mails.py
@@ -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
diff --git a/ietf/liaisons/management/commands/check_liaison_deadlines.py b/ietf/liaisons/management/commands/check_liaison_deadlines.py
index 1ba32a5e0..aebadb46f 100644
--- a/ietf/liaisons/management/commands/check_liaison_deadlines.py
+++ b/ietf/liaisons/management/commands/check_liaison_deadlines.py
@@ -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)
\ No newline at end of file
diff --git a/ietf/liaisons/management/commands/remind_update_sdo_list.py b/ietf/liaisons/management/commands/remind_update_sdo_list.py
index e730ac637..8e8971336 100644
--- a/ietf/liaisons/management/commands/remind_update_sdo_list.py
+++ b/ietf/liaisons/management/commands/remind_update_sdo_list.py
@@ -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
\ No newline at end of file
diff --git a/ietf/liaisons/migrations/0001_initial.py b/ietf/liaisons/migrations/0001_initial.py
index 83b81c408..61635a020 100644
--- a/ietf/liaisons/migrations/0001_initial.py
+++ b/ietf/liaisons/migrations/0001_initial.py
@@ -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,),
+ ),
+ ]
diff --git a/ietf/liaisons/migrations/0002_add_missing_titles.py b/ietf/liaisons/migrations/0002_add_missing_titles.py
deleted file mode 100644
index aaead2d7d..000000000
--- a/ietf/liaisons/migrations/0002_add_missing_titles.py
+++ /dev/null
@@ -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
diff --git a/ietf/liaisons/migrations/0002_schema_changes.py b/ietf/liaisons/migrations/0002_schema_changes.py
new file mode 100644
index 000000000..81f3e6c61
--- /dev/null
+++ b/ietf/liaisons/migrations/0002_schema_changes.py
@@ -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,
+ ),
+ ]
diff --git a/ietf/liaisons/migrations/0003_migrate_general.py b/ietf/liaisons/migrations/0003_migrate_general.py
new file mode 100644
index 000000000..4d0e4392b
--- /dev/null
+++ b/ietf/liaisons/migrations/0003_migrate_general.py
@@ -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),
+ ]
diff --git a/ietf/liaisons/migrations/0004_migrate_attachments.py b/ietf/liaisons/migrations/0004_migrate_attachments.py
new file mode 100644
index 000000000..9852ce23d
--- /dev/null
+++ b/ietf/liaisons/migrations/0004_migrate_attachments.py
@@ -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,
+ ),
+ ]
diff --git a/ietf/liaisons/migrations/0005_migrate_groups.py b/ietf/liaisons/migrations/0005_migrate_groups.py
new file mode 100644
index 000000000..f595ad164
--- /dev/null
+++ b/ietf/liaisons/migrations/0005_migrate_groups.py
@@ -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 ': [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 ': [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 ': [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 ': [u'itu-t-sg-15'],
+ u'ITU-T Study Group 15 Q4 ': [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'': ['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 ,Pat Thaler ',
+ 'ieee-802-1':'Paul Nikolich ,Glen Parsons ,John Messenger ',
+ 'ieee-802-11':'Dorothy Stanley , Adrian Stephens ',
+ 'cablelabs':'Greg White ',
+ 'iso-iec-jtc1-sc29':'Watanabe Shinji ',
+ 'iso-iec-jtc1-sc29-wg1':'Watanabe Shinji ',
+ 'iso-iec-jtc1-sc29-wg11':'Watanabe Shinji ',
+ 'unicode':'Richard McGowan ',
+ 'isotc46':'sabine.donnardcusse@afnor.org',
+ 'w3c':u'Wendy Seltzer ,Philippe Le Hégaret ',
+ # change to m3aawg
+ 'maawg':'Mike Adkins ,technical-chair@mailman.m3aawg.org',
+ 'ecma-tc39':'John Neuman ,Istvan Sebestyen ',
+}
+
\ No newline at end of file
diff --git a/ietf/liaisons/migrations/0006_remove_fields.py b/ietf/liaisons/migrations/0006_remove_fields.py
new file mode 100644
index 000000000..0c87983ea
--- /dev/null
+++ b/ietf/liaisons/migrations/0006_remove_fields.py
@@ -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',
+ #),
+ ]
diff --git a/ietf/liaisons/migrations/0007_auto_20151009_1220.py b/ietf/liaisons/migrations/0007_auto_20151009_1220.py
new file mode 100644
index 000000000..bd5d53b5d
--- /dev/null
+++ b/ietf/liaisons/migrations/0007_auto_20151009_1220.py
@@ -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,
+ ),
+ ]
diff --git a/ietf/liaisons/models.py b/ietf/liaisons/models.py
index f31a76bd1..6a0b308eb 100644
--- a/ietf/liaisons/models.py
+++ b/ietf/liaisons/models.py
@@ -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""
+
+ 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']
diff --git a/ietf/liaisons/resources.py b/ietf/liaisons/resources.py
index 0911bf7fe..0d1615eae 100644
--- a/ietf/liaisons/resources.py
+++ b/ietf/liaisons/resources.py
@@ -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())
+
diff --git a/ietf/liaisons/tests.py b/ietf/liaisons/tests.py
index 8e06aad63..0828add48 100644
--- a/ietf/liaisons/tests.py
+++ b/ietf/liaisons/tests.py
@@ -1,89 +1,117 @@
import datetime, os, shutil
+import json
from django.conf import settings
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse as urlreverse
+from django.db.models import Q
from StringIO import StringIO
from pyquery import PyQuery
from ietf.utils.test_utils import TestCase, login_testing_unauthorized
-from ietf.utils.test_data import make_test_data
+from ietf.utils.test_data import make_test_data, create_person
from ietf.utils.mail import outbox
-from ietf.liaisons.models import LiaisonStatement, LiaisonStatementPurposeName
+from ietf.doc.models import Document
+from ietf.liaisons.models import (LiaisonStatement, LiaisonStatementPurposeName,
+ LiaisonStatementState, LiaisonStatementEvent, LiaisonStatementGroupContacts,
+ LiaisonStatementAttachment)
from ietf.person.models import Person, Email
-from ietf.group.models import Group, Role
+from ietf.group.models import Group
from ietf.liaisons.mails import send_sdo_reminder, possibly_send_deadline_reminder
+from ietf.liaisons.views import contacts_from_roles
+
+# -------------------------------------------------
+# Helper Functions
+# -------------------------------------------------
def make_liaison_models():
sdo = Group.objects.create(
name="United League of Marsmen",
- acronym="",
+ acronym="ulm",
+ state_id="active",
+ type_id="sdo",
+ )
+ Group.objects.create(
+ name="Standards Development Organization",
+ acronym="sdo",
state_id="active",
type_id="sdo",
)
# liaison manager
- u = User.objects.create(username="zrk")
- p = Person.objects.create(
- name="Zrk Brekkk",
- ascii="Zrk Brekkk",
- user=u)
- manager = email = Email.objects.create(
- address="zrk@ulm.mars",
- person=p)
- Role.objects.create(
- name_id="liaiman",
- group=sdo,
- person=p,
- email=email)
-
- # authorized individual
- u = User.objects.create(username="rkz")
- p = Person.objects.create(
- name="Rkz Kkkreb",
- ascii="Rkz Kkkreb",
- user=u)
- email = Email.objects.create(
- address="rkz@ulm.mars",
- person=p)
- Role.objects.create(
- name_id="auth",
- group=sdo,
- person=p,
- email=email)
+ create_person(sdo, 'liaiman')
+ create_person(sdo, 'auth')
+ by = Person.objects.get(name='Ulm Liaiman')
mars_group = Group.objects.get(acronym="mars")
-
- l = LiaisonStatement.objects.create(
+ create_person(mars_group, 'secr')
+ create_person(Group.objects.get(acronym='iab'), "execdir")
+
+ # add an incoming liaison
+ s = LiaisonStatement.objects.create(
title="Comment from United League of Marsmen",
purpose_id="comment",
body="The recently proposed Martian Standard for Communication Links neglects the special ferro-magnetic conditions of the Martian soil.",
deadline=datetime.date.today() + datetime.timedelta(days=7),
- related_to=None,
- from_group=sdo,
- from_name=sdo.name,
- from_contact=manager,
- to_group=mars_group,
- to_name=mars_group.name,
- to_contact="%s@ietf.org" % mars_group.acronym,
- reply_to=email.address,
- response_contact="",
- technical_contact="",
- cc="",
- submitted=datetime.datetime.now(),
- modified=datetime.datetime.now(),
- approved=datetime.datetime.now(),
- action_taken=False,
+ from_contact=Email.objects.last(),
+ to_contacts="%s@ietf.org" % mars_group.acronym,
+ state_id='posted',
)
- return l
+ s.from_groups.add(sdo)
+ s.to_groups.add(mars_group)
+
+ LiaisonStatementEvent.objects.create(type_id='submitted',by=by,statement=s,desc='Statement Submitted')
+ LiaisonStatementEvent.objects.create(type_id='posted',by=by,statement=s,desc='Statement Posted')
+ doc = Document.objects.first()
+ LiaisonStatementAttachment.objects.create(statement=s,document=doc)
+
+ # add an outgoing liaison (dated 2010)
+ s2 = LiaisonStatement.objects.create(
+ title="Comment from Mars Group on video codec",
+ purpose_id="comment",
+ body="Hello, this is an interesting statement.",
+ from_contact=Email.objects.last(),
+ to_contacts="%s@ietf.org" % mars_group.acronym,
+ state_id='posted',
+ )
+ s2.from_groups.add(mars_group)
+ s2.to_groups.add(sdo)
+ LiaisonStatementEvent.objects.create(type_id='submitted',by=by,statement=s2,desc='Statement Submitted')
+ LiaisonStatementEvent.objects.create(type_id='posted',by=by,statement=s2,desc='Statement Posted')
+ s2.liaisonstatementevent_set.update(time=datetime.datetime(2010,1,1))
+
+ return s
+
+def get_liaison_post_data(type='incoming'):
+ '''Return a dictionary containing basic liaison entry data'''
+ if type == 'incoming':
+ from_group = Group.objects.get(acronym='ulm')
+ to_group = Group.objects.get(acronym="mars")
+ else:
+ to_group = Group.objects.get(acronym='ulm')
+ from_group = Group.objects.get(acronym="mars")
+
+ return dict(from_groups=str(from_group.pk),
+ from_contact='ulm-liaiman@ietf.org',
+ to_groups=str(to_group.pk),
+ to_contacts='to_contacts@example.com',
+ purpose="info",
+ title="title",
+ submitted_date=datetime.datetime.today().strftime("%Y-%m-%d"),
+ body="body",
+ send="1" )
+
+# -------------------------------------------------
+# Test Classes
+# -------------------------------------------------
class LiaisonTests(TestCase):
def test_overview(self):
make_test_data()
liaison = make_liaison_models()
- r = self.client.get(urlreverse('liaison_list'))
+ r = self.client.get(urlreverse('ietf.liaisons.views.liaison_list'))
self.assertEqual(r.status_code, 200)
self.assertTrue(liaison.title in r.content)
@@ -91,7 +119,7 @@ class LiaisonTests(TestCase):
make_test_data()
liaison = make_liaison_models()
- r = self.client.get(urlreverse("liaison_detail", kwargs={ 'object_id': liaison.pk }))
+ r = self.client.get(urlreverse("ietf.liaisons.views.liaison_detail", kwargs={ 'object_id': liaison.pk }))
self.assertEqual(r.status_code, 200)
self.assertTrue(liaison.title in r.content)
@@ -103,11 +131,11 @@ class LiaisonTests(TestCase):
self.assertEqual(r.status_code, 200)
self.assertTrue(liaison.title in r.content)
- r = self.client.get('/feed/liaison/from/%s/' % liaison.from_group.name)
+ r = self.client.get('/feed/liaison/from/%s/' % liaison.from_groups.first().acronym)
self.assertEqual(r.status_code, 200)
self.assertTrue(liaison.title in r.content)
- r = self.client.get('/feed/liaison/to/%s/' % liaison.to_name)
+ r = self.client.get('/feed/liaison/to/%s/' % liaison.to_groups.first().acronym)
self.assertEqual(r.status_code, 200)
self.assertTrue(liaison.title in r.content)
@@ -121,7 +149,7 @@ class LiaisonTests(TestCase):
r = self.client.get('/sitemap-liaison.xml')
self.assertEqual(r.status_code, 200)
- self.assertTrue(urlreverse("liaison_detail", kwargs={ 'object_id': liaison.pk }) in r.content)
+ self.assertTrue(urlreverse("ietf.liaisons.views.liaison_detail", kwargs={ 'object_id': liaison.pk }) in r.content)
def test_help_pages(self):
self.assertEqual(self.client.get('/liaison/help/').status_code, 200)
@@ -130,6 +158,166 @@ class LiaisonTests(TestCase):
self.assertEqual(self.client.get('/liaison/help/to_ietf/').status_code, 200)
+class UnitTests(TestCase):
+ def test_get_cc(self):
+ make_test_data()
+ make_liaison_models()
+ from ietf.liaisons.views import get_cc,EMAIL_ALIASES
+
+ # test IETF
+ cc = get_cc(Group.objects.get(acronym='ietf'))
+ self.assertTrue(EMAIL_ALIASES['IESG'] in cc)
+ self.assertTrue(EMAIL_ALIASES['IETFCHAIR'] in cc)
+ # test IAB
+ cc = get_cc(Group.objects.get(acronym='iab'))
+ self.assertTrue(EMAIL_ALIASES['IAB'] in cc)
+ self.assertTrue(EMAIL_ALIASES['IABCHAIR'] in cc)
+ self.assertTrue(EMAIL_ALIASES['IABEXECUTIVEDIRECTOR'] in cc)
+ # test an Area
+ area = Group.objects.filter(type='area').first()
+ cc = get_cc(area)
+ self.assertTrue(EMAIL_ALIASES['IETFCHAIR'] in cc)
+ self.assertTrue(contacts_from_roles([area.ad_role()]) in cc)
+ # test a Working Group
+ wg = Group.objects.filter(type='wg').first()
+ cc = get_cc(wg)
+ self.assertTrue(contacts_from_roles([wg.parent.ad_role()]) in cc)
+ self.assertTrue(contacts_from_roles([wg.get_chair()]) in cc)
+ # test an SDO
+ sdo = Group.objects.filter(type='sdo').first()
+ cc = get_cc(sdo)
+ self.assertTrue(contacts_from_roles([sdo.role_set.filter(name='liaiman').first()]) in cc)
+
+ def test_get_contacts_for_group(self):
+ make_test_data()
+ make_liaison_models()
+ from ietf.liaisons.views import get_contacts_for_group, EMAIL_ALIASES
+
+ # test explicit
+ sdo = Group.objects.filter(type='sdo').first()
+ LiaisonStatementGroupContacts.objects.create(group=sdo,contacts='bob@world.com')
+ contacts = get_contacts_for_group(sdo)
+ self.assertTrue('bob@world.com' in contacts)
+ # test area
+ area = Group.objects.filter(type='area').first()
+ contacts = get_contacts_for_group(area)
+ self.assertTrue(area.ad_role().email.address in contacts)
+ # test wg
+ wg = Group.objects.filter(type='wg').first()
+ contacts = get_contacts_for_group(wg)
+ self.assertTrue(wg.get_chair().email.address in contacts)
+ # test ietf
+ contacts = get_contacts_for_group(Group.objects.get(acronym='ietf'))
+ self.assertTrue(EMAIL_ALIASES['IETFCHAIR'] in contacts)
+ # test iab
+ contacts = get_contacts_for_group(Group.objects.get(acronym='iab'))
+ self.assertTrue(EMAIL_ALIASES['IABCHAIR'] in contacts)
+ self.assertTrue(EMAIL_ALIASES['IABEXECUTIVEDIRECTOR'] in contacts)
+ # test iesg
+ contacts = get_contacts_for_group(Group.objects.get(acronym='iesg'))
+ self.assertTrue(EMAIL_ALIASES['IESG'] in contacts)
+
+ def test_needs_approval(self):
+ make_test_data()
+ make_liaison_models()
+ from ietf.liaisons.views import needs_approval
+
+ group = Group.objects.get(acronym='ietf')
+ self.assertFalse(needs_approval(group,group.get_chair().person))
+ group = Group.objects.get(acronym='iab')
+ self.assertFalse(needs_approval(group,group.get_chair().person))
+ area = Group.objects.filter(type='area').first()
+ self.assertFalse(needs_approval(area,area.ad_role().person))
+ wg = Group.objects.filter(type='wg').first()
+ self.assertFalse(needs_approval(wg,wg.parent.ad_role().person))
+
+ def test_approvable_liaison_statements(self):
+ make_test_data()
+ make_liaison_models()
+ from ietf.liaisons.utils import approvable_liaison_statements
+
+ outgoing = LiaisonStatement.objects.filter(to_groups__type='sdo').first()
+ outgoing.set_state('pending')
+ user = outgoing.from_groups.first().ad_role().person.user
+ qs = approvable_liaison_statements(user)
+ self.assertEqual(len(qs),1)
+ self.assertEqual(qs[0].pk,outgoing.pk)
+
+
+class AjaxTests(TestCase):
+ def test_ajax(self):
+ make_test_data()
+ make_liaison_models()
+
+ url = urlreverse('ietf.liaisons.views.ajax_get_liaison_info') + "?to_groups=&from_groups="
+ self.client.login(username="secretary", password="secretary+password")
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ data = json.loads(r.content)
+ self.assertEqual(data["error"], False)
+ self.assertEqual(data["post_only"], False)
+ self.assertTrue('cc' in data)
+ self.assertTrue('needs_approval' in data)
+ self.assertTrue('to_contacts' in data)
+ self.assertTrue('response_contacts' in data)
+
+ def test_ajax_to_contacts(self):
+ make_test_data()
+ liaison = make_liaison_models()
+ group = liaison.to_groups.first()
+ LiaisonStatementGroupContacts.objects.create(group=group,contacts='test@example.com')
+
+ url = urlreverse('ietf.liaisons.views.ajax_get_liaison_info') + "?to_groups={}&from_groups=".format(group.pk)
+ self.client.login(username="secretary", password="secretary+password")
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ data = json.loads(r.content)
+ self.assertEqual(data["to_contacts"],[u'test@example.com'])
+
+ def test_ajax_select2_search_liaison_statements(self):
+ make_test_data()
+ liaison = make_liaison_models()
+
+ # test text search
+ url = urlreverse('ietf.liaisons.views.ajax_select2_search_liaison_statements') + "?q=%s" % liaison.title[:5]
+ self.client.login(username="secretary", password="secretary+password")
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ data = json.loads(r.content)
+ self.assertTrue(liaison.pk in [ x['id'] for x in data ])
+
+ # test id search
+ url = urlreverse('ietf.liaisons.views.ajax_select2_search_liaison_statements') + "?q=%s" % liaison.pk
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ data = json.loads(r.content)
+ self.assertTrue(liaison.pk in [ x['id'] for x in data ])
+
+
+class ManagementCommandTests(TestCase):
+ def test_check_liaison_deadlines(self):
+ make_test_data()
+ liaison = make_liaison_models()
+ from django.core.management import call_command
+
+ out = StringIO()
+ liaison.deadline = datetime.datetime.today() + datetime.timedelta(1)
+ liaison.save()
+ mailbox_before = len(outbox)
+ call_command('check_liaison_deadlines',stdout=out)
+ self.assertEqual(len(outbox), mailbox_before + 1)
+
+ def test_remind_update_sdo_list(self):
+ make_test_data()
+ make_liaison_models()
+ from django.core.management import call_command
+
+ out = StringIO()
+ mailbox_before = len(outbox)
+ call_command('remind_update_sdo_list',stdout=out)
+ self.assertTrue(len(outbox) > mailbox_before)
+
+
class LiaisonManagementTests(TestCase):
def setUp(self):
self.liaison_dir = os.path.abspath("tmp-liaison-dir")
@@ -140,17 +328,27 @@ class LiaisonManagementTests(TestCase):
def tearDown(self):
shutil.rmtree(self.liaison_dir)
+ def test_add_restrictions(self):
+ make_test_data()
+ make_liaison_models()
+
+ # incoming restrictions
+ url = urlreverse('ietf.liaisons.views.liaison_add', kwargs={'type':'incoming'})
+ self.client.login(username="secretary", password="secretary+password")
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+
def test_taken_care_of(self):
make_test_data()
liaison = make_liaison_models()
-
- url = urlreverse('liaison_detail', kwargs=dict(object_id=liaison.pk))
+
+ url = urlreverse('ietf.liaisons.views.liaison_detail', kwargs=dict(object_id=liaison.pk))
# normal get
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('form input[name=do_action_taken]')), 0)
-
+
# log in and get
self.client.login(username="secretary", password="secretary+password")
@@ -158,7 +356,7 @@ class LiaisonManagementTests(TestCase):
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('form input[name=do_action_taken]')), 1)
-
+
# mark action taken
r = self.client.post(url, dict(do_action_taken="1"))
self.assertEqual(r.status_code, 200)
@@ -170,91 +368,104 @@ class LiaisonManagementTests(TestCase):
def test_approval_process(self):
make_test_data()
liaison = make_liaison_models()
- # has to come from WG to need approval
- liaison.from_group = Group.objects.get(acronym="mars")
- liaison.approved = None
+ # must be outgoing liaison to need approval
+ liaison.from_groups.clear()
+ liaison.to_groups.clear()
+ liaison.from_groups.add(Group.objects.get(acronym="mars"))
+ liaison.to_groups.add(Group.objects.get(acronym='ulm'))
+ liaison.state=LiaisonStatementState.objects.get(slug='pending')
liaison.save()
# check the overview page
- url = urlreverse('liaison_approval_list')
- # this liaison is for a WG so we need the AD for the area
+ url = urlreverse('ietf.liaisons.views.liaison_list', kwargs=dict(state='pending'))
login_testing_unauthorized(self, "ad", url)
-
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertTrue(liaison.title in r.content)
- # check detail page
- url = urlreverse('liaison_approval_detail', kwargs=dict(object_id=liaison.pk))
+ # check the detail page / unauthorized
+ url = urlreverse('ietf.liaisons.views.liaison_detail', kwargs=dict(object_id=liaison.pk))
self.client.logout()
- login_testing_unauthorized(self, "ad", url)
-
- # normal get
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertTrue(liaison.title in r.content)
q = PyQuery(r.content)
- self.assertEqual(len(q('form input[name=do_approval]')), 1)
-
+ self.assertEqual(len(q('form input[name=approved]')), 0)
+
+ # check the detail page / authorized
+ self.client.login(username="ulm-liaiman", password="ulm-liaiman+password")
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ self.assertTrue(liaison.title in r.content)
+ q = PyQuery(r.content)
+ from ietf.liaisons.utils import can_edit_liaison
+ user = User.objects.get(username='ulm-liaiman')
+ self.assertTrue(can_edit_liaison(user, liaison))
+ self.assertEqual(len(q('form input[name=approved]')), 1)
+
# approve
mailbox_before = len(outbox)
- r = self.client.post(url, dict(do_approval="1"))
- self.assertEqual(r.status_code, 302)
-
+ r = self.client.post(url, dict(approved="1"))
+ self.assertEqual(r.status_code, 200)
+
liaison = LiaisonStatement.objects.get(id=liaison.id)
self.assertTrue(liaison.approved)
self.assertEqual(len(outbox), mailbox_before + 1)
self.assertTrue("Liaison Statement" in outbox[-1]["Subject"])
+ # ensure events created
+ self.assertTrue(liaison.liaisonstatementevent_set.filter(type='approved'))
+ self.assertTrue(liaison.liaisonstatementevent_set.filter(type='posted'))
def test_edit_liaison(self):
make_test_data()
liaison = make_liaison_models()
-
- url = urlreverse('liaison_edit', kwargs=dict(object_id=liaison.pk))
+ from_group = liaison.from_groups.first()
+ to_group = liaison.to_groups.first()
+
+ url = urlreverse('ietf.liaisons.views.liaison_edit', kwargs=dict(object_id=liaison.pk))
login_testing_unauthorized(self, "secretary", url)
# get
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
- self.assertEqual(len(q('form input[name=from_field]')), 1)
+ self.assertEqual(len(q('form input[name=from_contact]')), 1)
# edit
attachments_before = liaison.attachments.count()
test_file = StringIO("hello world")
test_file.name = "unnamed"
r = self.client.post(url,
- dict(from_field="from",
- replyto="replyto@example.com",
- organization="org",
- to_poc="to_poc@example.com",
- response_contact="responce_contact@example.com",
- technical_contact="technical_contact@example.com",
- cc1="cc@example.com",
- purpose="4",
- deadline_date=(liaison.deadline + datetime.timedelta(days=1)).strftime("%Y-%m-%d"),
+ dict(from_groups=str(from_group.pk),
+ from_contact=liaison.from_contact,
+ to_groups=str(to_group.pk),
+ to_contacts="to_poc@example.com",
+ technical_contacts="technical_contact@example.com",
+ cc_contacts="cc@example.com",
+ purpose="action",
+ deadline=(liaison.deadline + datetime.timedelta(days=1)).strftime("%Y-%m-%d"),
title="title",
- submitted_date=(liaison.submitted + datetime.timedelta(days=1)).strftime("%Y-%m-%d"),
+ submitted_date=(liaison.posted + datetime.timedelta(days=1)).strftime("%Y-%m-%d"),
body="body",
attach_file_1=test_file,
attach_title_1="attachment",
))
self.assertEqual(r.status_code, 302)
-
+
new_liaison = LiaisonStatement.objects.get(id=liaison.id)
- self.assertEqual(new_liaison.from_name, "from")
- self.assertEqual(new_liaison.reply_to, "replyto@example.com")
- self.assertEqual(new_liaison.to_name, "org")
- self.assertEqual(new_liaison.to_contact, "to_poc@example.com")
- self.assertEqual(new_liaison.response_contact, "responce_contact@example.com")
- self.assertEqual(new_liaison.technical_contact, "technical_contact@example.com")
- self.assertEqual(new_liaison.cc, "cc@example.com")
- self.assertEqual(new_liaison.purpose, LiaisonStatementPurposeName.objects.get(order=4))
+ self.assertEqual(new_liaison.from_groups.first(), from_group)
+ self.assertEqual(new_liaison.to_groups.first(), to_group)
+ self.assertEqual(new_liaison.to_contacts, "to_poc@example.com")
+ self.assertEqual(new_liaison.technical_contacts, "technical_contact@example.com")
+ self.assertEqual(new_liaison.cc_contacts, "cc@example.com")
+ self.assertEqual(new_liaison.purpose, LiaisonStatementPurposeName.objects.get(slug='action'))
self.assertEqual(new_liaison.deadline, liaison.deadline + datetime.timedelta(days=1)),
self.assertEqual(new_liaison.title, "title")
- self.assertEqual(new_liaison.submitted.date(), (liaison.submitted + datetime.timedelta(days=1)).date())
+ #self.assertEqual(new_liaison.submitted.date(), (liaison.submitted + datetime.timedelta(days=1)).date())
self.assertEqual(new_liaison.body, "body")
-
+ # ensure events created
+ self.assertTrue(liaison.liaisonstatementevent_set.filter(type='modified'))
+
self.assertEqual(new_liaison.attachments.count(), attachments_before + 1)
attachment = new_liaison.attachments.order_by("-name")[0]
self.assertEqual(attachment.title, "attachment")
@@ -263,12 +474,233 @@ class LiaisonManagementTests(TestCase):
test_file.seek(0)
self.assertEqual(written_content, test_file.read())
-
+
+ def test_incoming_access(self):
+ '''Ensure only Secretariat, Liaison Managers, and Authorized Individuals
+ have access to incoming liaisons.
+ '''
+ make_test_data()
+ make_liaison_models()
+ url = urlreverse('ietf.liaisons.views.liaison_list')
+ addurl = urlreverse('ietf.liaisons.views.liaison_add', kwargs={'type':'incoming'})
+
+ # public user no access
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ q = PyQuery(r.content)
+ self.assertEqual(len(q("a.btn:contains('New incoming liaison')")), 0)
+ r = self.client.get(addurl)
+ self.assertRedirects(r,settings.LOGIN_URL + '?next=/liaison/add/incoming/')
+
+ # regular Chair no access
+ self.client.login(username="marschairman", password="marschairman+password")
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ q = PyQuery(r.content)
+ self.assertEqual(len(q("a.btn:contains('New incoming liaison')")), 0)
+ r = self.client.get(addurl)
+ self.assertEqual(r.status_code, 403)
+
+ # Liaison Manager has access
+ self.client.login(username="ulm-liaiman", password="ulm-liaiman+password")
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ q = PyQuery(r.content)
+ self.assertEqual(len(q('a.btn:contains("New incoming liaison")')), 1)
+ r = self.client.get(addurl)
+ self.assertEqual(r.status_code, 200)
+
+ # Authorized Individual has access
+ self.client.login(username="ulm-auth", password="ulm-auth+password")
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ q = PyQuery(r.content)
+ self.assertEqual(len(q("a.btn:contains('New incoming liaison')")), 1)
+ r = self.client.get(addurl)
+ self.assertEqual(r.status_code, 200)
+
+ # Secretariat has access
+ self.client.login(username="secretary", password="secretary+password")
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ q = PyQuery(r.content)
+ self.assertEqual(len(q("a.btn:contains('New incoming liaison')")), 1)
+ r = self.client.get(addurl)
+ self.assertEqual(r.status_code, 200)
+
+ def test_outgoing_access(self):
+ make_test_data()
+ make_liaison_models()
+ url = urlreverse('ietf.liaisons.views.liaison_list')
+ addurl = urlreverse('ietf.liaisons.views.liaison_add', kwargs={'type':'outgoing'})
+
+ # public user no access
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ q = PyQuery(r.content)
+ self.assertEqual(len(q("a.btn:contains('New outgoing liaison')")), 0)
+ r = self.client.get(addurl)
+ self.assertRedirects(r,settings.LOGIN_URL + '?next=/liaison/add/outgoing/')
+
+ # AD has access
+ self.client.login(username="ad", password="ad+password")
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ q = PyQuery(r.content)
+ self.assertEqual(len(q("a.btn:contains('New outgoing liaison')")), 1)
+ r = self.client.get(addurl)
+ self.assertEqual(r.status_code, 200)
+
+ # WG Chair has access
+ self.client.login(username="marschairman", password="marschairman+password")
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ q = PyQuery(r.content)
+ self.assertEqual(len(q("a.btn:contains('New outgoing liaison')")), 1)
+ r = self.client.get(addurl)
+ self.assertEqual(r.status_code, 200)
+
+ # WG Secretary has access
+ self.client.login(username="mars-secr", password="mars-secr+password")
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ q = PyQuery(r.content)
+ self.assertEqual(len(q("a.btn:contains('New outgoing liaison')")), 1)
+ r = self.client.get(addurl)
+ self.assertEqual(r.status_code, 200)
+
+ # IETF Chair has access
+ self.client.login(username="ietf-chair", password="ietf-chair+password")
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ q = PyQuery(r.content)
+ self.assertEqual(len(q("a.btn:contains('New outgoing liaison')")), 1)
+ r = self.client.get(addurl)
+ self.assertEqual(r.status_code, 200)
+
+ # IAB Chair has access
+ self.client.login(username="iab-chair", password="iab-chair+password")
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ q = PyQuery(r.content)
+ self.assertEqual(len(q("a.btn:contains('New outgoing liaison')")), 1)
+ r = self.client.get(addurl)
+ self.assertEqual(r.status_code, 200)
+
+ # IAB Executive Director
+ self.client.login(username="iab-execdir", password="iab-execdir+password")
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ q = PyQuery(r.content)
+ self.assertEqual(len(q("a.btn:contains('New outgoing liaison')")), 1)
+ r = self.client.get(addurl)
+ self.assertEqual(r.status_code, 200)
+
+ # Liaison Manager has access
+ self.client.login(username="ulm-liaiman", password="ulm-liaiman+password")
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ q = PyQuery(r.content)
+ self.assertEqual(len(q('a.btn:contains("New outgoing liaison")')), 1)
+ r = self.client.get(addurl)
+ self.assertEqual(r.status_code, 200)
+
+ # Authorized Individual has no access
+ self.client.login(username="ulm-auth", password="ulm-auth+password")
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ q = PyQuery(r.content)
+ self.assertEqual(len(q("a.btn:contains('New outgoing liaison')")), 0)
+ r = self.client.get(addurl)
+ self.assertEqual(r.status_code, 403)
+
+ # Secretariat has access
+ self.client.login(username="secretary", password="secretary+password")
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ q = PyQuery(r.content)
+ self.assertEqual(len(q("a.btn:contains('New outgoing liaison')")), 1)
+ r = self.client.get(addurl)
+ self.assertEqual(r.status_code, 200)
+
+ def test_incoming_options(self):
+ '''Check from_groups, to_groups options for different user classes'''
+ make_test_data()
+ make_liaison_models()
+ url = urlreverse('ietf.liaisons.views.liaison_add', kwargs={'type':'incoming'})
+
+ # get count of all IETF entities for to_group options
+ top = Q(acronym__in=('ietf','iesg','iab'))
+ areas = Q(type_id='area',state='active')
+ wgs = Q(type_id='wg',state='active')
+ all_entity_count = Group.objects.filter(top|areas|wgs).count()
+
+ # Regular user
+ # from_groups = groups for which they are Liaison Manager or Authorized Individual
+ # to_groups = all IETF entities
+ login_testing_unauthorized(self, "ulm-liaiman", url)
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ q = PyQuery(r.content)
+ self.assertEqual(len(q('select#id_from_groups option')), 1)
+ self.assertEqual(len(q('select#id_to_groups option')), all_entity_count)
+
+ # Secretariat
+ # from_groups = all active SDOs
+ # to_groups = all IETF entities
+ self.client.login(username="secretary", password="secretary+password")
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ q = PyQuery(r.content)
+ all_sdos = Group.objects.filter(type_id='sdo',state='active').count()
+ self.assertEqual(len(q('select#id_from_groups option')), all_sdos)
+ self.assertEqual(len(q('select#id_to_groups option')), all_entity_count)
+
+ def test_outgoing_options(self):
+ make_test_data()
+ make_liaison_models()
+ url = urlreverse('ietf.liaisons.views.liaison_add', kwargs={'type':'outgoing'})
+
+ # get count of all IETF entities for to_group options
+ top = Q(acronym__in=('ietf','iesg','iab'))
+ areas = Q(type_id='area',state='active')
+ wgs = Q(type_id='wg',state='active')
+ all_entity_count = Group.objects.filter(top|areas|wgs).count()
+
+ # Regular user (Chair, AD)
+ # from_groups = limited by role
+ # to_groups = all SDOs
+ person = Person.objects.filter(role__name='chair',role__group__acronym='mars').first()
+ self.client.login(username="marschairman", password="marschairman+password")
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ q = PyQuery(r.content)
+ groups = Group.objects.filter(role__person=person,role__name='chair',state='active',type='wg')
+ all_sdos = Group.objects.filter(state='active',type='sdo')
+ self.assertEqual(len(q('select#id_from_groups option')), groups.count())
+ self.assertEqual(len(q('select#id_to_groups option')), all_sdos.count())
+
+ # Liaison Manager
+ # from_groups =
+ # to_groups = limited to managed group
+
+ # Secretariat
+ # from_groups = all IETF entities
+ # to_groups = all active SDOs
+ self.client.login(username="secretary", password="secretary+password")
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ q = PyQuery(r.content)
+ all_sdos = Group.objects.filter(type_id='sdo',state='active').count()
+ self.assertEqual(len(q('select#id_from_groups option')), all_entity_count)
+ self.assertEqual(len(q('select#id_to_groups option')), all_sdos)
+
+
def test_add_incoming_liaison(self):
make_test_data()
liaison = make_liaison_models()
-
- url = urlreverse('add_liaison') + "?incoming=1"
+
+ url = urlreverse('ietf.liaisons.views.liaison_add', kwargs={'type':'incoming'})
login_testing_unauthorized(self, "secretary", url)
# get
@@ -281,21 +713,22 @@ class LiaisonManagementTests(TestCase):
mailbox_before = len(outbox)
test_file = StringIO("hello world")
test_file.name = "unnamed"
- from_group = Group.objects.filter(type="sdo")[0]
+ from_groups = [ str(g.pk) for g in Group.objects.filter(type="sdo") ]
to_group = Group.objects.get(acronym="mars")
submitter = Person.objects.get(user__username="marschairman")
today = datetime.date.today()
related_liaison = liaison
r = self.client.post(url,
- dict(from_field="%s_%s" % (from_group.type_id, from_group.pk),
- from_fake_user=str(submitter.pk),
- replyto="replyto@example.com",
- organization="%s_%s" % (to_group.type_id, to_group.pk),
- response_contact="responce_contact@example.com",
- technical_contact="technical_contact@example.com",
- cc1="cc@example.com",
- purpose="4",
- deadline_date=(today + datetime.timedelta(days=1)).strftime("%Y-%m-%d"),
+ dict(from_groups=from_groups,
+ from_contact=submitter.email_address(),
+ to_groups=str(to_group.pk),
+ to_contacts='to_contacts@example.com',
+ technical_contacts="technical_contact@example.com",
+ action_holder_contacts="action_holder_contacts@example.com",
+ cc_contacts="cc@example.com",
+ purpose="action",
+ deadline=(today + datetime.timedelta(days=1)).strftime("%Y-%m-%d"),
+ other_identifiers="IETF-1234",
related_to=str(related_liaison.pk),
title="title",
submitted_date=today.strftime("%Y-%m-%d"),
@@ -305,23 +738,26 @@ class LiaisonManagementTests(TestCase):
send="1",
))
self.assertEqual(r.status_code, 302)
-
+
l = LiaisonStatement.objects.all().order_by("-id")[0]
- self.assertEqual(l.from_group, from_group)
+ self.assertEqual(l.from_groups.count(),2)
self.assertEqual(l.from_contact.address, submitter.email_address())
- self.assertEqual(l.reply_to, "replyto@example.com")
- self.assertEqual(l.to_group, to_group)
- self.assertEqual(l.response_contact, "responce_contact@example.com")
- self.assertEqual(l.technical_contact, "technical_contact@example.com")
- self.assertEqual(l.cc, "cc@example.com")
- self.assertEqual(l.purpose, LiaisonStatementPurposeName.objects.get(order=4))
+ self.assertSequenceEqual(l.to_groups.all(),[to_group])
+ self.assertEqual(l.technical_contacts, "technical_contact@example.com")
+ self.assertEqual(l.action_holder_contacts, "action_holder_contacts@example.com")
+ self.assertEqual(l.cc_contacts, "cc@example.com")
+ self.assertEqual(l.purpose, LiaisonStatementPurposeName.objects.get(slug='action'))
self.assertEqual(l.deadline, today + datetime.timedelta(days=1)),
- self.assertEqual(l.related_to, liaison),
+ self.assertEqual(l.other_identifiers, "IETF-1234"),
+ self.assertEqual(l.source_of_set.first().target,liaison),
self.assertEqual(l.title, "title")
self.assertEqual(l.submitted.date(), today)
self.assertEqual(l.body, "body")
- self.assertTrue(l.approved)
-
+ self.assertEqual(l.state.slug, 'posted')
+ # ensure events created
+ self.assertTrue(l.liaisonstatementevent_set.filter(type='submitted'))
+ self.assertTrue(l.liaisonstatementevent_set.filter(type='posted'))
+
self.assertEqual(l.attachments.count(), 1)
attachment = l.attachments.all()[0]
self.assertEqual(attachment.title, "attachment")
@@ -333,12 +769,12 @@ class LiaisonManagementTests(TestCase):
self.assertEqual(len(outbox), mailbox_before + 1)
self.assertTrue("Liaison Statement" in outbox[-1]["Subject"])
-
+
def test_add_outgoing_liaison(self):
make_test_data()
liaison = make_liaison_models()
-
- url = urlreverse('add_liaison')
+
+ url = urlreverse('ietf.liaisons.views.liaison_add', kwargs={'type':'outgoing'})
login_testing_unauthorized(self, "secretary", url)
# get
@@ -357,18 +793,16 @@ class LiaisonManagementTests(TestCase):
today = datetime.date.today()
related_liaison = liaison
r = self.client.post(url,
- dict(from_field="%s_%s" % (from_group.type_id, from_group.pk),
- from_fake_user=str(submitter.pk),
+ dict(from_groups=str(from_group.pk),
+ from_contact=submitter.email_address(),
+ to_groups=str(to_group.pk),
+ to_contacts='to_contacts@example.com',
approved="",
- replyto="replyto@example.com",
- to_poc="to_poc@example.com",
- organization="%s_%s" % (to_group.type_id, to_group.pk),
- other_organization="",
- response_contact="responce_contact@example.com",
- technical_contact="technical_contact@example.com",
- cc1="cc@example.com",
- purpose="4",
- deadline_date=(today + datetime.timedelta(days=1)).strftime("%Y-%m-%d"),
+ technical_contacts="technical_contact@example.com",
+ cc_contacts="cc@example.com",
+ purpose="action",
+ deadline=(today + datetime.timedelta(days=1)).strftime("%Y-%m-%d"),
+ other_identifiers="IETF-1234",
related_to=str(related_liaison.pk),
title="title",
submitted_date=today.strftime("%Y-%m-%d"),
@@ -378,24 +812,26 @@ class LiaisonManagementTests(TestCase):
send="1",
))
self.assertEqual(r.status_code, 302)
-
+
l = LiaisonStatement.objects.all().order_by("-id")[0]
- self.assertEqual(l.from_group, from_group)
+ self.assertSequenceEqual(l.from_groups.all(), [from_group])
self.assertEqual(l.from_contact.address, submitter.email_address())
- self.assertEqual(l.reply_to, "replyto@example.com")
- self.assertEqual(l.to_group, to_group)
- self.assertEqual(l.to_contact, "to_poc@example.com")
- self.assertEqual(l.response_contact, "responce_contact@example.com")
- self.assertEqual(l.technical_contact, "technical_contact@example.com")
- self.assertEqual(l.cc, "cc@example.com")
- self.assertEqual(l.purpose, LiaisonStatementPurposeName.objects.get(order=4))
+ self.assertSequenceEqual(l.to_groups.all(), [to_group])
+ self.assertEqual(l.to_contacts, "to_contacts@example.com")
+ self.assertEqual(l.technical_contacts, "technical_contact@example.com")
+ self.assertEqual(l.cc_contacts, "cc@example.com")
+ self.assertEqual(l.purpose, LiaisonStatementPurposeName.objects.get(slug='action'))
self.assertEqual(l.deadline, today + datetime.timedelta(days=1)),
- self.assertEqual(l.related_to, liaison),
+ self.assertEqual(l.other_identifiers, "IETF-1234"),
+ self.assertEqual(l.source_of_set.first().target,liaison),
self.assertEqual(l.title, "title")
self.assertEqual(l.submitted.date(), today)
self.assertEqual(l.body, "body")
- self.assertTrue(not l.approved)
-
+ self.assertEqual(l.state.slug,'pending')
+ # ensure events created
+ self.assertTrue(l.liaisonstatementevent_set.filter(type='submitted'))
+ self.assertFalse(l.liaisonstatementevent_set.filter(type='posted'))
+
self.assertEqual(l.attachments.count(), 1)
attachment = l.attachments.all()[0]
self.assertEqual(attachment.title, "attachment")
@@ -407,31 +843,307 @@ class LiaisonManagementTests(TestCase):
self.assertEqual(len(outbox), mailbox_before + 1)
self.assertTrue("Liaison Statement" in outbox[-1]["Subject"])
-
- # try adding statement to non-predefined organization
+
+ def test_add_outgoing_liaison_unapproved_post_only(self):
+ make_test_data()
+ make_liaison_models()
+
+ url = urlreverse('ietf.liaisons.views.liaison_add', kwargs={'type':'outgoing'})
+ login_testing_unauthorized(self, "secretary", url)
+
+ # add new
+ mailbox_before = len(outbox)
+ from_group = Group.objects.get(acronym="mars")
+ to_group = Group.objects.filter(type="sdo")[0]
+ submitter = Person.objects.get(user__username="marschairman")
+ today = datetime.date.today()
r = self.client.post(url,
- dict(from_field="%s_%s" % (from_group.type_id, from_group.pk),
- from_fake_user=str(submitter.pk),
- approved="1",
- replyto="replyto@example.com",
- to_poc="to_poc@example.com",
- organization="othersdo",
- other_organization="Mars Institute",
- response_contact="responce_contact@example.com",
- technical_contact="technical_contact@example.com",
- cc1="cc@example.com",
- purpose="4",
- deadline_date=(today + datetime.timedelta(days=1)).strftime("%Y-%m-%d"),
- related_to=str(related_liaison.pk),
- title="new title",
+ dict(from_groups=str(from_group.pk),
+ from_contact=submitter.email_address(),
+ to_groups=str(to_group.pk),
+ to_contacts='to_contacts@example.com',
+ approved="",
+ purpose="info",
+ title="title",
submitted_date=today.strftime("%Y-%m-%d"),
body="body",
+ post_only="1",
))
self.assertEqual(r.status_code, 302)
-
l = LiaisonStatement.objects.all().order_by("-id")[0]
- self.assertEqual(l.to_group, None)
- self.assertEqual(l.to_name, "Mars Institute")
+ self.assertEqual(l.state.slug,'pending')
+ self.assertEqual(len(outbox), mailbox_before + 1)
+
+ def test_liaison_add_attachment(self):
+ make_test_data()
+ liaison = make_liaison_models()
+
+ # get minimum edit post data
+ file = StringIO('dummy file')
+ file.name = "upload.txt"
+ post_data = dict(
+ from_groups = ','.join([ str(x.pk) for x in liaison.from_groups.all() ]),
+ from_contact = liaison.from_contact.address,
+ to_groups = ','.join([ str(x.pk) for x in liaison.to_groups.all() ]),
+ purpose = liaison.purpose.slug,
+ deadline = liaison.deadline,
+ title = liaison.title,
+ submitted_date = liaison.submitted.strftime('%Y-%m-%d'),
+ body = liaison.body,
+ attach_title_1 = 'Test Attachment',
+ attach_file_1 = file,
+ )
+
+ url = urlreverse('ietf.liaisons.views.liaison_edit', kwargs=dict(object_id=liaison.pk))
+ login_testing_unauthorized(self, "secretary", url)
+ r = self.client.post(url,post_data)
+ if r.status_code != 302:
+ q = PyQuery(r.content)
+ print(q('div.has-error span.help-block div').text())
+ print r.content
+ self.assertEqual(r.status_code, 302)
+ self.assertEqual(liaison.attachments.count(),2)
+ event = liaison.liaisonstatementevent_set.order_by('id').last()
+ self.assertTrue(event.desc.startswith('Added attachment'))
+
+ def test_liaison_edit_attachment(self):
+ make_test_data()
+ make_liaison_models()
+
+ attachment = LiaisonStatementAttachment.objects.first()
+ url = urlreverse('ietf.liaisons.views.liaison_edit_attachment', kwargs=dict(object_id=attachment.statement_id,doc_id=attachment.document_id))
+ login_testing_unauthorized(self, "secretary", url)
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ post_data = dict(title='New Title')
+ r = self.client.post(url,post_data)
+ self.assertEqual(r.status_code, 302)
+ self.assertEqual(attachment.document.title,'New Title')
+
+ def test_liaison_delete_attachment(self):
+ make_test_data()
+ liaison = make_liaison_models()
+ attachment = LiaisonStatementAttachment.objects.get(statement=liaison)
+ url = urlreverse('ietf.liaisons.views.liaison_delete_attachment', kwargs=dict(object_id=liaison.pk,attach_id=attachment.pk))
+ login_testing_unauthorized(self, "secretary", url)
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 302)
+ self.assertEqual(liaison.liaisonstatementattachment_set.filter(removed=False).count(),0)
+
+ def test_in_response(self):
+ '''A statement with purpose=in_response must have related statement specified'''
+ make_test_data()
+ make_liaison_models()
+
+ url = urlreverse('ietf.liaisons.views.liaison_add',kwargs=dict(type='incoming'))
+ login_testing_unauthorized(self, "secretary", url)
+ data = get_liaison_post_data()
+ data['purpose'] = 'response'
+ r = self.client.post(url,data)
+ q = PyQuery(r.content)
+ self.assertEqual(r.status_code, 200)
+ self.assertTrue(q("form .has-error"))
+
+ def test_liaison_history(self):
+ make_test_data()
+ liaison = make_liaison_models()
+
+ url = urlreverse('ietf.liaisons.views.liaison_history',kwargs=dict(object_id=liaison.pk))
+ r = self.client.get(url)
+ q = PyQuery(r.content)
+ self.assertEqual(r.status_code, 200)
+ event_count = liaison.liaisonstatementevent_set.count()
+ self.assertEqual(len(q('tr')),event_count + 1) # +1 for header row
+
+ def test_resend_liaison(self):
+ make_test_data()
+ liaison = make_liaison_models()
+
+ url = urlreverse('ietf.liaisons.views.liaison_resend',kwargs=dict(object_id=liaison.pk))
+ login_testing_unauthorized(self, "secretary", url)
+
+ mailbox_before = len(outbox)
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 302)
+
+ self.assertEqual(len(outbox), mailbox_before + 1)
+ self.assertTrue(liaison.liaisonstatementevent_set.filter(type='resent'))
+
+ def test_kill_liaison(self):
+ make_test_data()
+ liaison = make_liaison_models()
+ # must be outgoing liaison to need approval
+ liaison.from_groups.clear()
+ liaison.from_groups.add(Group.objects.get(acronym="mars"))
+ liaison.set_state('pending')
+
+ url = urlreverse('ietf.liaisons.views.liaison_detail', kwargs=dict(object_id=liaison.pk))
+ self.client.login(username="secretary", password="secretary+password")
+ r = self.client.post(url, dict(dead="1"))
+ self.assertEqual(r.status_code, 200)
+
+ # need to reacquire object to check current state
+ liaison = LiaisonStatement.objects.get(pk=liaison.pk)
+ self.assertEqual(liaison.state.slug,'dead')
+ self.assertTrue(liaison.liaisonstatementevent_set.filter(type='killed'))
+
+ def test_dead_view(self):
+ make_test_data()
+ liaison = make_liaison_models()
+ liaison.set_state('dead')
+
+ url = urlreverse('ietf.liaisons.views.liaison_list', kwargs=dict(state='dead'))
+ login_testing_unauthorized(self, "secretary", url)
+ r = self.client.get(url)
+ q = PyQuery(r.content)
+ self.assertEqual(r.status_code, 200)
+ dead_liaison_count = LiaisonStatement.objects.filter(state='dead').count()
+ self.assertEqual(len(q('tr')),dead_liaison_count + 1) # +1 for header row
+
+ def test_liaison_reply(self):
+ make_test_data()
+ liaison = make_liaison_models()
+
+ # unauthorized, no reply to button
+ url = urlreverse('ietf.liaisons.views.liaison_detail', kwargs=dict(object_id=liaison.pk))
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ q = PyQuery(r.content)
+ self.assertEqual(len(q("a.btn:contains('Reply to liaison')")), 0)
+
+ # authorized
+ self.client.login(username="ulm-liaiman", password="ulm-liaiman+password")
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ q = PyQuery(r.content)
+ self.assertEqual(len(q("a.btn:contains('Reply to liaison')")), 1)
+
+ # check form initial values
+ url = urlreverse('ietf.liaisons.views.liaison_reply', kwargs=dict(object_id=liaison.pk))
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 200)
+ q = PyQuery(r.content)
+ reply_to_group_id = str(liaison.from_groups.first().pk)
+ reply_from_group_id = str(liaison.to_groups.first().pk)
+ self.assertEqual(q('#id_from_groups').find('option:selected').val(),reply_from_group_id)
+ self.assertEqual(q('#id_to_groups').find('option:selected').val(),reply_to_group_id)
+ self.assertEqual(q('#id_related_to').val(),str(liaison.pk))
+
+ def test_search(self):
+ make_test_data()
+ make_liaison_models()
+
+ # test list only, no search filters
+ url = urlreverse('ietf.liaisons.views.liaison_list')
+ r = self.client.get(url)
+ q = PyQuery(r.content)
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(len(q('tr')),3) # two results
+
+ # test 0 results
+ url = urlreverse('ietf.liaisons.views.liaison_list') + "?text=gobbledygook&source=&destination=&start_date=&end_date="
+ r = self.client.get(url)
+ q = PyQuery(r.content)
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(len(q('tr')),0) # no results
+
+ # test body text
+ url = urlreverse('ietf.liaisons.views.liaison_list') + "?text=recently&source=&destination=&start_date=&end_date="
+ r = self.client.get(url)
+ q = PyQuery(r.content)
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(len(q('tr')),2) # one result
+
+ # test from group
+ url = urlreverse('ietf.liaisons.views.liaison_list') + "?text=&source=ulm&destination=&start_date=&end_date="
+ r = self.client.get(url)
+ q = PyQuery(r.content)
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(len(q('tr')),2) # one result
+
+ # test start date
+ url = urlreverse('ietf.liaisons.views.liaison_list') + "?text=&source=&destination=&start_date=2015-01-01&end_date="
+ r = self.client.get(url)
+ q = PyQuery(r.content)
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(len(q('tr')),2) # one result
+
+ # -------------------------------------------------
+ # Test Redirects
+ # -------------------------------------------------
+ def test_redirect_add(self):
+ self.client.login(username="secretary", password="secretary+password")
+ url = urlreverse('ietf.liaisons.views.redirect_add')
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 302)
+
+ def test_redirect_for_approval(self):
+ make_test_data()
+ liaison = make_liaison_models()
+ liaison.set_state('pending')
+
+ self.client.login(username="secretary", password="secretary+password")
+ url = urlreverse('ietf.liaisons.views.redirect_for_approval')
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 302)
+ url = urlreverse('ietf.liaisons.views.redirect_for_approval', kwargs={'object_id':liaison.pk})
+ r = self.client.get(url)
+ self.assertEqual(r.status_code, 302)
+
+ # -------------------------------------------------
+ # Form validations
+ # -------------------------------------------------
+ def test_post_and_send_fail(self):
+ make_test_data()
+ make_liaison_models()
+
+ url = urlreverse('ietf.liaisons.views.liaison_add', kwargs={'type':'incoming'})
+ login_testing_unauthorized(self, "ulm-liaiman", url)
+
+ r = self.client.post(url,get_liaison_post_data(),follow=True)
+
+ self.assertEqual(r.status_code, 200)
+ self.assertTrue('As an IETF Liaison Manager you can not send incoming liaison statements' in r.content)
+
+ def test_deadline_field(self):
+ '''Required for action, comment, not info, response'''
+ pass
+
+ def test_email_validations(self):
+ make_test_data()
+ make_liaison_models()
+
+ url = urlreverse('ietf.liaisons.views.liaison_add', kwargs={'type':'incoming'})
+ login_testing_unauthorized(self, "secretary", url)
+
+ post_data = get_liaison_post_data()
+ post_data['from_contact'] = 'bademail'
+ post_data['to_contacts'] = 'bademail'
+ post_data['technical_contacts'] = 'bad_email'
+ post_data['action_holder_contacts'] = 'bad_email'
+ post_data['cc_contacts'] = 'bad_email'
+ r = self.client.post(url,post_data,follow=True)
+
+ q = PyQuery(r.content)
+ self.assertEqual(r.status_code, 200)
+ result = q('#id_technical_contacts').parent().parent('.has-error')
+ result = q('#id_action_holder_contacts').parent().parent('.has-error')
+ result = q('#id_cc_contacts').parent().parent('.has-error')
+ self.assertEqual(len(result), 1)
+
+ def test_body_or_attachment(self):
+ make_test_data()
+ make_liaison_models()
+
+ url = urlreverse('ietf.liaisons.views.liaison_add', kwargs={'type':'incoming'})
+ login_testing_unauthorized(self, "secretary", url)
+
+ post_data = get_liaison_post_data()
+ post_data['body'] = ''
+ r = self.client.post(url,post_data,follow=True)
+
+ self.assertEqual(r.status_code, 200)
+ self.assertTrue('You must provide a body or attachment files' in r.content)
def test_send_sdo_reminder(self):
make_test_data()
@@ -454,7 +1166,7 @@ class LiaisonManagementTests(TestCase):
# try pushing the deadline
liaison.deadline = liaison.deadline + datetime.timedelta(days=30)
liaison.save()
-
+
mailbox_before = len(outbox)
possibly_send_deadline_reminder(liaison)
self.assertEqual(len(outbox), mailbox_before)
diff --git a/ietf/liaisons/urls.py b/ietf/liaisons/urls.py
index 732ee951c..e59bc82b2 100644
--- a/ietf/liaisons/urls.py
+++ b/ietf/liaisons/urls.py
@@ -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\d+)/$', 'liaison_detail', name='liaison_detail'),
- url(r'^(?P\d+)/edit/$', 'liaison_edit', name='liaison_edit'),
- url(r'^for_approval/$', 'liaison_approval_list', name='liaison_approval_list'),
- url(r'^for_approval/(?P\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(posted|pending|dead))/$', 'liaison_list'),
+ (r'^(?P\d+)/$', 'liaison_detail'),
+ (r'^(?P\d+)/edit/$', 'liaison_edit'),
+ (r'^(?P\d+)/edit-attachment/(?P[A-Za-z0-9._+-]+)$', 'liaison_edit_attachment'),
+ (r'^(?P\d+)/delete-attachment/(?P[A-Za-z0-9._+-]+)$', 'liaison_delete_attachment'),
+ (r'^(?P\d+)/history/$', 'liaison_history'),
+ (r'^(?P\d+)/reply/$', 'liaison_reply'),
+ (r'^(?P\d+)/resend/$', 'liaison_resend'),
+ (r'^add/(?P(incoming|outgoing))/$', 'liaison_add'),
+
+ # Redirects for backwards compatibility
+ (r'^add/$', 'redirect_add'),
+ (r'^for_approval/$', 'redirect_for_approval'),
+ (r'^for_approval/(?P\d+)/$', 'redirect_for_approval'),
)
diff --git a/ietf/liaisons/utils.py b/ietf/liaisons/utils.py
index 74b1d84dc..a3a6b52c6 100644
--- a/ietf/liaisons/utils.py
+++ b/ietf/liaisons/utils.py
@@ -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()
diff --git a/ietf/liaisons/views.py b/ietf/liaisons/views.py
index 3a9abaaac..f39c4595b 100644
--- a/ietf/liaisons/views.py
+++ b/ietf/liaisons/views.py
@@ -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 ',
+ 'IESG':'The IESG ',
+ 'IAB':'The IAB ',
+ 'IABCHAIR':'The IAB Chair ',
+ 'IABEXECUTIVEDIRECTOR':'The IAB Executive Director '}
-
-@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')
+
diff --git a/ietf/liaisons/widgets.py b/ietf/liaisons/widgets.py
index 09628d48f..fa2f4de92 100644
--- a/ietf/liaisons/widgets.py
+++ b/ietf/liaisons/widgets.py
@@ -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' %s' % (conditional_escape(value), conditional_escape(name), conditional_escape(name), conditional_escape(text))
- base += u' ' + conditional_escape(self.submitter) + u' '
- if self.full_power_on:
- base += ''
- for from_code in self.full_power_on:
- base += ' ' % conditional_escape(from_code)
- for to_code in self.reduced_to_set:
- base += ' ' % conditional_escape(to_code)
- base += '
'
- return mark_safe(base)
-
-
-class ReadOnlyWidget(Widget):
- def render(self, name, value, attrs=None):
- html = u'%s
' % (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'' % name
html += u'
No files attached '
html += u'
'
- return mark_safe(html)
+ return mark_safe(html)
\ No newline at end of file
diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json
index 33c2a612d..c0dc0dcef 100644
--- a/ietf/name/fixtures/names.json
+++ b/ietf/name/fixtures/names.json
@@ -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,
diff --git a/ietf/name/migrations/0006_add_liaison_names.py b/ietf/name/migrations/0006_add_liaison_names.py
new file mode 100644
index 000000000..392ca6634
--- /dev/null
+++ b/ietf/name/migrations/0006_add_liaison_names.py
@@ -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,),
+ ),
+ ]
diff --git a/ietf/name/migrations/0007_populate_liaison_names.py b/ietf/name/migrations/0007_populate_liaison_names.py
new file mode 100644
index 000000000..212f65975
--- /dev/null
+++ b/ietf/name/migrations/0007_populate_liaison_names.py
@@ -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),
+ ]
diff --git a/ietf/name/models.py b/ietf/name/models.py
index 3c6692cfb..06daac660 100644
--- a/ietf/name/models.py
+++ b/ietf/name/models.py
@@ -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"
diff --git a/ietf/name/resources.py b/ietf/name/resources.py
index 97adac067..2a9cdee0b 100644
--- a/ietf/name/resources.py
+++ b/ietf/name/resources.py
@@ -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())
-
diff --git a/ietf/secr/groups/forms.py b/ietf/secr/groups/forms.py
index 01b5a19a7..079de6a20 100644
--- a/ietf/secr/groups/forms.py
+++ b/ietf/secr/groups/forms.py
@@ -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.")
diff --git a/ietf/secr/groups/views.py b/ietf/secr/groups/views.py
index 9223223d8..feb55d6c1 100644
--- a/ietf/secr/groups/views.py
+++ b/ietf/secr/groups/views.py
@@ -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)
diff --git a/ietf/secr/templates/groups/view.html b/ietf/secr/templates/groups/view.html
index 6cdcaf18f..b3d816e34 100644
--- a/ietf/secr/templates/groups/view.html
+++ b/ietf/secr/templates/groups/view.html
@@ -46,6 +46,9 @@
Email Address: {{ group.list_email }}
Email Subscription: {{ group.list_subscribe }}
Email Archive: {{ group.list_archive }}
+ {% if group.liaison_contacts %}
+ Default Liaison Contacts: {{ group.liaison_contacts.contacts }}
+ {% endif %}
{% if group.features.has_chartering_process %}
Charter: View Charter
{% else %}
diff --git a/ietf/settings.py b/ietf/settings.py
index ceb43e597..0b556a0ce 100644
--- a/ietf/settings.py
+++ b/ietf/settings.py
@@ -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 = {
diff --git a/ietf/static/ietf/css/liaisons.css b/ietf/static/ietf/css/liaisons.css
new file mode 100644
index 000000000..0a1586f90
--- /dev/null
+++ b/ietf/static/ietf/css/liaisons.css
@@ -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;
+}
\ No newline at end of file
diff --git a/ietf/static/ietf/js/liaisons.js b/ietf/static/ietf/js/liaisons.js
index 3b5ecf8ec..60b7c5c72 100644
--- a/ietf/static/ietf/js/liaisons.js
+++ b/ietf/static/ietf/js/liaisons.js
@@ -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 = '';
+ 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 += '
';
+ html += container.attr('id');
+ html += ' ';
+ container.hide();
+ });
+ //html += '
';
+ html += '
Delete ';
+ html += '
';
+ attachmentWidget.config.showOnDisplay.html(html);
+ attachmentWidget.count += 1;
+ attachmentWidget.initFileInput();
+ },
- var cloneFields = function() {
- var html = '';
- 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] + ' <
'+person[1]+' >
';
+ } else {
+ //html += person[0] + ' <'+person[1]+'>\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 += '
';
- html += container.attr('id');
- html += ' ';
- container.hide();
- });
- html += '
';
- html += '
';
- 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] + ' <'+person[1]+' > ';
- } else {
- html += person[0] + ' <'+person[1]+'>\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 += ''+ person[1][0] + ' <' + person[1][1] + '> ';
- });
- select.remove();
- link.after('' + options +' ');
- 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);
});
diff --git a/ietf/templates/liaisons/approval_detail.html b/ietf/templates/liaisons/approval_detail.html
deleted file mode 100644
index b82ec1d1a..000000000
--- a/ietf/templates/liaisons/approval_detail.html
+++ /dev/null
@@ -1,13 +0,0 @@
-{% extends "liaisons/detail.html" %}
-{# Copyright The IETF Trust 2015, All Rights Reserved #}
-{% load origin %}
-
-{% block content %}
- {% origin %}
- {{ block.super }}
-
-
-{% endblock %}
diff --git a/ietf/templates/liaisons/approval_list.html b/ietf/templates/liaisons/approval_list.html
deleted file mode 100644
index 4955d9fb3..000000000
--- a/ietf/templates/liaisons/approval_list.html
+++ /dev/null
@@ -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 %}
- Pending liaison statements
-{% endblock %}
-
-{% block management-links %}
-
- Return to liaison list
-
-{% endblock %}
diff --git a/ietf/templates/liaisons/detail.html b/ietf/templates/liaisons/detail.html
index 570ae2d26..58b58bbeb 100644
--- a/ietf/templates/liaisons/detail.html
+++ b/ietf/templates/liaisons/detail.html
@@ -13,65 +13,68 @@
Liaison statement{% include 'liaisons/liaison_title.html' %}
-
-
- Submission date
- {{ liaison.submitted|date:"Y-m-d" }}
-
-
- From
-
- {{ liaison.from_name }}
- {% if liaison.from_contact %}({{ liaison.from_contact.person }} )
- {% endif %}
-
-
-
- To
-
- {% if liaison.from_contact %}
- {{ liaison.to_name }} ({{ liaison.to_contact|parse_email_list }})
- {% else %}
- {{ liaison.to_name|urlize }}
- {% endif %}
-
-
+ {% include "liaisons/detail_tabs.html" %}
+
+
+
+ State
+ {{ liaison.state }}
+
+
+ Submission Date
+ {{ liaison.submitted|date:"Y-m-d" }}
- {% if liaison.from_contact %}
-
- Cc
-
- {{ liaison.cc|parse_email_list }}
-
-
- {% endif %}
+{% if liaison.from_contact %}
+
+ Sender
+
+ {{ liaison.from_contact.person }}
+
+
+{% endif %}
- {% if liaison.response_contact %}
-
- Response contact
-
- {{ liaison.response_contact|parse_email_list }}
-
-
- {% endif %}
+
+ From
+ {{ liaison.from_groups_display }}
+
- {% if liaison.technical_contact %}
-
- Technical contact
-
- {{ liaison.technical_contact|parse_email_list }}
-
-
- {% endif %}
+
+ To
+ {{ liaison.to_groups_display }}
+
- {% if liaison.purpose %}
-
- Purpose
- {{ liaison.purpose.name }}
-
- {% endif %}
+{% if liaison.cc_contacts %}
+
+ Cc {{ liaison.cc_contacts|parse_email_list|make_one_per_line|safe|linebreaksbr }}
+
+{% endif %}
+{% if liaison.response_contacts %}
+
+ Response Contact
+ {{ liaison.response_contacts|parse_email_list|make_one_per_line|safe|linebreaksbr }}
+
+{% endif %}
+{% if liaison.technical_contacts %}
+
+ Technical Contact
+ {{ liaison.technical_contacts|parse_email_list|make_one_per_line|safe|linebreaksbr }}
+
+{% endif %}
+
+{% if liaison.action_holder_contacts %}
+
+ Action Holder Contacts
+ {{ liaison.action_holder_contacts|parse_email_list|make_one_per_line|safe|linebreaksbr }}
+
+{% endif %}
+
+
+ Purpose
+ {{ liaison.purpose.name }}
+
+
{% if liaison.deadline %}
Deadline
@@ -97,7 +100,7 @@
Liaisons referring to this
{% for rel in relations %}
-
+
{% if rel.title %}{{ rel.title }}{% else %}Liaison #{{ rel.pk }}{% endif %}
@@ -111,7 +114,7 @@
Referenced liaison
-
+
{% if liaison.related_to.title %}{{ liaison.related_to.title }}{% else %}Liaison #{{ liaison.related_to.pk }}{% endif %}
@@ -119,11 +122,18 @@
{% endif %}
{% endif %}
+ {% if liaison.other_identifiers %}
+
+ Other Identifiers
+ {{ liaison.other_identifiers }}
+
+ {% endif %}
+
Attachments
- {% for doc in liaison.attachments.all %}
- {{ doc.title }}
+ {% for doc in liaison.active_attachments.all %}
+ {{ doc.title }}
{% if not forloop.last %} {% endif %}
{% empty %}
(None)
@@ -131,6 +141,28 @@
+{% if relations_by %}
+
+ Liaisons referred by this one
+
+ {% for rel in relations_by %}
+ {% if rel.title %}{{ rel.title }}{% else %}Liaison #{{ rel.pk }}{% endif %}
+ {% endfor %}
+
+
+{% endif %}
+
+{% if relations_to %}
+
+ Liaisons referring to this one
+
+ {% for rel in relations_to %}
+ {% if rel.title %}{{ rel.title }}{% else %}Liaison #{{ rel.pk }}{% endif %}
+ {% endfor %}
+
+
+{% endif %}
+
{% if liaison.from_contact and liaison.body %}
Body
@@ -139,12 +171,29 @@
{% endif %}
-
+
+
- {% if can_edit %}
-
- Edit liaison
-
+
+
+
{% endblock %}
diff --git a/ietf/templates/liaisons/detail_history.html b/ietf/templates/liaisons/detail_history.html
new file mode 100644
index 000000000..af73f9ecc
--- /dev/null
+++ b/ietf/templates/liaisons/detail_history.html
@@ -0,0 +1,47 @@
+{% extends "base.html" %}
+
+{% load future %}
+{% load ietf_filters %}
+
+{% block title %}History for Liaison Statement - {{ liaison.title }}{% endblock %}
+
+{% block content %}
+ History for Liaison Statement{{ liaison.title }}
+
+ {% include "liaisons/detail_tabs.html" %}
+
+ {% comment %}
+ {% if user|has_role:"Area Director,Secretariat,IANA,RFC Editor" %}
+
+ Add comment
+ Add email
+
+ {% endif %}
+ {% endcomment %}
+
+
+
+
+ Date
+ Type
+ By
+ Text
+
+
+
+
+ {% for e in events %}
+
+ {{ e.time|date:"Y-m-d" }}
+ {{ e.type }}
+ {% if e.response_due and e.response_past_due %}
+
+ {% endif %}
+
+ {{ e.by }}
+ {{ e.desc|format_history_text }}
+
+ {% endfor %}
+
+
+{% endblock content %}
diff --git a/ietf/templates/liaisons/detail_tabs.html b/ietf/templates/liaisons/detail_tabs.html
new file mode 100644
index 000000000..56779b248
--- /dev/null
+++ b/ietf/templates/liaisons/detail_tabs.html
@@ -0,0 +1,5 @@
+
+ {% for name, link, selected in tabs %}
+ {{ name }}
+ {% endfor %}
+
diff --git a/ietf/templates/liaisons/edit.html b/ietf/templates/liaisons/edit.html
index 859d0e1cb..1dee7f659 100644
--- a/ietf/templates/liaisons/edit.html
+++ b/ietf/templates/liaisons/edit.html
@@ -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 @@
+
{% endblock %}
{% block morecss %}
@@ -41,18 +43,22 @@
Fields marked with are required. For detailed descriptions of the fields see the field help .
{% endif %}
-
+
+{% endblock %}
+
+
+
diff --git a/ietf/templates/liaisons/feed_item_description.html b/ietf/templates/liaisons/feed_item_description.html
index 1e21644d6..832414003 100644
--- a/ietf/templates/liaisons/feed_item_description.html
+++ b/ietf/templates/liaisons/feed_item_description.html
@@ -5,7 +5,7 @@
{% if attachments %}
{% else %}
diff --git a/ietf/templates/liaisons/field_help.html b/ietf/templates/liaisons/field_help.html
index 0182a2c93..4de075f1d 100644
--- a/ietf/templates/liaisons/field_help.html
+++ b/ietf/templates/liaisons/field_help.html
@@ -20,12 +20,12 @@
- 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
- Liaison statements from the IETF: guidelines for completing the "Cc:" field
+ Liaison statements from the IETF: guidelines for completing the "Cc:" field
@@ -47,65 +47,65 @@
- Field
- Sub-field
- Description
- Comments
+ Fieldset
+ Field
+ Description
+ Comments
From
- Organization
- The organization submitting the liaison statement.
- The field is filled in automatically.
+ Groups
+ The organization(s) submitting the liaison statement.
+ Use arrows to select or type name to search
- Submitter
+ From Contact
- The name and e-mail address of the person submitting the liaison statement.
+ The e-mail address of the person submitting the liaison statement.
The field is filled in automatically.
- Reply to
+ Response Contacts
- 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.
Mandatory format: Name <e-mail address>
To
- Organization
+ Groups
- 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.
- 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
- POC(s)
+ Contacts
- 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.
- The field is filled in automatically.
+ The field may be filled in automatically.
Other email addresses
- Response contact
+ Technical contact
- 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.
Optional. Suggested format: Name <e-mail address>
- Technical contact
+ Action Holder Contacts
- 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.
Optional. Suggested format: Name <e-mail address>
@@ -113,7 +113,7 @@
Cc
- 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.
Optional. Suggested format: Name <e-mail address>
@@ -121,10 +121,10 @@
Purpose
Purpose
- 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.
- The submitter selects one of the four choices, or selects "other" and indicates the intent.
+ The submitter selects one of the four choices.
@@ -132,7 +132,7 @@
Deadline
The date by which a comment or action is required.
- Mandatory if the purpose is "For comment" or "For action." Otherwise, optional.
+ Mandatory if the purpose is "For comment" or "For action."
@@ -153,7 +153,7 @@
Body
The text of the liaison statement.
- 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.
@@ -162,7 +162,7 @@
Title
The title of the attachment.
- 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.
@@ -170,7 +170,7 @@
File
Browse to find the attachment.
- 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.
diff --git a/ietf/templates/liaisons/liaison_base.html b/ietf/templates/liaisons/liaison_base.html
new file mode 100644
index 000000000..b6a028e4e
--- /dev/null
+++ b/ietf/templates/liaisons/liaison_base.html
@@ -0,0 +1,55 @@
+{% extends "base.html" %}
+{# Copyright The IETF Trust 2015, All Rights Reserved #}
+{% load origin %}
+{% load ietf_filters %}
+{% load staticfiles %}
+
+{% block pagehead %}
+
+{% endblock %}
+
+{% block title %}Liaison Statements - {{ selected_menu_entry|capfirst }}{% endblock %}
+
+{% block content %}
+ {% origin %}
+
+ Liaison Statements
+
+ {% if with_search %}
+
+ {% include "liaisons/search_form.html" %}
+
+ {% endif %}
+
+
+ {% for name, url in menu_entries %}
+
+ {{ name }}
+
+ {% endfor %}
+
+
+ {% if menu_actions %}
+
+ {% endif %}
+
+ {% block group_content %}
+
+ {% if search_conducted and not liaisons %}
+ No statements match your query.
+ {% else %}
+ {% include "liaisons/liaison_table.html" %}
+ {% endif %}
+
+ {% endblock group_content %}
+
+{% endblock content %}
+
+{% block js %}
+
+
+{% endblock %}
diff --git a/ietf/templates/liaisons/liaison_mail.txt b/ietf/templates/liaisons/liaison_mail.txt
index c9c2369da..12f1c6283 100644
--- a/ietf/templates/liaisons/liaison_mail.txt
+++ b/ietf/templates/liaisons/liaison_mail.txt
@@ -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 %}
diff --git a/ietf/templates/liaisons/liaison_table.html b/ietf/templates/liaisons/liaison_table.html
new file mode 100644
index 000000000..f1a9df3d3
--- /dev/null
+++ b/ietf/templates/liaisons/liaison_table.html
@@ -0,0 +1,46 @@
+{# Copyright The IETF Trust 2007, All Rights Reserved #}
+{% load ietf_filters %}
+{% load future %}
+
+
diff --git a/ietf/templates/liaisons/liaison_title.html b/ietf/templates/liaisons/liaison_title.html
index 0655be236..387f36728 100644
--- a/ietf/templates/liaisons/liaison_title.html
+++ b/ietf/templates/liaisons/liaison_title.html
@@ -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 %}
diff --git a/ietf/templates/liaisons/overview.html b/ietf/templates/liaisons/overview.html
deleted file mode 100644
index 2db3d63c6..000000000
--- a/ietf/templates/liaisons/overview.html
+++ /dev/null
@@ -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 %}
- Liaison statements
- {% endblock %}
-
- {% block management-links %}
- {% if can_manage %}
-
- {% if can_send_incoming %}
- New incoming liaison
- {% endif %}
- {% if can_send_outgoing %}
- New outgoing liaison
- {% endif %}
- {% if approvable %}
- Approve pending liaison statement{{ approvable|pluralize }} {{ approvable }}
- {% endif %}
-
- {% endif %}
- {% endblock %}
-
-
- {% load ietf_filters %}
-
-
-{% endblock %}
-
diff --git a/ietf/templates/liaisons/pending_liaison_mail.txt b/ietf/templates/liaisons/pending_liaison_mail.txt
index f95438921..f4a964446 100644
--- a/ietf/templates/liaisons/pending_liaison_mail.txt
+++ b/ietf/templates/liaisons/pending_liaison_mail.txt
@@ -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.
diff --git a/ietf/templates/liaisons/sdo_reminder.txt b/ietf/templates/liaisons/sdo_reminder.txt
index 57f1de7f1..8ae3468a8 100644
--- a/ietf/templates/liaisons/sdo_reminder.txt
+++ b/ietf/templates/liaisons/sdo_reminder.txt
@@ -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 %}
------
diff --git a/ietf/templates/liaisons/search_form.html b/ietf/templates/liaisons/search_form.html
new file mode 100644
index 000000000..65d368966
--- /dev/null
+++ b/ietf/templates/liaisons/search_form.html
@@ -0,0 +1,87 @@
+{# Copyright The IETF Trust 2015, All Rights Reserved #}{% load origin %}{% origin %}
+{% load widget_tweaks %}
+{% load ietf_filters %}
+{% load bootstrap3 %}
+
+
+