Merged in^/personal/rcross/v6.7.3.dev0@10465, which included [10382],[10464],[10465], introducing a number of usability fixes to the liaison tool, including a new tab for action needed statements and the ability to add comments to liaison statements' history.
- Legacy-Id: 10506 Note: SVN reference [10382] has been migrated to Git commitb7bbfd8312
Note: SVN reference [10464] has been migrated to Git commit1ea5dcf907
Note: SVN reference [10465] has been migrated to Git commiteed28dbb49
This commit is contained in:
commit
6ab5365296
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
20
ietf/liaisons/migrations/0008_auto_20151110_1352.py
Normal file
20
ietf/liaisons/migrations/0008_auto_20151110_1352.py
Normal file
|
@ -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),
|
||||
]
|
22
ietf/liaisons/migrations/0009_remove_fields.py
Normal file
22
ietf/liaisons/migrations/0009_remove_fields.py
Normal file
|
@ -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',
|
||||
),
|
||||
]
|
|
@ -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"<no title>"
|
||||
|
||||
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -20,8 +20,9 @@ urlpatterns += patterns('ietf.liaisons.views',
|
|||
# Views
|
||||
urlpatterns += patterns('ietf.liaisons.views',
|
||||
(r'^$', 'liaison_list'),
|
||||
(r'^(?P<state>(posted|pending|dead))/$', 'liaison_list'),
|
||||
(r'^(?P<state>(posted|pending|dead))/', 'liaison_list'),
|
||||
(r'^(?P<object_id>\d+)/$', 'liaison_detail'),
|
||||
(r'^(?P<object_id>\d+)/addcomment/$', 'add_comment'),
|
||||
(r'^(?P<object_id>\d+)/edit/$', 'liaison_edit'),
|
||||
(r'^(?P<object_id>\d+)/edit-attachment/(?P<doc_id>[A-Za-z0-9._+-]+)$', 'liaison_edit_attachment'),
|
||||
(r'^(?P<object_id>\d+)/delete-attachment/(?P<attach_id>[A-Za-z0-9._+-]+)$', 'liaison_delete_attachment'),
|
||||
|
|
|
@ -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')):
|
||||
|
|
|
@ -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 <chair@ietf.org>',
|
||||
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
19
ietf/name/migrations/0010_new_liaison_names.py
Normal file
19
ietf/name/migrations/0010_new_liaison_names.py
Normal file
|
@ -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),
|
||||
]
|
|
@ -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;
|
||||
|
|
|
@ -128,4 +128,8 @@ input[id$='DELETE'] {
|
|||
|
||||
#id_to_groups + span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.liaison-group-col {
|
||||
min-width: 10em;
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
|
|
27
ietf/templates/liaisons/add_comment.html
Normal file
27
ietf/templates/liaisons/add_comment.html
Normal file
|
@ -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 %}
|
||||
<h1>Add comment<br><small>{{ liaison.title }}</small></h1>
|
||||
|
||||
<p>The comment will be added to the history trail of the statement.</p>
|
||||
|
||||
{% bootstrap_messages %}
|
||||
|
||||
<form class="add-comment" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{% bootstrap_form form %}
|
||||
|
||||
{% buttons %}
|
||||
<button type="submit" class="btn btn-primary">Add Comment</button>
|
||||
<a class="btn btn-default pull-right" href="{% url "ietf.liaisons.views.liaison_history" object_id=liaison.id %}">Back</a>
|
||||
{% endbuttons %}
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -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 %}
|
||||
|
|
|
@ -10,14 +10,11 @@
|
|||
|
||||
{% include "liaisons/detail_tabs.html" %}
|
||||
|
||||
{% comment %}
|
||||
{% if user|has_role:"Area Director,Secretariat,IANA,RFC Editor" %}
|
||||
<p class="buttonlist">
|
||||
<a class="btn btn-default" href="{% url "ipr_add_comment" id=ipr.id %}" title="Add comment to history">Add comment</a>
|
||||
<a class="btn btn-default" href="{% url "ipr_add_email" id=ipr.id %}" title="Add email to history">Add email</a>
|
||||
<a class="btn btn-default" href="{% url "ietf.liaisons.views.add_comment" object_id=liaison.id %}" title="Add comment to history">Add comment</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endcomment %}
|
||||
|
||||
<table class="table table-condensed table-striped history">
|
||||
<thead>
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
<p class="help-block">Fields marked with <label class="required"></label> are required. For detailed descriptions of the fields see the <a href="{% url "liaisons_field_help" %}">field help</a>.</p>
|
||||
{% endif %}
|
||||
|
||||
<form role="form" class="liaisons-form form-horizontal show-required" method="post" enctype="multipart/form-data" data-ajax-info-url="{% url "ietf.liaisons.views.ajax_get_liaison_info" %}">{% csrf_token %}
|
||||
<form role="form" class="liaisons-form form-horizontal show-required" method="post" enctype="multipart/form-data" data-edit-form="{{ form.edit }}" data-ajax-info-url="{% url "ietf.liaisons.views.ajax_get_liaison_info" %}">{% csrf_token %}
|
||||
|
||||
{% for fieldset in form.fieldsets %}
|
||||
{% if forloop.first and user|has_role:"Secretariat" %}
|
||||
|
|
|
@ -47,6 +47,8 @@
|
|||
|
||||
{% endblock group_content %}
|
||||
|
||||
<p>Total Statements: {{ liaisons|length }}<p>
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
{% block js %}
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -27,19 +27,13 @@
|
|||
{% for liaison in liaisons %}
|
||||
<tr>
|
||||
<td class="text-nowrap">{{ liaison.sort_date|date:"Y-m-d" }}</td>
|
||||
<td>{{ liaison.from_groups_display }}</td>
|
||||
<td>{{ liaison.to_groups_display }}</td>
|
||||
<td class="text-nowrap">{{ liaison.deadline|default:"-"|date:"Y-m-d" }}{% if liaison.deadline and not liaison.action_taken %}<br><span class="label label-warning">Action Needed</span>{% endif %}</td>
|
||||
<td>
|
||||
{% if not liaison.from_contact_id %}
|
||||
{% for doc in liaison.attachments.all %}
|
||||
<a href="{{ doc.href }}">{{ doc.title }}</a>
|
||||
<br>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<a href="{% url "ietf.liaisons.views.liaison_detail" object_id=liaison.pk %}">{{ liaison.title }}</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<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 {% if liaison.is_outgoing %}label-warning{% else %}label-info{% endif %}">Action Needed</span>
|
||||
{% endif %}</td>
|
||||
<td><a href="{% url "ietf.liaisons.views.liaison_detail" object_id=liaison.pk %}">{{ liaison.title }}</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
{% load ietf_filters %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
<form id="search_form" class="form-horizontal" action="{% url "ietf.liaisons.views.liaison_list" state=state %}" method="get">
|
||||
<form id="liaison_search_form" class="form-horizontal" action="{% url "ietf.liaisons.views.liaison_list" state=state %}" method="get">
|
||||
|
||||
<div class="input-group search_field">
|
||||
{{ form.text|add_class:"form-control"|attr:"placeholder:Title, body, identifiers, etc." }}
|
||||
|
@ -32,7 +32,7 @@
|
|||
|
||||
<div class="form-group search_field">
|
||||
<div class="col-sm-4">
|
||||
<label for="id_source" class="control-label">Source</label>
|
||||
<label for="id_source" class="control-label">From Group(s)</label>
|
||||
</div>
|
||||
<div class="col-sm-8">
|
||||
{{ form.source|add_class:"form-control" }}
|
||||
|
@ -41,7 +41,7 @@
|
|||
|
||||
<div class="form-group search_field">
|
||||
<div class="col-sm-4">
|
||||
<label for="id_destination" class="control-label">Destination</label>
|
||||
<label for="id_destination" class="control-label">To Group(s)</label>
|
||||
</div>
|
||||
<div class="col-sm-8">
|
||||
{{ form.destination|add_class:"form-control" }}
|
||||
|
@ -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">
|
||||
|
|
Loading…
Reference in a new issue