Skip to content

Commit cebc979

Browse files
committed
Summary: Resolve person/email/document multiselect issue by importing
select2 and switching the widgets over to using that. Port the milestones editing page to Bootstrap. - Legacy-Id: 8713
1 parent c7342d2 commit cebc979

37 files changed

Lines changed: 1856 additions & 519 deletions

ietf/doc/fields.py

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,16 @@
88

99
from ietf.doc.models import Document, DocAlias
1010

11-
def tokeninput_id_doc_name_json(objs):
12-
return json.dumps([{ "id": o.pk, "name": escape(o.name) } for o in objs])
11+
def select2_id_doc_name_json(objs):
12+
return json.dumps([{ "id": o.pk, "text": escape(o.name) } for o in objs])
1313

14-
class AutocompletedDocumentsField(forms.CharField):
15-
"""Tokenizing autocompleted multi-select field for choosing
16-
documents using jquery.tokeninput.js.
14+
class SearchableDocumentsField(forms.CharField):
15+
"""Server-based multi-select field for choosing documents using
16+
select2.js.
1717
1818
The field uses a comma-separated list of primary keys in a
19-
CharField element as its API, the tokeninput Javascript adds some
20-
selection magic on top of this so we have to pass it a JSON
21-
representation of ids and user-understandable labels."""
19+
CharField element as its API with some extra attributes used by
20+
the Javascript part."""
2221

2322
def __init__(self,
2423
max_entries=None, # max number of selected objs
@@ -31,39 +30,45 @@ def __init__(self,
3130
self.doc_type = doc_type
3231
self.model = model
3332

34-
super(AutocompletedDocumentsField, self).__init__(*args, **kwargs)
33+
super(SearchableDocumentsField, self).__init__(*args, **kwargs)
3534

36-
self.widget.attrs["class"] = "tokenized-field"
37-
self.widget.attrs["data-hint-text"] = hint_text
35+
self.widget.attrs["class"] = "select2-field"
36+
self.widget.attrs["data-placeholder"] = hint_text
3837
if self.max_entries != None:
3938
self.widget.attrs["data-max-entries"] = self.max_entries
4039

41-
def parse_tokenized_value(self, value):
40+
def parse_select2_value(self, value):
4241
return [x.strip() for x in value.split(",") if x.strip()]
4342

4443
def prepare_value(self, value):
4544
if not value:
4645
value = ""
4746
if isinstance(value, basestring):
48-
pks = self.parse_tokenized_value(value)
49-
value = self.model.objects.filter(pk__in=pks, type=self.doc_type)
47+
pks = self.parse_select2_value(value)
48+
value = self.model.objects.filter(pk__in=pks)
49+
filter_args = {}
50+
if self.model == DocAlias:
51+
filter_args["document__type"] = self.doc_type
52+
else:
53+
filter_args["type"] = self.doc_type
54+
value = value.filter(**filter_args)
5055
if isinstance(value, self.model):
5156
value = [value]
5257

53-
self.widget.attrs["data-pre"] = tokeninput_id_doc_name_json(value)
58+
self.widget.attrs["data-pre"] = select2_id_doc_name_json(value)
5459

5560
# doing this in the constructor is difficult because the URL
5661
# patterns may not have been fully constructed there yet
57-
self.widget.attrs["data-ajax-url"] = urlreverse("ajax_tokeninput_search_docs", kwargs={
62+
self.widget.attrs["data-ajax-url"] = urlreverse("ajax_select2_search_docs", kwargs={
5863
"doc_type": self.doc_type,
5964
"model_name": self.model.__name__.lower()
6065
})
6166

62-
return ",".join(o.pk for o in value)
67+
return u",".join(unicode(o.pk) for o in value)
6368

6469
def clean(self, value):
65-
value = super(AutocompletedDocumentsField, self).clean(value)
66-
pks = self.parse_tokenized_value(value)
70+
value = super(SearchableDocumentsField, self).clean(value)
71+
pks = self.parse_select2_value(value)
6772

6873
objs = self.model.objects.filter(pk__in=pks)
6974

@@ -77,7 +82,7 @@ def clean(self, value):
7782

7883
return objs
7984

80-
class AutocompletedDocAliasField(AutocompletedDocumentsField):
85+
class SearchableDocAliasesField(SearchableDocumentsField):
8186
def __init__(self, model=DocAlias, *args, **kwargs):
82-
super(AutocompletedDocAliasField, self).__init__(model=model, *args, **kwargs)
87+
super(SearchableDocAliasesField, self).__init__(model=model, *args, **kwargs)
8388

ietf/doc/tests.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def test_ajax_search_docs(self):
127127
draft = make_test_data()
128128

129129
# Document
130-
url = urlreverse("ajax_tokeninput_search_docs", kwargs={
130+
url = urlreverse("ajax_select2_search_docs", kwargs={
131131
"model_name": "document",
132132
"doc_type": "draft",
133133
})
@@ -139,7 +139,7 @@ def test_ajax_search_docs(self):
139139
# DocAlias
140140
doc_alias = draft.docalias_set.get()
141141

142-
url = urlreverse("ajax_tokeninput_search_docs", kwargs={
142+
url = urlreverse("ajax_select2_search_docs", kwargs={
143143
"model_name": "docalias",
144144
"doc_type": "draft",
145145
})

ietf/doc/tests_draft.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1227,32 +1227,28 @@ def test_change_replaces(self):
12271227

12281228
# Post that says replacea replaces base a
12291229
self.assertEqual(self.basea.get_state().slug,'active')
1230-
repljson='{"%d":"%s"}'%(DocAlias.objects.get(name=self.basea.name).id,self.basea.name)
1231-
r = self.client.post(url, dict(replaces=repljson))
1230+
r = self.client.post(url, dict(replaces=str(DocAlias.objects.get(name=self.basea.name).id)))
12321231
self.assertEqual(r.status_code, 302)
12331232
self.assertEqual(RelatedDocument.objects.filter(relationship__slug='replaces',source=self.replacea).count(),1)
12341233
self.assertEqual(Document.objects.get(name='draft-test-base-a').get_state().slug,'repl')
12351234

12361235
# Post that says replaceboth replaces both base a and base b
12371236
url = urlreverse('doc_change_replaces', kwargs=dict(name=self.replaceboth.name))
12381237
self.assertEqual(self.baseb.get_state().slug,'expired')
1239-
repljson='{"%d":"%s","%d":"%s"}'%(DocAlias.objects.get(name=self.basea.name).id,self.basea.name,
1240-
DocAlias.objects.get(name=self.baseb.name).id,self.baseb.name)
1241-
r = self.client.post(url, dict(replaces=repljson))
1238+
r = self.client.post(url, dict(replaces=str(DocAlias.objects.get(name=self.basea.name).id) + "," + str(DocAlias.objects.get(name=self.baseb.name).id)))
12421239
self.assertEqual(r.status_code, 302)
12431240
self.assertEqual(Document.objects.get(name='draft-test-base-a').get_state().slug,'repl')
12441241
self.assertEqual(Document.objects.get(name='draft-test-base-b').get_state().slug,'repl')
12451242

12461243
# Post that undoes replaceboth
1247-
repljson='{}'
1248-
r = self.client.post(url, dict(replaces=repljson))
1244+
r = self.client.post(url, dict(replaces=""))
12491245
self.assertEqual(r.status_code, 302)
12501246
self.assertEqual(Document.objects.get(name='draft-test-base-a').get_state().slug,'repl') # Because A is still also replaced by replacea
12511247
self.assertEqual(Document.objects.get(name='draft-test-base-b').get_state().slug,'expired')
12521248

12531249
# Post that undoes replacea
12541250
url = urlreverse('doc_change_replaces', kwargs=dict(name=self.replacea.name))
1255-
r = self.client.post(url, dict(replaces=repljson))
1251+
r = self.client.post(url, dict(replaces=""))
12561252
self.assertEqual(r.status_code, 302)
12571253
self.assertEqual(Document.objects.get(name='draft-test-base-a').get_state().slug,'active')
12581254

ietf/doc/urls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949

5050
url(r'^all/$', views_search.index_all_drafts, name="index_all_drafts"),
5151
url(r'^active/$', views_search.index_active_drafts, name="index_active_drafts"),
52-
url(r'^tokeninputsearch/(?P<model_name>(document|docalias))/(?P<doc_type>draft)/$', views_search.ajax_tokeninput_search_docs, name="ajax_tokeninput_search_docs"),
52+
url(r'^select2search/(?P<model_name>(document|docalias))/(?P<doc_type>draft)/$', views_search.ajax_select2_search_docs, name="ajax_select2_search_docs"),
5353

5454
url(r'^(?P<name>[A-Za-z0-9._+-]+)/(?:(?P<rev>[0-9-]+)/)?$', views_doc.document_main, name="doc_view"),
5555
url(r'^(?P<name>[A-Za-z0-9._+-]+)/history/$', views_doc.document_history, name="doc_history"),

ietf/doc/views_draft.py

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# changing state and metadata on Internet Drafts
22

3-
import datetime, json
3+
import datetime
44

55
from django import forms
66
from django.http import HttpResponseRedirect, HttpResponseForbidden, Http404
@@ -24,13 +24,14 @@
2424
get_tags_for_stream_id, nice_consensus,
2525
update_reminder, update_telechat, make_notify_changed_event, get_initial_notify )
2626
from ietf.doc.lastcall import request_last_call
27+
from ietf.doc.fields import SearchableDocAliasesField
2728
from ietf.group.models import Group, Role
2829
from ietf.iesg.models import TelechatDate
2930
from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream, user_is_person
3031
from ietf.ietfauth.utils import role_required
3132
from ietf.message.models import Message
3233
from ietf.name.models import IntendedStdLevelName, DocTagName, StreamName
33-
from ietf.person.fields import AutocompletedEmailField
34+
from ietf.person.fields import SearchableEmailField
3435
from ietf.person.models import Person, Email
3536
from ietf.secr.lib.template import jsonapi
3637
from ietf.utils.mail import send_mail, send_mail_message
@@ -307,35 +308,21 @@ def collect_email_addresses(emails, doc):
307308
return emails
308309

309310
class ReplacesForm(forms.Form):
310-
replaces = forms.CharField(max_length=512,widget=forms.HiddenInput)
311+
replaces = SearchableDocAliasesField(required=False)
311312
comment = forms.CharField(widget=forms.Textarea, required=False)
312313

313314
def __init__(self, *args, **kwargs):
314315
self.doc = kwargs.pop('doc')
315316
super(ReplacesForm, self).__init__(*args, **kwargs)
316-
drafts = {}
317-
for d in self.doc.related_that_doc("replaces"):
318-
drafts[d.id] = d.document.name
319-
self.initial['replaces'] = json.dumps(drafts)
317+
self.initial['replaces'] = self.doc.related_that_doc("replaces")
320318

321319
def clean_replaces(self):
322-
data = self.cleaned_data['replaces'].strip()
323-
if data:
324-
ids = [int(x) for x in json.loads(data)]
325-
else:
326-
return []
327-
objects = []
328-
for id in ids:
329-
try:
330-
d = DocAlias.objects.get(pk=id)
331-
except DocAlias.DoesNotExist:
332-
raise forms.ValidationError("ERROR: %s not found for id %d" % DocAlias._meta.verbos_name, id)
320+
for d in self.cleaned_data['replaces']:
333321
if d.document == self.doc:
334-
raise forms.ValidationError("ERROR: A draft can't replace itself")
322+
raise forms.ValidationError("A draft can't replace itself")
335323
if d.document.type_id == "draft" and d.document.get_state_slug() == "rfc":
336-
raise forms.ValidationError("ERROR: A draft can't replace an RFC")
337-
objects.append(d)
338-
return objects
324+
raise forms.ValidationError("A draft can't replace an RFC")
325+
return self.cleaned_data['replaces']
339326

340327
def replaces(request, name):
341328
"""Change 'replaces' set of a Document of type 'draft' , notifying parties
@@ -942,7 +929,7 @@ def edit_shepherd_writeup(request, name):
942929
context_instance=RequestContext(request))
943930

944931
class ShepherdForm(forms.Form):
945-
shepherd = AutocompletedEmailField(required=False, only_users=True)
932+
shepherd = SearchableEmailField(required=False, only_users=True)
946933

947934
def edit_shepherd(request, name):
948935
"""Change the shepherd for a Document"""
@@ -968,7 +955,7 @@ def edit_shepherd(request, name):
968955
c.desc = "Document shepherd changed to "+ (doc.shepherd.person.name if doc.shepherd else "(None)")
969956
c.save()
970957

971-
if doc.shepherd.formatted_email() not in doc.notify:
958+
if doc.shepherd and doc.shepherd.formatted_email() not in doc.notify:
972959
login = request.user.person
973960
addrs = doc.notify
974961
if addrs:

ietf/doc/views_search.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
from ietf.doc.models import ( Document, DocAlias, State, RelatedDocument, DocEvent,
4646
LastCallDocEvent, TelechatDocEvent, IESG_SUBSTATE_TAGS )
4747
from ietf.doc.expire import expirable_draft
48-
from ietf.doc.fields import tokeninput_id_doc_name_json
48+
from ietf.doc.fields import select2_id_doc_name_json
4949
from ietf.group.models import Group
5050
from ietf.idindex.index import active_drafts_index_by_group
5151
from ietf.ipr.models import IprDocAlias
@@ -629,7 +629,7 @@ def index_active_drafts(request):
629629

630630
return render_to_response("doc/index_active_drafts.html", { 'groups': groups }, context_instance=RequestContext(request))
631631

632-
def ajax_tokeninput_search_docs(request, model_name, doc_type):
632+
def ajax_select2_search_docs(request, model_name, doc_type):
633633
if model_name == "docalias":
634634
model = DocAlias
635635
else:
@@ -652,4 +652,4 @@ def ajax_tokeninput_search_docs(request, model_name, doc_type):
652652

653653
objs = qs.distinct().order_by("name")[:20]
654654

655-
return HttpResponse(tokeninput_id_doc_name_json(objs), content_type='application/json')
655+
return HttpResponse(select2_id_doc_name_json(objs), content_type='application/json')

ietf/group/edit.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from ietf.group.utils import save_group_in_history, can_manage_group_type
2020
from ietf.group.utils import get_group_or_404
2121
from ietf.ietfauth.utils import has_role
22-
from ietf.person.fields import AutocompletedEmailsField
22+
from ietf.person.fields import SearchableEmailsField
2323
from ietf.person.models import Person, Email
2424
from ietf.group.mails import email_iesg_secretary_re_charter
2525

@@ -29,11 +29,11 @@ class GroupForm(forms.Form):
2929
name = forms.CharField(max_length=255, label="Name", required=True)
3030
acronym = forms.CharField(max_length=10, label="Acronym", required=True)
3131
state = forms.ModelChoiceField(GroupStateName.objects.all(), label="State", required=True)
32-
chairs = AutocompletedEmailsField(label="Chairs", required=False, only_users=True)
33-
secretaries = AutocompletedEmailsField(label="Secretarias", required=False, only_users=True)
34-
techadv = AutocompletedEmailsField(label="Technical Advisors", required=False, only_users=True)
35-
delegates = AutocompletedEmailsField(label="Delegates", required=False, only_users=True, max_entries=MAX_GROUP_DELEGATES,
36-
help_text=mark_safe("Chairs can delegate the authority to update the state of group documents - at most %s persons at a given time." % MAX_GROUP_DELEGATES))
32+
chairs = SearchableEmailsField(label="Chairs", required=False, only_users=True)
33+
secretaries = SearchableEmailsField(label="Secretarias", required=False, only_users=True)
34+
techadv = SearchableEmailsField(label="Technical Advisors", required=False, only_users=True)
35+
delegates = SearchableEmailsField(label="Delegates", required=False, only_users=True, max_entries=MAX_GROUP_DELEGATES,
36+
help_text=mark_safe("Chairs can delegate the authority to update the state of group documents - at most %s persons at a given time." % MAX_GROUP_DELEGATES))
3737
ad = forms.ModelChoiceField(Person.objects.filter(role__name="ad", role__group__state="active").order_by('name'), label="Shepherding AD", empty_label="(None)", required=False)
3838
parent = forms.ModelChoiceField(Group.objects.filter(state="active").order_by('name'), empty_label="(None)", required=False)
3939
list_email = forms.CharField(max_length=64, required=False)

ietf/group/info.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ def construct_group_menu_context(request, group, selected, group_type, others):
303303

304304
if group.features.has_milestones:
305305
if group.state_id != "proposed" and (is_chair or can_manage):
306-
actions.append((u"Add or edit milestones", urlreverse("group_edit_milestones", kwargs=kwargs)))
306+
actions.append((u"Edit milestones", urlreverse("group_edit_milestones", kwargs=kwargs)))
307307

308308
if group.features.has_materials and can_manage_materials(request.user, group):
309309
actions.append((u"Upload material", urlreverse("ietf.doc.views_material.choose_material_type", kwargs=kwargs)))

0 commit comments

Comments
 (0)