diff --git a/hold-for-merge b/hold-for-merge index 206ee5527..9e3248fd6 100644 --- a/hold-for-merge +++ b/hold-for-merge @@ -2,7 +2,6 @@ branch/iola/event-saving-refactor-r10076 @ 10190 -personal/rcross/v6.7.3.dev0@10382 # Test errors, corrected in later commit branch/amsl/liaisons@10160 # Merged as branch/amsl/liaisons/6.4.1 @ 10160 personal/lars/6.2.1.dev0@9970 # Requires the timeline work personal/lars/6.0.5.dev0@9734 # Obsoleted - don't use 'bleach' diff --git a/ietf/liaisons/feeds.py b/ietf/liaisons/feeds.py index f9434d23b..cbe59f66b 100644 --- a/ietf/liaisons/feeds.py +++ b/ietf/liaisons/feeds.py @@ -41,11 +41,11 @@ class LiaisonStatementsFeed(Feed): # wildcards to make it easier to construct a URL that # matches search_string = re.sub(r"[^a-zA-Z1-9]", ".", search) - statement = LiaisonStatement.objects.filter(from_name__iregex=search_string).first() + statement = LiaisonStatement.objects.filter(from_groups__name__iregex=search_string).first() if not statement: raise FeedDoesNotExist - name = statement.from_name + name = statement.from_groups.first().name obj['filter'] = { 'from_name': name } obj['title'] = u'Liaison Statements from %s' % name return obj @@ -97,4 +97,4 @@ class LiaisonStatementsFeed(Feed): return item.submitted def item_author_name(self, item): - return item.from_name + return item.from_groups.first().name diff --git a/ietf/liaisons/forms.py b/ietf/liaisons/forms.py index f97a64c8a..9c09414ff 100644 --- a/ietf/liaisons/forms.py +++ b/ietf/liaisons/forms.py @@ -46,6 +46,13 @@ with the IAB). def liaison_manager_sdos(person): return Group.objects.filter(type="sdo", state="active", role__person=person, role__name="liaiman").distinct() +def flatten_choices(choices): + '''Returns a flat choice list given one with option groups defined''' + flat = [] + for optgroup,options in choices: + flat.extend(options) + return flat + 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 @@ -75,7 +82,8 @@ def get_groups_for_person(person): 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')] + Q(role__person=person,role__name__in=('chair','secretary'),type='wg',state='active'), + Q(parent__role__person=person,parent__role__name='ad',type='wg',state='active')] return Group.objects.filter(reduce(operator.or_,queries)).order_by('acronym').distinct() def liaison_form_factory(request, type=None, **kwargs): @@ -109,6 +117,9 @@ def validate_emails(value): # ------------------------------------------------- # Form Classes # ------------------------------------------------- +class AddCommentForm(forms.Form): + comment = forms.CharField(required=True, widget=forms.Textarea) + private = forms.BooleanField(label="Private comment", required=False,help_text="If this box is checked the comment will not appear in the statement's public history view.") class RadioRenderer(RadioFieldRenderer): def render(self): @@ -119,6 +130,7 @@ class RadioRenderer(RadioFieldRenderer): class SearchLiaisonForm(forms.Form): + '''Expects initial keyword argument queryset which then gets filtered based on form data''' 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) @@ -126,24 +138,42 @@ class SearchLiaisonForm(forms.Form): 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 __init__(self, *args, **kwargs): + self.queryset = kwargs.pop('queryset') + super(SearchLiaisonForm, self).__init__(*args, **kwargs) + def get_results(self): - results = LiaisonStatement.objects.filter(state__slug='posted') + results = self.queryset 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)) + q = (Q(title__icontains=query) | + Q(from_contact__address__icontains=query) | + Q(to_contacts__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)) + source_list = source.split(',') + if len(source_list) > 1: + results = results.filter(Q(from_groups__acronym__in=source_list)) + else: + 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)) + destination_list = destination.split(',') + if len(destination_list) > 1: + results = results.filter(Q(to_groups__acronym__in=destination_list)) + else: + 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') @@ -174,10 +204,13 @@ class CustomModelMultipleChoiceField(forms.ModelMultipleChoiceField): 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') + '''Specify fields which require a custom widget or that are not part of the model. + NOTE: from_groups and to_groups are marked as not required because select2 has + a problem with validating + ''' + from_groups = forms.ModelMultipleChoiceField(queryset=Group.objects.all(),label=u'Groups',required=False) from_contact = forms.EmailField() - to_groups = forms.ModelMultipleChoiceField(queryset=Group.objects,label=u'Groups') + to_groups = forms.ModelMultipleChoiceField(queryset=Group.objects,label=u'Groups',required=False) 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()) @@ -203,13 +236,15 @@ class LiaisonModelForm(BetterModelForm): def __init__(self, user, *args, **kwargs): super(LiaisonModelForm, self).__init__(*args, **kwargs) self.user = user + self.edit = False self.person = get_person_for_user(user) self.is_new = not self.instance.pk 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["other_identifiers"].widget.attrs["rows"] = 2 + # add email validators for field in ['from_contact','to_contacts','technical_contacts','action_holder_contacts','cc_contacts']: if field in self.fields: @@ -218,6 +253,18 @@ class LiaisonModelForm(BetterModelForm): self.set_from_fields() self.set_to_fields() + def clean_from_groups(self): + from_groups = self.cleaned_data.get('from_groups') + if not from_groups: + raise forms.ValidationError('You must specify a From Group') + return from_groups + + def clean_to_groups(self): + to_groups = self.cleaned_data.get('to_groups') + if not to_groups: + raise forms.ValidationError('You must specify a To Group') + return to_groups + def clean_from_contact(self): contact = self.cleaned_data.get('from_contact') try: @@ -275,6 +322,7 @@ class LiaisonModelForm(BetterModelForm): self.save_related_liaisons() self.save_attachments() + self.save_tags() return self.instance @@ -331,6 +379,11 @@ class LiaisonModelForm(BetterModelForm): if related.target not in new_related: related.delete() + def save_tags(self): + '''Create tags as needed''' + if self.instance.deadline and not self.instance.tags.filter(slug='taken'): + self.instance.tags.add('required') + def set_from_fields(self): assert NotImplemented @@ -408,7 +461,14 @@ class OutgoingLiaisonForm(LiaisonModelForm): 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) + choices = get_internal_choices(self.user) + self.fields['from_groups'].choices = choices + + # set initial value if only one entry + flat_choices = flatten_choices(choices) + if len(flat_choices) == 1: + self.fields['from_groups'].initial = [flat_choices[0][0]] + if has_role(self.user, "Secretariat"): return @@ -462,9 +522,9 @@ class EditLiaisonForm(LiaisonModelForm): 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') + queryset = Group.objects.filter(type="sdo").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') + queryset = Group.objects.filter(type="sdo", 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 @@ -475,10 +535,10 @@ class EditLiaisonForm(LiaisonModelForm): 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') + queryset = Group.objects.filter(type="sdo", 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') + queryset = Group.objects.filter(type="sdo").order_by('name') self.fields['to_groups'].queryset = queryset else: self.fields['to_groups'].choices = get_internal_choices(None) diff --git a/ietf/liaisons/migrations/0008_auto_20151110_1352.py b/ietf/liaisons/migrations/0008_auto_20151110_1352.py new file mode 100644 index 000000000..605af9251 --- /dev/null +++ b/ietf/liaisons/migrations/0008_auto_20151110_1352.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations + +def create_required_tags(apps, schema_editor): + LiaisonStatement = apps.get_model("liaisons", "LiaisonStatement") + for s in LiaisonStatement.objects.filter(deadline__isnull=False): + if not s.tags.filter(slug='taken'): + s.tags.add('required') + +class Migration(migrations.Migration): + + dependencies = [ + ('liaisons', '0007_auto_20151009_1220'), + ] + + operations = [ + migrations.RunPython(create_required_tags), + ] diff --git a/ietf/liaisons/migrations/0009_remove_fields.py b/ietf/liaisons/migrations/0009_remove_fields.py new file mode 100644 index 000000000..69068fb33 --- /dev/null +++ b/ietf/liaisons/migrations/0009_remove_fields.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('liaisons', '0008_auto_20151110_1352'), + ] + + operations = [ + migrations.RemoveField( + model_name='liaisonstatement', + name='from_name', + ), + migrations.RemoveField( + model_name='liaisonstatement', + name='to_name', + ), + ] diff --git a/ietf/liaisons/models.py b/ietf/liaisons/models.py index 648c5dda0..c17e79476 100644 --- a/ietf/liaisons/models.py +++ b/ietf/liaisons/models.py @@ -24,11 +24,11 @@ STATE_EVENT_MAPPING = { class LiaisonStatement(models.Model): - title = models.CharField(blank=True, max_length=255) + title = models.CharField(max_length=255) from_groups = models.ManyToManyField(Group, blank=True, related_name='liaisonstatement_from_set') from_contact = models.ForeignKey(Email, blank=True, null=True) 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") + to_contacts = models.CharField(max_length=255, help_text="Contacts at recipient group") 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 @@ -44,10 +44,6 @@ class LiaisonStatement(models.Model): attachments = models.ManyToManyField(Document, through='LiaisonStatementAttachment', blank=True) state = models.ForeignKey(LiaisonStatementState, default='pending') - # 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") - def __unicode__(self): return self.title or u"" @@ -92,9 +88,12 @@ class LiaisonStatement(models.Model): @property def posted(self): - event = self.latest_event(type='posted') - if event: - return event.time + if hasattr(self,'prefetched_posted_events') and self.prefetched_posted_events: + return self.prefetched_posted_events[0].time + else: + event = self.latest_event(type='posted') + if event: + return event.time return None @property @@ -110,7 +109,7 @@ class LiaisonStatement(models.Model): for pending statements this is submitted date""" if self.state_id == 'posted': return self.posted - elif self.state_id == 'pending': + else: return self.submitted @property @@ -126,8 +125,11 @@ class LiaisonStatement(models.Model): @property def action_taken(self): - return self.tags.filter(slug='taken').exists() - + if hasattr(self,'prefetched_tags'): + return bool(self.prefetched_tags) + else: + 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) @@ -138,17 +140,31 @@ class LiaisonStatement(models.Model): return bool(self._awaiting_action) return self.tags.filter(slug='awaiting').exists() + def _get_group_display(self, groups): + '''Returns comma separated string of group acronyms, non-wg are uppercase''' + acronyms = [] + for group in groups: + if group.type.slug == 'wg': + acronyms.append(group.acronym) + else: + acronyms.append(group.acronym.upper()) + return ', '.join(acronyms) + @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) + if hasattr(self, 'prefetched_from_groups'): + return self._get_group_display(self.prefetched_from_groups) + else: + return self._get_group_display(self.from_groups.order_by('acronym')) @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) + if hasattr(self, 'prefetched_to_groups'): + return self._get_group_display(self.prefetched_to_groups) + else: + return self._get_group_display(self.to_groups.order_by('acronym')) def from_groups_short_display(self): '''Returns comma separated list of from_group acronyms. For use in admin diff --git a/ietf/liaisons/tests.py b/ietf/liaisons/tests.py index e5c4bb6d4..cf38b578f 100644 --- a/ietf/liaisons/tests.py +++ b/ietf/liaisons/tests.py @@ -338,6 +338,35 @@ class LiaisonManagementTests(TestCase): r = self.client.get(url) self.assertEqual(r.status_code, 200) + def test_add_comment(self): + make_test_data() + liaison = make_liaison_models() + + # test unauthorized + url = urlreverse('ietf.liaisons.views.liaison_history',kwargs=dict(object_id=liaison.pk)) + addurl = urlreverse('ietf.liaisons.views.add_comment',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('Add Comment')")), 0) + login_testing_unauthorized(self, "secretary", addurl) + + # public comment + self.client.login(username="secretary", password="secretary+password") + comment = 'Test comment' + r = self.client.post(addurl, dict(comment=comment)) + self.assertEqual(r.status_code,302) + qs = liaison.liaisonstatementevent_set.filter(type='comment',desc=comment) + self.assertTrue(qs.count(),1) + + # private comment + r = self.client.post(addurl, dict(comment='Private comment',private=True),follow=True) + self.assertEqual(r.status_code,200) + self.assertTrue('Private comment' in r.content) + self.client.logout() + r = self.client.get(url) + self.assertFalse('Private comment' in r.content) + def test_taken_care_of(self): make_test_data() liaison = make_liaison_models() @@ -721,7 +750,7 @@ class LiaisonManagementTests(TestCase): r = self.client.post(url, dict(from_groups=from_groups, from_contact=submitter.email_address(), - to_groups=str(to_group.pk), + 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", @@ -889,6 +918,7 @@ class LiaisonManagementTests(TestCase): 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() ]), + to_contacts = 'to_contacts@example.com', purpose = liaison.purpose.slug, deadline = liaison.deadline, title = liaison.title, diff --git a/ietf/liaisons/urls.py b/ietf/liaisons/urls.py index e59bc82b2..74a6e70f8 100644 --- a/ietf/liaisons/urls.py +++ b/ietf/liaisons/urls.py @@ -20,8 +20,9 @@ urlpatterns += patterns('ietf.liaisons.views', # Views urlpatterns += patterns('ietf.liaisons.views', (r'^$', 'liaison_list'), - (r'^(?P(posted|pending|dead))/$', 'liaison_list'), + (r'^(?P(posted|pending|dead))/', 'liaison_list'), (r'^(?P\d+)/$', 'liaison_detail'), + (r'^(?P\d+)/addcomment/$', 'add_comment'), (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'), diff --git a/ietf/liaisons/utils.py b/ietf/liaisons/utils.py index 9e77a1e8d..d714a91c1 100644 --- a/ietf/liaisons/utils.py +++ b/ietf/liaisons/utils.py @@ -11,7 +11,7 @@ can_submit_liaison_required = passes_test_decorator( def approvable_liaison_statements(user): '''Returns a queryset of Liaison Statements in pending state that user has authority to approve''' - liaisons = LiaisonStatement.objects.filter(state__slug='pending') + liaisons = LiaisonStatement.objects.filter(state__slug__in=('pending','dead')) person = get_person_for_user(user) if has_role(user, "Secretariat"): return liaisons @@ -27,9 +27,19 @@ def approvable_liaison_statements(user): return liaisons.filter(id__in=approvable_liaisons) def can_edit_liaison(user, liaison): - '''Return True if user is Secretariat or Liaison Manager of all SDO groups involved''' + '''Returns True if user has edit / approval authority. + + True if: + - user is Secretariat + - liaison is outgoing and user has approval authority + - user is liaison manager of all SDOs involved + ''' if has_role(user, "Secretariat"): return True + + if liaison.is_outgoing() and liaison in approvable_liaison_statements(user): + return True + 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')): diff --git a/ietf/liaisons/views.py b/ietf/liaisons/views.py index cc4563cdc..d595d2249 100644 --- a/ietf/liaisons/views.py +++ b/ietf/liaisons/views.py @@ -5,7 +5,7 @@ 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.db.models import Q, Prefetch from django.http import HttpResponse, HttpResponseForbidden from django.shortcuts import render, render_to_response, get_object_or_404, redirect from django.template import RequestContext @@ -18,9 +18,10 @@ from ietf.liaisons.models import (LiaisonStatement,LiaisonStatementEvent, 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.forms import liaison_form_factory, SearchLiaisonForm, EditAttachmentForm, AddCommentForm from ietf.liaisons.mails import notify_pending_by_email, send_liaison_by_email from ietf.liaisons.fields import select2_id_liaison_json +from ietf.name.models import LiaisonStatementTagName EMAIL_ALIASES = { 'IETFCHAIR':'The IETF Chair ', @@ -287,6 +288,32 @@ def redirect_for_approval(request, object_id=None): # ------------------------------------------------- # View Functions # ------------------------------------------------- +@role_required('Secretariat',) +def add_comment(request, object_id): + """Add comment to history""" + statement = get_object_or_404(LiaisonStatement, id=object_id) + login = request.user.person + + if request.method == 'POST': + form = AddCommentForm(request.POST) + if form.is_valid(): + if form.cleaned_data.get('private'): + type_id = 'private_comment' + else: + type_id = 'comment' + + LiaisonStatementEvent.objects.create( + by=login, + type_id=type_id, + statement=statement, + desc=form.cleaned_data['comment'] + ) + messages.success(request, 'Comment added.') + return redirect("ietf.liaisons.views.liaison_history", object_id=statement.id) + else: + form = AddCommentForm() + + return render(request, 'liaisons/add_comment.html',dict(liaison=statement,form=form)) @can_submit_liaison_required def liaison_add(request, type=None, **kwargs): @@ -303,7 +330,10 @@ def liaison_add(request, type=None, **kwargs): liaison = form.save() # notifications - if 'send' in request.POST and liaison.state.slug == 'posted': + if 'save' in request.POST: + # the result of an edit, no notifications necessary + messages.success(request, 'The statement has been updated') + elif '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': @@ -326,6 +356,8 @@ 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") + if not has_role(request.user, "Secretariat"): + events = events.exclude(type='private_comment') return render(request, "liaisons/detail_history.html", { 'events':events, @@ -434,32 +466,47 @@ def liaison_edit_attachment(request, object_id, doc_id): def liaison_list(request, state='posted'): """A generic list view with tabs for different states: posted, pending, dead""" - liaisons = LiaisonStatement.objects.filter(state=state) - + # use prefetch to speed up main liaison page load + selected_menu_entry = state + liaisons = LiaisonStatement.objects.filter(state=state).prefetch_related( + Prefetch('from_groups',queryset=Group.objects.order_by('acronym').select_related('type'),to_attr='prefetched_from_groups'), + Prefetch('to_groups',queryset=Group.objects.order_by('acronym').select_related('type'),to_attr='prefetched_to_groups'), + Prefetch('tags',queryset=LiaisonStatementTagName.objects.filter(slug='taken'),to_attr='prefetched_tags'), + Prefetch('liaisonstatementevent_set',queryset=LiaisonStatementEvent.objects.filter(type='posted'),to_attr='prefetched_posted_events') + ) + # 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) + if 'tags' in request.GET: + value = request.GET.get('tags') + liaisons = liaisons.filter(tags__slug=value) + selected_menu_entry = 'action needed' + # perform search / filter if 'text' in request.GET: - form = SearchLiaisonForm(data=request.GET) + form = SearchLiaisonForm(data=request.GET,queryset=liaisons) search_conducted = True if form.is_valid(): results = form.get_results() liaisons = results else: - form = SearchLiaisonForm() + form = SearchLiaisonForm(queryset=liaisons) 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) + liaisons = sorted(liaisons, key=lambda a: a.sort_date, reverse=True) + liaisons = sorted(liaisons, key=lambda a: a.from_groups_display.lower()) if sort == 'to_groups': - liaisons = sorted(liaisons, key=lambda a: a.to_groups_display) + liaisons = sorted(liaisons, key=lambda a: a.sort_date, reverse=True) + liaisons = sorted(liaisons, key=lambda a: a.to_groups_display.lower()) if sort == 'deadline': liaisons = liaisons.order_by('-deadline') if sort == 'title': @@ -469,6 +516,7 @@ def liaison_list(request, state='posted'): entries = [] entries.append(("Posted", urlreverse("ietf.liaisons.views.liaison_list", kwargs={'state':'posted'}))) if can_add_liaison(request.user): + entries.append(("Action Needed", urlreverse("ietf.liaisons.views.liaison_list", kwargs={'state':'posted'}) + '?tags=required')) entries.append(("Pending", urlreverse("ietf.liaisons.views.liaison_list", kwargs={'state':'pending'}))) entries.append(("Dead", urlreverse("ietf.liaisons.views.liaison_list", kwargs={'state':'dead'}))) @@ -481,7 +529,7 @@ def liaison_list(request, state='posted'): return render(request, 'liaisons/liaison_base.html', { 'liaisons':liaisons, - 'selected_menu_entry':state, + 'selected_menu_entry':selected_menu_entry, 'menu_entries':entries, 'menu_actions':actions, 'sort':sort, @@ -501,6 +549,7 @@ def liaison_reply(request,object_id): initial = dict( to_groups=[ x.pk for x in liaison.from_groups.all() ], from_groups=[ x.pk for x in liaison.to_groups.all() ], + to_contacts=liaison.response_contacts, related_to=str(liaison.pk)) return liaison_add(request,type=reply_type,initial=initial) diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index 8baaddb78..e270b992f 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -1580,6 +1580,16 @@ "model": "name.liaisonstatementeventtypename", "pk": "comment" }, +{ + "fields": { + "order": 10, + "used": true, + "name": "Private Comment", + "desc": "" + }, + "model": "name.liaisonstatementeventtypename", + "pk": "private_comment" +}, { "fields": { "order": 1, diff --git a/ietf/name/migrations/0010_new_liaison_names.py b/ietf/name/migrations/0010_new_liaison_names.py new file mode 100644 index 000000000..0123ae53d --- /dev/null +++ b/ietf/name/migrations/0010_new_liaison_names.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations + +def populate_names(apps, schema_editor): + LiaisonStatementEventTypeName = apps.get_model("name", "LiaisonStatementEventTypeName") + LiaisonStatementEventTypeName.objects.create(slug="private_comment", order=10, name="Private Comment") + + +class Migration(migrations.Migration): + + dependencies = [ + ('name', '0009_auto_20151021_1102'), + ] + + operations = [ + migrations.RunPython(populate_names), + ] diff --git a/ietf/static/ietf/css/ietf.css b/ietf/static/ietf/css/ietf.css index 41831c26b..a1700fc63 100644 --- a/ietf/static/ietf/css/ietf.css +++ b/ietf/static/ietf/css/ietf.css @@ -320,6 +320,10 @@ ampersand you get is dependent on which fonts are available in the browser. Hac /* misc pages */ +.liaison-group-col { + min-width: 10em; +} + #reset-charter-milestones .date { display: inline-block; min-width: 5em; diff --git a/ietf/static/ietf/css/liaisons.css b/ietf/static/ietf/css/liaisons.css index 0a1586f90..02e34bb52 100644 --- a/ietf/static/ietf/css/liaisons.css +++ b/ietf/static/ietf/css/liaisons.css @@ -128,4 +128,8 @@ input[id$='DELETE'] { #id_to_groups + span { display: none; +} + +.liaison-group-col { + min-width: 10em; } \ No newline at end of file diff --git a/ietf/static/ietf/js/liaisons.js b/ietf/static/ietf/js/liaisons.js index 60b7c5c72..b4c111afc 100644 --- a/ietf/static/ietf/js/liaisons.js +++ b/ietf/static/ietf/js/liaisons.js @@ -124,6 +124,7 @@ var attachmentWidget = { var liaisonForm = { initVariables : function() { + liaisonForm.is_edit_form = liaisonForm.form.attr("data-edit-form") == "True" 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'); @@ -189,6 +190,11 @@ var liaisonForm = { }, updateInfo : function(first_time, sender) { + // don't overwrite fields when editing existing liaison + if(liaisonForm.is_edit_form){ + return false; + } + var from_ids = liaisonForm.from_groups.val(); var to_ids = liaisonForm.to_groups.val(); var url = liaisonForm.form.data("ajaxInfoUrl"); @@ -278,26 +284,12 @@ 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; - } - }); - } - return advanced; + $("#search-clear-btn").bind("click", searchForm.clearForm); }, - toggleSubmit : function() { - var textSearch = $.trim($("#id_text").val()); - searchForm.form.find("button[type=submit]").get(0).disabled = !textSearch && !searchForm.anyAdvancedActive(); + clearForm : function() { + var form = $(this).parents("form"); + form.find("input").val(""); } } @@ -307,5 +299,5 @@ $(document).ready(function () { $.ajaxSetup({ traditional: true }); $('form.liaisons-form').each(liaisonForm.init); - $('#search_form').each(searchForm.init); + $('#liaison_search_form').each(searchForm.init); }); diff --git a/ietf/templates/liaisons/add_comment.html b/ietf/templates/liaisons/add_comment.html new file mode 100644 index 000000000..6debe0f14 --- /dev/null +++ b/ietf/templates/liaisons/add_comment.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} +{# Copyright The IETF Trust 2015, All Rights Reserved #} +{% load origin %} + +{% load bootstrap3 %} + +{% block title %}Add comment on {{ liaison.title }}{% endblock %} + +{% block content %} + {% origin %} +

Add comment
{{ liaison.title }}

+ +

The comment will be added to the history trail of the statement.

+ + {% bootstrap_messages %} + +
+ {% csrf_token %} + + {% bootstrap_form form %} + + {% buttons %} + + Back + {% endbuttons %} +
+{% endblock %} diff --git a/ietf/templates/liaisons/detail.html b/ietf/templates/liaisons/detail.html index 594fab20f..0bcd2f5bf 100644 --- a/ietf/templates/liaisons/detail.html +++ b/ietf/templates/liaisons/detail.html @@ -21,12 +21,21 @@ {{ liaison.state }} - Submission Date - {{ liaison.submitted|date:"Y-m-d" }} + {% if liaison.state.slug == "posted" %} + Posted Date + {{ liaison.posted|date:"Y-m-d" }} + {% else %} + Submitted Date + {{ liaison.submitted|date:"Y-m-d" }} + {% endif %} + + From Group{{ liaison.from_groups.all|pluralize }} + {{ liaison.from_groups_display }} + {% if liaison.from_contact %} - Sender + From Contact {{ liaison.from_contact.person }} @@ -34,15 +43,19 @@ {% endif %} - From - {{ liaison.from_groups_display }} - - - - To + To Group{{ liaison.to_groups.all|pluralize }} {{ liaison.to_groups_display }} +{% if liaison.to_contacts %} + + To Contacts + + {{ liaison.to_contacts|parse_email_list|make_one_per_line|safe|linebreaksbr }} + + +{% endif %} + {% if liaison.cc_contacts %} Cc{{ liaison.cc_contacts|parse_email_list|make_one_per_line|safe|linebreaksbr }} @@ -177,7 +190,9 @@
{% csrf_token %} - {% if liaison.state.slug != 'dead' and can_edit %} + {% if liaison.state.slug == 'pending' and can_edit %} + Edit liaison + {% elif liaison.state.slug == 'posted' and user|has_role:"Secretariat" %} Edit liaison {% endif %} {% if liaison.state.slug != 'dead' and can_reply %} diff --git a/ietf/templates/liaisons/detail_history.html b/ietf/templates/liaisons/detail_history.html index af73f9ecc..9749dc9b7 100644 --- a/ietf/templates/liaisons/detail_history.html +++ b/ietf/templates/liaisons/detail_history.html @@ -10,14 +10,11 @@ {% include "liaisons/detail_tabs.html" %} - {% comment %} {% if user|has_role:"Area Director,Secretariat,IANA,RFC Editor" %}

- Add comment - Add email + Add comment

{% endif %} - {% endcomment %} diff --git a/ietf/templates/liaisons/edit.html b/ietf/templates/liaisons/edit.html index 1dee7f659..a7a690f8c 100644 --- a/ietf/templates/liaisons/edit.html +++ b/ietf/templates/liaisons/edit.html @@ -43,7 +43,7 @@

Fields marked with are required. For detailed descriptions of the fields see the field help.

{% endif %} - {% csrf_token %} + {% csrf_token %} {% for fieldset in form.fieldsets %} {% if forloop.first and user|has_role:"Secretariat" %} diff --git a/ietf/templates/liaisons/liaison_base.html b/ietf/templates/liaisons/liaison_base.html index b6a028e4e..02a4d875a 100644 --- a/ietf/templates/liaisons/liaison_base.html +++ b/ietf/templates/liaisons/liaison_base.html @@ -47,6 +47,8 @@ {% endblock group_content %} +

Total Statements: {{ liaisons|length }}

+ {% endblock content %} {% block js %} diff --git a/ietf/templates/liaisons/liaison_mail.txt b/ietf/templates/liaisons/liaison_mail.txt index 12f1c6283..8adcfad78 100644 --- a/ietf/templates/liaisons/liaison_mail.txt +++ b/ietf/templates/liaisons/liaison_mail.txt @@ -2,9 +2,9 @@ Submission Date: {{ liaison.submitted|date:"Y-m-d" }} 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 }} <{% if liaison.from_contact %}{{ liaison.from_contact.address }}{% endif %}>) -To: {{ liaison.to_name }} ({{ liaison.to_contacts }}) -Cc: {{ liaison.cc }} +From: {% if liaison.from_contact %}{{ liaison.from_contact.formatted_email }}{% endif %} +To: {{ liaison.to_contacts }} +Cc: {{ liaison.cc_contacts }} Response Contacts: {{ liaison.response_contacts }} Technical Contacts: {{ liaison.technical_contacts }} Purpose: {{ liaison.purpose.name }} diff --git a/ietf/templates/liaisons/liaison_table.html b/ietf/templates/liaisons/liaison_table.html index f1a9df3d3..fecb0020d 100644 --- a/ietf/templates/liaisons/liaison_table.html +++ b/ietf/templates/liaisons/liaison_table.html @@ -27,19 +27,13 @@ {% for liaison in liaisons %}

- - - - + + + + {% endfor %} diff --git a/ietf/templates/liaisons/search_form.html b/ietf/templates/liaisons/search_form.html index 65d368966..9fd569571 100644 --- a/ietf/templates/liaisons/search_form.html +++ b/ietf/templates/liaisons/search_form.html @@ -3,7 +3,7 @@ {% load ietf_filters %} {% load bootstrap3 %} - +
{{ form.text|add_class:"form-control"|attr:"placeholder:Title, body, identifiers, etc." }} @@ -32,7 +32,7 @@
- +
{{ form.source|add_class:"form-control" }} @@ -41,7 +41,7 @@
- +
{{ form.destination|add_class:"form-control" }} @@ -70,7 +70,7 @@
- +
{{ 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 %} -
{{ 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 %}
{{ liaison.title }}