Skip to content

Commit 5604914

Browse files
committed
Add AutocompletedDocumentsField to be used for replacements submission
and refactor milestones tool to use this field - Legacy-Id: 8381
1 parent 25423f6 commit 5604914

File tree

9 files changed

+142
-23
lines changed

9 files changed

+142
-23
lines changed

ietf/doc/fields.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import json
2+
3+
from django.utils.html import escape
4+
from django import forms
5+
from django.core.urlresolvers import reverse as urlreverse
6+
7+
import debug # pyflakes:ignore
8+
9+
from ietf.doc.models import Document, DocAlias
10+
11+
def tokeninput_id_doc_name_json(objs):
12+
return json.dumps([{ "id": o.pk, "name": escape(o.name) } for o in objs])
13+
14+
class AutocompletedDocumentsField(forms.CharField):
15+
"""Tokenizing autocompleted multi-select field for choosing
16+
documents using jquery.tokeninput.js.
17+
18+
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."""
22+
23+
def __init__(self,
24+
max_entries=None, # max number of selected objs
25+
model=Document,
26+
hint_text="Type in name to search for document",
27+
doc_type="draft",
28+
*args, **kwargs):
29+
kwargs["max_length"] = 10000
30+
self.max_entries = max_entries
31+
self.doc_type = doc_type
32+
self.model = model
33+
34+
super(AutocompletedDocumentsField, self).__init__(*args, **kwargs)
35+
36+
self.widget.attrs["class"] = "tokenized-field"
37+
self.widget.attrs["data-hint-text"] = hint_text
38+
if self.max_entries != None:
39+
self.widget.attrs["data-max-entries"] = self.max_entries
40+
41+
def parse_tokenized_value(self, value):
42+
return [x.strip() for x in value.split(",") if x.strip()]
43+
44+
def prepare_value(self, value):
45+
if not value:
46+
value = ""
47+
if isinstance(value, basestring):
48+
pks = self.parse_tokenized_value(value)
49+
value = self.model.objects.filter(pk__in=pks, type=self.doc_type)
50+
if isinstance(value, self.model):
51+
value = [value]
52+
53+
self.widget.attrs["data-pre"] = tokeninput_id_doc_name_json(value)
54+
55+
# doing this in the constructor is difficult because the URL
56+
# patterns may not have been fully constructed there yet
57+
self.widget.attrs["data-ajax-url"] = urlreverse("ajax_tokeninput_search_docs", kwargs={
58+
"doc_type": self.doc_type,
59+
"model_name": self.model.__name__.lower()
60+
})
61+
62+
return ",".join(o.pk for o in value)
63+
64+
def clean(self, value):
65+
value = super(AutocompletedDocumentsField, self).clean(value)
66+
pks = self.parse_tokenized_value(value)
67+
68+
objs = self.model.objects.filter(pk__in=pks)
69+
70+
found_pks = [str(o.pk) for o in objs]
71+
failed_pks = [x for x in pks if x not in found_pks]
72+
if failed_pks:
73+
raise forms.ValidationError(u"Could not recognize the following documents: {pks}. You can only input documents already registered in the Datatracker.".format(pks=", ".join(failed_pks)))
74+
75+
if self.max_entries != None and len(objs) > self.max_entries:
76+
raise forms.ValidationError(u"You can select at most %s entries only." % self.max_entries)
77+
78+
return objs
79+
80+
class AutocompletedDocAliasField(AutocompletedDocumentsField):
81+
def __init__(self, model=DocAlias, *args, **kwargs):
82+
super(AutocompletedDocAliasField, self).__init__(model=model, *args, **kwargs)
83+

ietf/doc/tests.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import datetime
2+
import json
23
import sys
34
if sys.version_info[0] == 2 and sys.version_info[1] < 7:
45
import unittest2 as unittest
@@ -121,6 +122,32 @@ def test_indexes(self):
121122
r = self.client.get(urlreverse("index_active_drafts"))
122123
self.assertEqual(r.status_code, 200)
123124
self.assertTrue(draft.title in r.content)
125+
126+
def test_ajax_search_docs(self):
127+
draft = make_test_data()
128+
129+
# Document
130+
url = urlreverse("ajax_tokeninput_search_docs", kwargs={
131+
"model_name": "document",
132+
"doc_type": "draft",
133+
})
134+
r = self.client.get(url, dict(q=draft.name))
135+
self.assertEqual(r.status_code, 200)
136+
data = json.loads(r.content)
137+
self.assertEqual(data[0]["id"], draft.pk)
138+
139+
# DocAlias
140+
doc_alias = draft.docalias_set.get()
141+
142+
url = urlreverse("ajax_tokeninput_search_docs", kwargs={
143+
"model_name": "docalias",
144+
"doc_type": "draft",
145+
})
146+
147+
r = self.client.get(url, dict(q=doc_alias.name))
148+
self.assertEqual(r.status_code, 200)
149+
data = json.loads(r.content)
150+
self.assertEqual(data[0]["id"], doc_alias.pk)
124151

125152

126153
class DocTestCase(TestCase):

ietf/doc/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +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"),
5253

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

ietf/doc/views_search.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,21 +37,23 @@
3737
from django.shortcuts import render_to_response
3838
from django.db.models import Q
3939
from django.template import RequestContext
40-
from django.http import Http404, HttpResponseBadRequest
40+
from django.http import Http404, HttpResponseBadRequest, HttpResponse
4141

4242
import debug # pyflakes:ignore
4343

4444
from ietf.community.models import CommunityList
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
4849
from ietf.group.models import Group
4950
from ietf.idindex.index import active_drafts_index_by_group
5051
from ietf.ipr.models import IprDocAlias
5152
from ietf.name.models import DocTagName, DocTypeName, StreamName
5253
from ietf.person.models import Person
5354
from ietf.utils.draft_search import normalize_draftname
5455

56+
5557
class SearchForm(forms.Form):
5658
name = forms.CharField(required=False)
5759
rfcs = forms.BooleanField(required=False, initial=True)
@@ -626,3 +628,28 @@ def index_active_drafts(request):
626628
groups = active_drafts_index_by_group()
627629

628630
return render_to_response("doc/index_active_drafts.html", { 'groups': groups }, context_instance=RequestContext(request))
631+
632+
def ajax_tokeninput_search_docs(request, model_name, doc_type):
633+
if model_name == "docalias":
634+
model = DocAlias
635+
else:
636+
model = Document
637+
638+
q = [w.strip() for w in request.GET.get('q', '').split() if w.strip()]
639+
640+
if not q:
641+
objs = model.objects.none()
642+
else:
643+
qs = model.objects.all()
644+
645+
if model == Document:
646+
qs = qs.filter(type=doc_type)
647+
elif model == DocAlias:
648+
qs = qs.filter(document__type=doc_type)
649+
650+
for t in q:
651+
qs = qs.filter(name__icontains=t)
652+
653+
objs = qs.distinct().order_by("name")[:20]
654+
655+
return HttpResponse(tokeninput_id_doc_name_json(objs), content_type='application/json')

ietf/group/milestones.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from ietf.doc.models import Document, DocEvent
1313
from ietf.doc.utils import get_chartering_type
14+
from ietf.doc.fields import AutocompletedDocumentsField
1415
from ietf.group.models import GroupMilestone, MilestoneGroupEvent
1516
from ietf.group.utils import (save_milestone_in_history, can_manage_group_type, milestone_reviewer_for_group_type,
1617
get_group_or_404)
@@ -34,7 +35,7 @@ class MilestoneForm(forms.Form):
3435

3536
delete = forms.BooleanField(required=False, initial=False)
3637

37-
docs = forms.CharField(max_length=10000, required=False)
38+
docs = AutocompletedDocumentsField(required=False)
3839

3940
accept = forms.ChoiceField(choices=(("accept", "Accept"), ("reject", "Reject and delete"), ("noaction", "No action")),
4041
required=False, initial="noaction", widget=forms.RadioSelect)
@@ -95,10 +96,6 @@ def __init__(self, *args, **kwargs):
9596
# calculate whether we've changed
9697
self.changed = self.is_bound and (not self.milestone or any(unicode(self[f].data) != unicode(self.initial[f]) for f in self.fields.iterkeys()))
9798

98-
def clean_docs(self):
99-
s = self.cleaned_data["docs"]
100-
return Document.objects.filter(pk__in=[x.strip() for x in s.split(",") if x.strip()], type="draft")
101-
10299
def clean_resolved(self):
103100
r = self.cleaned_data["resolved"].strip()
104101

@@ -391,8 +388,3 @@ def reset_charter_milestones(request, group_type, acronym):
391388
charter_milestones=charter_milestones,
392389
current_milestones=current_milestones,
393390
))
394-
395-
396-
def ajax_search_docs(request, group_type, acronym):
397-
docs = Document.objects.filter(name__icontains=request.GET.get('q',''), type="draft").order_by('name').distinct()[:20]
398-
return HttpResponse(json_doc_names(docs), content_type='application/json')

ietf/group/tests_info.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -866,15 +866,6 @@ def test_send_milestones_overdue_reminders(self):
866866
self.assertTrue(m1.desc in unicode(outbox[-1]))
867867
self.assertTrue(m2.desc in unicode(outbox[-1]))
868868

869-
def test_ajax_search_docs(self):
870-
draft = make_test_data()
871-
872-
r = self.client.get(urlreverse("group_ajax_search_docs", kwargs=dict(group_type=draft.group.type_id, acronym=draft.group.acronym)),
873-
dict(q=draft.name))
874-
self.assertEqual(r.status_code, 200)
875-
data = json.loads(r.content)
876-
self.assertTrue(data[0]["id"], draft.name)
877-
878869
class CustomizeWorkflowTests(TestCase):
879870
def test_customize_workflow(self):
880871
make_test_data()

ietf/group/urls.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/milestones/$', 'ietf.group.milestones.edit_milestones', {'milestone_set': "current"}, "group_edit_milestones"),
2323
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/milestones/charter/$', 'ietf.group.milestones.edit_milestones', {'milestone_set': "charter"}, "group_edit_charter_milestones"),
2424
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/milestones/charter/reset/$', 'ietf.group.milestones.reset_charter_milestones', None, "group_reset_charter_milestones"),
25-
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/ajax/searchdocs/$', 'ietf.group.milestones.ajax_search_docs', None, "group_ajax_search_docs"),
2625
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/workflow/$', 'ietf.group.edit.customize_workflow'),
2726

2827
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/about/(?P<group_type>.)?$', 'ietf.group.info.group_about', None, 'group_about'),

ietf/group/urls_info.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,5 @@
3131
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/milestones/$', milestones.edit_milestones, {'milestone_set': "current"}, "group_edit_milestones"),
3232
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/milestones/charter/$', milestones.edit_milestones, {'milestone_set': "charter"}, "group_edit_charter_milestones"),
3333
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/milestones/charter/reset/$', milestones.reset_charter_milestones, None, "group_reset_charter_milestones"),
34-
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/ajax/searchdocs/$', milestones.ajax_search_docs, None, "group_ajax_search_docs"),
3534
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/workflow/$', edit.customize_workflow),
3635
)

ietf/templates/group/milestone_form.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
</tr>
2727
<tr class="docs">
2828
<td>Drafts:</td>
29-
<td><input name="{{ form.docs.html_name }}" class="tokenized-field" data-ajax-url="{% url "group_ajax_search_docs" group_type=group.type_id acronym=group.acronym %}" data-pre="{{ form.docs_prepopulate }}"/>
29+
<td>{{ form.docs }}
3030
{{ form.docs.errors }}
3131
</td>
3232
</tr>

0 commit comments

Comments
 (0)