Add AutocompletedDocumentsField to be used for replacements submission

and refactor milestones tool to use this field
 - Legacy-Id: 8381
This commit is contained in:
Ole Laursen 2014-10-09 17:14:14 +00:00
parent 25423f6779
commit 5604914bfc
9 changed files with 142 additions and 23 deletions

83
ietf/doc/fields.py Normal file
View file

@ -0,0 +1,83 @@
import json
from django.utils.html import escape
from django import forms
from django.core.urlresolvers import reverse as urlreverse
import debug # pyflakes:ignore
from ietf.doc.models import Document, DocAlias
def tokeninput_id_doc_name_json(objs):
return json.dumps([{ "id": o.pk, "name": escape(o.name) } for o in objs])
class AutocompletedDocumentsField(forms.CharField):
"""Tokenizing autocompleted multi-select field for choosing
documents using jquery.tokeninput.js.
The field uses a comma-separated list of primary keys in a
CharField element as its API, the tokeninput Javascript adds some
selection magic on top of this so we have to pass it a JSON
representation of ids and user-understandable labels."""
def __init__(self,
max_entries=None, # max number of selected objs
model=Document,
hint_text="Type in name to search for document",
doc_type="draft",
*args, **kwargs):
kwargs["max_length"] = 10000
self.max_entries = max_entries
self.doc_type = doc_type
self.model = model
super(AutocompletedDocumentsField, self).__init__(*args, **kwargs)
self.widget.attrs["class"] = "tokenized-field"
self.widget.attrs["data-hint-text"] = hint_text
if self.max_entries != None:
self.widget.attrs["data-max-entries"] = self.max_entries
def parse_tokenized_value(self, value):
return [x.strip() for x in value.split(",") if x.strip()]
def prepare_value(self, value):
if not value:
value = ""
if isinstance(value, basestring):
pks = self.parse_tokenized_value(value)
value = self.model.objects.filter(pk__in=pks, type=self.doc_type)
if isinstance(value, self.model):
value = [value]
self.widget.attrs["data-pre"] = tokeninput_id_doc_name_json(value)
# doing this in the constructor is difficult because the URL
# patterns may not have been fully constructed there yet
self.widget.attrs["data-ajax-url"] = urlreverse("ajax_tokeninput_search_docs", kwargs={
"doc_type": self.doc_type,
"model_name": self.model.__name__.lower()
})
return ",".join(o.pk for o in value)
def clean(self, value):
value = super(AutocompletedDocumentsField, self).clean(value)
pks = self.parse_tokenized_value(value)
objs = self.model.objects.filter(pk__in=pks)
found_pks = [str(o.pk) for o in objs]
failed_pks = [x for x in pks if x not in found_pks]
if failed_pks:
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)))
if self.max_entries != None and len(objs) > self.max_entries:
raise forms.ValidationError(u"You can select at most %s entries only." % self.max_entries)
return objs
class AutocompletedDocAliasField(AutocompletedDocumentsField):
def __init__(self, model=DocAlias, *args, **kwargs):
super(AutocompletedDocAliasField, self).__init__(model=model, *args, **kwargs)

View file

@ -1,4 +1,5 @@
import datetime
import json
import sys
if sys.version_info[0] == 2 and sys.version_info[1] < 7:
import unittest2 as unittest
@ -121,6 +122,32 @@ class SearchTestCase(TestCase):
r = self.client.get(urlreverse("index_active_drafts"))
self.assertEqual(r.status_code, 200)
self.assertTrue(draft.title in r.content)
def test_ajax_search_docs(self):
draft = make_test_data()
# Document
url = urlreverse("ajax_tokeninput_search_docs", kwargs={
"model_name": "document",
"doc_type": "draft",
})
r = self.client.get(url, dict(q=draft.name))
self.assertEqual(r.status_code, 200)
data = json.loads(r.content)
self.assertEqual(data[0]["id"], draft.pk)
# DocAlias
doc_alias = draft.docalias_set.get()
url = urlreverse("ajax_tokeninput_search_docs", kwargs={
"model_name": "docalias",
"doc_type": "draft",
})
r = self.client.get(url, dict(q=doc_alias.name))
self.assertEqual(r.status_code, 200)
data = json.loads(r.content)
self.assertEqual(data[0]["id"], doc_alias.pk)
class DocTestCase(TestCase):

View file

@ -49,6 +49,7 @@ urlpatterns = patterns('',
url(r'^all/$', views_search.index_all_drafts, name="index_all_drafts"),
url(r'^active/$', views_search.index_active_drafts, name="index_active_drafts"),
url(r'^tokeninputsearch/(?P<model_name>(document|docalias))/(?P<doc_type>draft)/$', views_search.ajax_tokeninput_search_docs, name="ajax_tokeninput_search_docs"),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/(?:(?P<rev>[0-9-]+)/)?$', views_doc.document_main, name="doc_view"),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/history/$', views_doc.document_history, name="doc_history"),

View file

@ -37,7 +37,7 @@ from django.core.exceptions import ObjectDoesNotExist
from django.shortcuts import render_to_response
from django.db.models import Q
from django.template import RequestContext
from django.http import Http404, HttpResponseBadRequest
from django.http import Http404, HttpResponseBadRequest, HttpResponse
import debug # pyflakes:ignore
@ -45,6 +45,7 @@ from ietf.community.models import CommunityList
from ietf.doc.models import ( Document, DocAlias, State, RelatedDocument, DocEvent,
LastCallDocEvent, TelechatDocEvent, IESG_SUBSTATE_TAGS )
from ietf.doc.expire import expirable_draft
from ietf.doc.fields import tokeninput_id_doc_name_json
from ietf.group.models import Group
from ietf.idindex.index import active_drafts_index_by_group
from ietf.ipr.models import IprDocAlias
@ -52,6 +53,7 @@ from ietf.name.models import DocTagName, DocTypeName, StreamName
from ietf.person.models import Person
from ietf.utils.draft_search import normalize_draftname
class SearchForm(forms.Form):
name = forms.CharField(required=False)
rfcs = forms.BooleanField(required=False, initial=True)
@ -626,3 +628,28 @@ def index_active_drafts(request):
groups = active_drafts_index_by_group()
return render_to_response("doc/index_active_drafts.html", { 'groups': groups }, context_instance=RequestContext(request))
def ajax_tokeninput_search_docs(request, model_name, doc_type):
if model_name == "docalias":
model = DocAlias
else:
model = Document
q = [w.strip() for w in request.GET.get('q', '').split() if w.strip()]
if not q:
objs = model.objects.none()
else:
qs = model.objects.all()
if model == Document:
qs = qs.filter(type=doc_type)
elif model == DocAlias:
qs = qs.filter(document__type=doc_type)
for t in q:
qs = qs.filter(name__icontains=t)
objs = qs.distinct().order_by("name")[:20]
return HttpResponse(tokeninput_id_doc_name_json(objs), content_type='application/json')

View file

@ -11,6 +11,7 @@ from django.contrib.auth.decorators import login_required
from ietf.doc.models import Document, DocEvent
from ietf.doc.utils import get_chartering_type
from ietf.doc.fields import AutocompletedDocumentsField
from ietf.group.models import GroupMilestone, MilestoneGroupEvent
from ietf.group.utils import (save_milestone_in_history, can_manage_group_type, milestone_reviewer_for_group_type,
get_group_or_404)
@ -34,7 +35,7 @@ class MilestoneForm(forms.Form):
delete = forms.BooleanField(required=False, initial=False)
docs = forms.CharField(max_length=10000, required=False)
docs = AutocompletedDocumentsField(required=False)
accept = forms.ChoiceField(choices=(("accept", "Accept"), ("reject", "Reject and delete"), ("noaction", "No action")),
required=False, initial="noaction", widget=forms.RadioSelect)
@ -95,10 +96,6 @@ class MilestoneForm(forms.Form):
# calculate whether we've changed
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()))
def clean_docs(self):
s = self.cleaned_data["docs"]
return Document.objects.filter(pk__in=[x.strip() for x in s.split(",") if x.strip()], type="draft")
def clean_resolved(self):
r = self.cleaned_data["resolved"].strip()
@ -391,8 +388,3 @@ def reset_charter_milestones(request, group_type, acronym):
charter_milestones=charter_milestones,
current_milestones=current_milestones,
))
def ajax_search_docs(request, group_type, acronym):
docs = Document.objects.filter(name__icontains=request.GET.get('q',''), type="draft").order_by('name').distinct()[:20]
return HttpResponse(json_doc_names(docs), content_type='application/json')

View file

@ -866,15 +866,6 @@ class MilestoneTests(TestCase):
self.assertTrue(m1.desc in unicode(outbox[-1]))
self.assertTrue(m2.desc in unicode(outbox[-1]))
def test_ajax_search_docs(self):
draft = make_test_data()
r = self.client.get(urlreverse("group_ajax_search_docs", kwargs=dict(group_type=draft.group.type_id, acronym=draft.group.acronym)),
dict(q=draft.name))
self.assertEqual(r.status_code, 200)
data = json.loads(r.content)
self.assertTrue(data[0]["id"], draft.name)
class CustomizeWorkflowTests(TestCase):
def test_customize_workflow(self):
make_test_data()

View file

@ -22,7 +22,6 @@ urlpatterns = patterns('',
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/milestones/$', 'ietf.group.milestones.edit_milestones', {'milestone_set': "current"}, "group_edit_milestones"),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/milestones/charter/$', 'ietf.group.milestones.edit_milestones', {'milestone_set': "charter"}, "group_edit_charter_milestones"),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/milestones/charter/reset/$', 'ietf.group.milestones.reset_charter_milestones', None, "group_reset_charter_milestones"),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/ajax/searchdocs/$', 'ietf.group.milestones.ajax_search_docs', None, "group_ajax_search_docs"),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/workflow/$', 'ietf.group.edit.customize_workflow'),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/about/(?P<group_type>.)?$', 'ietf.group.info.group_about', None, 'group_about'),

View file

@ -31,6 +31,5 @@ urlpatterns = patterns('',
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/milestones/$', milestones.edit_milestones, {'milestone_set': "current"}, "group_edit_milestones"),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/milestones/charter/$', milestones.edit_milestones, {'milestone_set': "charter"}, "group_edit_charter_milestones"),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/milestones/charter/reset/$', milestones.reset_charter_milestones, None, "group_reset_charter_milestones"),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/ajax/searchdocs/$', milestones.ajax_search_docs, None, "group_ajax_search_docs"),
(r'^(?P<acronym>[a-zA-Z0-9-._]+)/workflow/$', edit.customize_workflow),
)

View file

@ -26,7 +26,7 @@
</tr>
<tr class="docs">
<td>Drafts:</td>
<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 }}"/>
<td>{{ form.docs }}
{{ form.docs.errors }}
</td>
</tr>