Add AutocompletedDocumentsField to be used for replacements submission
and refactor milestones tool to use this field - Legacy-Id: 8381
This commit is contained in:
parent
25423f6779
commit
5604914bfc
83
ietf/doc/fields.py
Normal file
83
ietf/doc/fields.py
Normal 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)
|
||||
|
|
@ -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):
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue