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 += '' - 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'' html += u'
' if value and isinstance(value, QuerySet): for attachment in value: - html += u'%s
' % (settings.LIAISON_ATTACH_URL, conditional_escape(attachment.external_url), conditional_escape(attachment.title)) + html += u'%s ' % (settings.LIAISON_ATTACH_URL, conditional_escape(attachment.document.external_url), conditional_escape(attachment.document.title)) + html += u'Edit '.format(urlreverse("ietf.liaisons.views.liaison_edit_attachment", kwargs={'object_id':attachment.statement.pk,'doc_id':attachment.document.pk})) + html += u'Delete '.format(urlreverse("ietf.liaisons.views.liaison_delete_attachment", kwargs={'object_id':attachment.statement.pk,'attach_id':attachment.pk})) + html += u'
' else: 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 += ''; + 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 += ''; - 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 += ''; - }); - select.remove(); - link.after(''); - 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 }} - -
- {% csrf_token %} - -
-{% 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' %} - - - - - - - - - - - - - + {% include "liaisons/detail_tabs.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 %} -
+ + + + + + + - {% if liaison.from_contact %} - - - - - {% endif %} +{% if liaison.from_contact %} + + + + +{% endif %} - {% if liaison.response_contact %} - - - - - {% endif %} + + + + - {% if liaison.technical_contact %} - - - - - {% endif %} + + + + - {% if liaison.purpose %} - - - - - {% endif %} +{% if liaison.cc_contacts %} + + + +{% endif %} +{% if liaison.response_contacts %} + + + + +{% endif %} +{% if liaison.technical_contacts %} + + + + +{% endif %} + +{% if liaison.action_holder_contacts %} + + + + +{% endif %} + + + + + + {% if liaison.deadline %} @@ -97,7 +100,7 @@ @@ -119,11 +122,18 @@ {% endif %} {% endif %} + {% if liaison.other_identifiers %} + + + + + {% endif %} + +{% if relations_by %} + + + + +{% endif %} + +{% if relations_to %} + + + + +{% endif %} + {% if liaison.from_contact and liaison.body %} @@ -139,12 +171,29 @@ {% endif %} -
State{{ liaison.state }}
Submission Date{{ liaison.submitted|date:"Y-m-d" }}
Cc - {{ liaison.cc|parse_email_list }} -
Sender + {{ liaison.from_contact.person }} +
Response contact - {{ liaison.response_contact|parse_email_list }} -
From{{ liaison.from_groups_display }}
Technical contact - {{ liaison.technical_contact|parse_email_list }} -
To{{ liaison.to_groups_display }}
Purpose{{ liaison.purpose.name }}
Cc{{ liaison.cc_contacts|parse_email_list|make_one_per_line|safe|linebreaksbr }}
Response Contact{{ liaison.response_contacts|parse_email_list|make_one_per_line|safe|linebreaksbr }}
Technical Contact{{ liaison.technical_contacts|parse_email_list|make_one_per_line|safe|linebreaksbr }}
Action Holder Contacts{{ liaison.action_holder_contacts|parse_email_list|make_one_per_line|safe|linebreaksbr }}
Purpose{{ liaison.purpose.name }}
DeadlineLiaisons 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 %}
Other Identifiers{{ liaison.other_identifiers }}
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 @@
Liaisons referred by this one + {% for rel in relations_by %} + {% if rel.title %}{{ rel.title }}{% else %}Liaison #{{ rel.pk }}{% endif %}
+ {% endfor %} +
Liaisons referring to this one + {% for rel in relations_to %} + {% if rel.title %}{{ rel.title }}{% else %}Liaison #{{ rel.pk }}{% endif %}
+ {% endfor %} +
Body
+ + - {% if can_edit %} -

- Edit liaison -

+

+

+ {% csrf_token %} + {% if liaison.state.slug != 'dead' and can_edit %} + Edit liaison {% endif %} + {% if liaison.state.slug != 'dead' and can_reply %} + Reply to liaison + {% endif %} + {% if liaison.state.slug == 'pending' and can_edit %} + + + {% endif %} + {% if liaison.state.slug == 'posted' and user|has_role:"Secretariat" %} + Resend statement + {% endif %} + {% if liaison.state.slug == 'dead' and can_edit %} + + {% endif %} +
+

{% 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 %} + + + + + + + + + + + + + {% for e in events %} + + + + + + + {% endfor %} + +
DateTypeByText
{{ 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 }}
+{% 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 @@ + 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 %} -
{% csrf_token %} + {% csrf_token %} - {% for fieldset in form.get_fieldsets %} -

{{ fieldset.name }}

- - {% for field in fieldset.fields %} + {% for fieldset in form.fieldsets %} + {% if forloop.first and user|has_role:"Secretariat" %} +

{{ fieldset.name }}

+ {% else %} +

{{ fieldset.name }}

+ {% endif %} + + {% for field in fieldset %} {% bootstrap_field field layout="horizontal" %} {% endfor %} {% endfor %} {% buttons %} - Cancel + Cancel {% if not liaison %} diff --git a/ietf/templates/liaisons/edit_attachment.html b/ietf/templates/liaisons/edit_attachment.html new file mode 100644 index 000000000..8f11d1ec0 --- /dev/null +++ b/ietf/templates/liaisons/edit_attachment.html @@ -0,0 +1,44 @@ +{% extends "base.html" %} +{# Copyright The IETF Trust 2015, All Rights Reserved #} +{% load origin %} +{% load bootstrap3 widget_tweaks %} +{% load staticfiles %} + +{% block title %}Edit liaison attachment{% endblock %} + +{% block pagehead %} + +{% endblock %} + +{% block morecss %} +.widget { height: auto; min-height: 34px; } +{% endblock %} + +{% block content %} + {% origin %} + +

Edit liaison attachment

+ + {% bootstrap_messages %} + + {% if form.errors %} +
+

There were errors in the submitted form -- see below. Please correct these and resubmit.

+
+ {% endif %} + + {% bootstrap_form_errors form %} + + + {% csrf_token %} + {% bootstrap_form form %} + {% buttons %} + Cancel + + {% endbuttons %} +
+ +{% 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 @@ - - - - + + + + - - - + + + - + - + - + - + - + - + - + @@ -113,7 +113,7 @@ @@ -121,10 +121,10 @@ @@ -132,7 +132,7 @@ @@ -153,7 +153,7 @@ @@ -162,7 +162,7 @@ @@ -170,7 +170,7 @@
    FieldSub-fieldDescriptionCommentsFieldsetFieldDescriptionComments
    FromOrganizationThe organization submitting the liaison statement.The field is filled in automatically.GroupsThe organization(s) submitting the liaison statement.Use arrows to select or type name to search
    SubmitterFrom 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 toResponse 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>
    ToOrganizationGroups - 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 addressesResponse contactTechnical 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 contactAction 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>
    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>
    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.
    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."
    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.
    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.
    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 %} + + + + {% if menu_actions %} +
    + {% for name, url in menu_actions %} + {{ name }} + {% endfor %} +
    + {% 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 %} + + + + + + + + + + + + + + {% for liaison in liaisons %} + + + + + + + + {% endfor %} + +
    + Date {% if sort == "date" %}{% endif %} + + From {% if sort == "from_groups" %}{% endif %} + + To {% if sort == "to_groups" %}{% endif %} + + Deadline {% if sort == "deadline" %}{% endif %} + + Title {% if sort == "title" %}{% endif %} +
    {{ liaison.sort_date|date:"Y-m-d" }}{{ liaison.from_groups_display }}{{ liaison.to_groups_display }}{{ liaison.deadline|default:"-"|date:"Y-m-d" }}{% if liaison.deadline and not liaison.action_taken %}
    Action Needed{% endif %}
    + {% if not liaison.from_contact_id %} + {% for doc in liaison.attachments.all %} + {{ doc.title }} +
    + {% endfor %} + {% else %} + {{ liaison.title }} + {% endif %} +
    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 %} - - - - - - - - - - - - - {% for liaison in liaisons %} - - - - - - - - {% endfor %} - -
    - Date {% if sort == "submitted" %}{% endif %} - - From {% if sort == "from_name" %}{% endif %} - - To {% if sort == "to_name" %}{% endif %} - - Deadline {% if sort == "deadline" %}{% endif %} - - Title {% if sort == "title" %}{% endif %} -
    {{ liaison.submitted|date:"Y-m-d" }}{{ liaison.from_name }} - {% if liaison.from_contact_id %} - {{ liaison.to_name }} - {% else %} - {{ liaison.to_name|strip_email }} - {% endif %} - {{ liaison.deadline|default:"-"|date:"Y-m-d" }} - {% if not liaison.from_contact_id %} - {% for doc in liaison.attachments.all %} - {{ doc.title }} -
    - {% endfor %} - {% else %} - {{ liaison.title }} - {% endif %} -
    - -{% 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 %} + +
    + +
    + {{ form.text|add_class:"form-control"|attr:"placeholder:Title, body, identifiers, etc." }} + + + +
    + + {{ form.sort }} {# hidden field #} + +
    +
    + +
    +
    + + +
    +
    + +
    +
    + {{ form.source|add_class:"form-control" }} +
    +
    + +
    +
    + +
    +
    + {{ form.destination|add_class:"form-control" }} +
    +
    + +
    +
    + +
    +
    + {{ form.start_date|add_class:"form-control" }} +
    +
    + +
    +
    + +
    +
    + {{ form.end_date|add_class:"form-control" }} +
    +
    + + + +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    +
    +