Various small bug fixes and tweaks to UI of Liaison Management Tool. Commit ready for merge

- Legacy-Id: 10382
This commit is contained in:
Ryan Cross 2015-11-03 07:52:05 +00:00
parent 79c49094e2
commit b7bbfd8312
13 changed files with 149 additions and 56 deletions

View file

@ -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):
@ -126,15 +134,25 @@ 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.state = kwargs.pop('state')
super(SearchLiaisonForm, self).__init__(*args, **kwargs)
def get_results(self):
results = LiaisonStatement.objects.filter(state__slug='posted')
results = LiaisonStatement.objects.filter(state=self.state)
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')
@ -209,7 +227,8 @@ class LiaisonModelForm(BetterModelForm):
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:
@ -408,7 +427,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

View file

@ -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 body")
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
@ -92,9 +92,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 +113,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 +129,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 +144,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

View file

@ -889,6 +889,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,

View file

@ -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')):

View file

@ -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
@ -21,6 +21,7 @@ from ietf.liaisons.utils import (get_person_for_user, can_add_outgoing_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
from ietf.name.models import LiaisonStatementTagName
EMAIL_ALIASES = {
'IETFCHAIR':'The IETF Chair <chair@ietf.org>',
@ -303,7 +304,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':
@ -434,8 +438,14 @@ 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
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"
@ -443,23 +453,26 @@ def liaison_list(request, state='posted'):
# perform search / filter
if 'text' in request.GET:
form = SearchLiaisonForm(data=request.GET)
form = SearchLiaisonForm(data=request.GET,state=state)
search_conducted = True
if form.is_valid():
results = form.get_results()
liaisons = results
else:
form = SearchLiaisonForm()
form = SearchLiaisonForm(state=state)
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':
@ -501,6 +514,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)

View file

@ -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;

View file

@ -128,4 +128,8 @@ input[id$='DELETE'] {
#id_to_groups + span {
display: none;
}
.liaison-group-col {
min-width: 10em;
}

View file

@ -279,6 +279,7 @@ var searchForm = {
init : function() {
searchForm.form = $(this);
searchForm.form.find(".search_field input,select").change(searchForm.toggleSubmit).click(searchForm.toggleSubmit).keyup(searchForm.toggleSubmit);
$("#search-clear-btn").bind("click", searchForm.clearForm);
},
anyAdvancedActive : function() {
@ -298,6 +299,11 @@ var searchForm = {
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("");
}
}

View file

@ -21,12 +21,21 @@
<td>{{ liaison.state }}</td>
</tr>
<tr>
<th class="text-nowrap">Submission Date</th>
<td>{{ liaison.submitted|date:"Y-m-d" }}</td></tr>
{% if liaison.state.slug == "posted" %}
<th class="text-nowrap">Posted Date</th>
<td>{{ liaison.posted|date:"Y-m-d" }}</td></tr>
{% else %}
<th class="text-nowrap">Submitted Date</th>
<td>{{ liaison.submitted|date:"Y-m-d" }}</td></tr>
{% endif %}
<tr>
<th class="text-nowrap">From Group{{ liaison.from_groups.all|pluralize }}</th>
<td>{{ liaison.from_groups_display }}</td>
</tr>
{% if liaison.from_contact %}
<tr>
<th class="text-nowrap">Sender</th>
<th class="text-nowrap">From Contact</th>
<td>
<a href="mailto:{{ liaison.from_contact.address }}">{{ liaison.from_contact.person }}</a>
</td>
@ -34,15 +43,19 @@
{% endif %}
<tr>
<th class="text-nowrap">From</th>
<td>{{ liaison.from_groups_display }}</td>
</tr>
<tr>
<th class="text-nowrap">To</th>
<th class="text-nowrap">To Group{{ liaison.to_groups.all|pluralize }}</th>
<td>{{ liaison.to_groups_display }}</td>
</tr>
{% if liaison.to_contacts %}
<tr>
<th class="text-nowrap">To Contacts</th>
<td>
{{ liaison.to_contacts|parse_email_list|make_one_per_line|safe|linebreaksbr }}
</td>
</tr>
{% endif %}
{% if liaison.cc_contacts %}
<tr>
<th class="text-nowrap">Cc</th><td>{{ liaison.cc_contacts|parse_email_list|make_one_per_line|safe|linebreaksbr }}</td>
@ -177,7 +190,9 @@
<form method="post">
{% csrf_token %}
{% if liaison.state.slug != 'dead' and can_edit %}
{% if liaison.state.slug == 'pending' and can_edit %}
<a class="btn btn-default" href="{% url "ietf.liaisons.views.liaison_edit" object_id=liaison.pk %}">Edit liaison</a>
{% elif liaison.state.slug == 'posted' and user|has_role:"Secretariat" %}
<a class="btn btn-default" href="{% url "ietf.liaisons.views.liaison_edit" object_id=liaison.pk %}">Edit liaison</a>
{% endif %}
{% if liaison.state.slug != 'dead' and can_reply %}

View file

@ -47,6 +47,8 @@
{% endblock group_content %}
<p>Total Statements: {{ liaisons|length }}<p>
{% endblock content %}
{% block js %}

View file

@ -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 }}

View file

@ -27,19 +27,10 @@
{% for liaison in liaisons %}
<tr>
<td class="text-nowrap">{{ liaison.sort_date|date:"Y-m-d" }}</td>
<td>{{ liaison.from_groups_display }}</td>
<td>{{ liaison.to_groups_display }}</td>
<td class="liaison-group-col">{{ liaison.from_groups_display }}</td>
<td class="liaison-group-col">{{ liaison.to_groups_display }}</td>
<td class="text-nowrap">{{ liaison.deadline|default:"-"|date:"Y-m-d" }}{% if liaison.deadline and not liaison.action_taken %}<br><span class="label label-warning">Action Needed</span>{% endif %}</td>
<td>
{% if not liaison.from_contact_id %}
{% for doc in liaison.attachments.all %}
<a href="{{ doc.href }}">{{ doc.title }}</a>
<br>
{% endfor %}
{% else %}
<a href="{% url "ietf.liaisons.views.liaison_detail" object_id=liaison.pk %}">{{ liaison.title }}</a>
{% endif %}
</td>
<td><a href="{% url "ietf.liaisons.views.liaison_detail" object_id=liaison.pk %}">{{ liaison.title }}</a></td>
</tr>
{% endfor %}
</tbody>

View file

@ -70,7 +70,7 @@
<div class="form-group search_field">
<div class="col-md-offset-4 col-sm-4">
<button class="btn btn-default btn-block" type="reset">Clear</button>
<button id="search-clear-btn" class="btn btn-default btn-block" type="button">Clear</button>
</div>
<div class="col-sm-4">
<button class="btn btn-primary btn-block" type="submit">