Revamp the AutocompletedEmailsField API a bit to allow for an AutocompletedPersonsField

- Legacy-Id: 8282
This commit is contained in:
Ole Laursen 2014-08-18 13:55:43 +00:00
parent 065660b66f
commit 92e5694ed3
8 changed files with 106 additions and 48 deletions

View file

@ -1043,7 +1043,7 @@ def edit_shepherd_writeup(request, name):
context_instance=RequestContext(request))
class ShepherdForm(forms.Form):
shepherd = AutocompletedEmailField(required=False)
shepherd = AutocompletedEmailField(required=False, only_users=True)
def edit_shepherd(request, name):
"""Change the shepherd for a Document"""

View file

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

View file

@ -31,7 +31,7 @@ def stream_documents(request, acronym):
return render_to_response('group/stream_documents.html', {'stream':stream, 'docs':docs, 'meta':meta }, context_instance=RequestContext(request))
class StreamEditForm(forms.Form):
delegates = AutocompletedEmailsField(label="Delegates", required=False)
delegates = AutocompletedEmailsField(required=False, only_users=True)
def stream_edit(request, acronym):
group = get_object_or_404(Group, acronym=acronym)

View file

@ -6,26 +6,42 @@ from django.core.urlresolvers import reverse as urlreverse
import debug # pyflakes:ignore
from ietf.person.models import Email
from ietf.person.models import Email, Person
def json_emails(emails):
if isinstance(emails, basestring):
emails = Email.objects.filter(address__in=[x.strip() for x in emails.split(",") if x.strip()]).select_related("person")
return json.dumps([{"id": e.address + "", "name": escape(u"%s <%s>" % (e.person.name, e.address))} for e in emails])
def tokeninput_id_name_json(objs):
def format_email(e):
return escape(u"%s <%s>" % (e.person.name, e.address))
def format_person(p):
return escape(p.name)
class AutocompletedEmailsField(forms.CharField):
"""Multi-select field using jquery.tokeninput.js. Since the API of
tokeninput" is asymmetric, we have to pass it a JSON
representation on the way out and parse the ids coming back as a
comma-separated list on the way in."""
formatter = format_email if objs and isinstance(objs[0], Email) else format_person
def __init__(self, max_entries=None, hint_text="Type in name or email to search for person and email address", only_users=True,
return json.dumps([{ "id": o.pk, "name": formatter(o) } for o in objs])
class AutocompletedPersonsField(forms.CharField):
"""Tokenizing autocompleted multi-select field for choosing
persons/emails or just persons using jquery.tokeninput.js.
The field operates on either Email or Person models. In the case
of Email models, the person name is shown next to the email address.
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
only_users=False, # only select persons who also have a user
model=Person, # or Email
hint_text="Type in name to search for person",
*args, **kwargs):
kwargs["max_length"] = 1000
self.max_entries = max_entries
self.only_users = only_users
self.model = model
super(AutocompletedEmailsField, self).__init__(*args, **kwargs)
super(AutocompletedPersonsField, self).__init__(*args, **kwargs)
self.widget.attrs["class"] = "tokenized-field"
self.widget.attrs["data-hint-text"] = hint_text
@ -39,41 +55,64 @@ class AutocompletedEmailsField(forms.CharField):
if not value:
value = ""
if isinstance(value, basestring):
addresses = self.parse_tokenized_value(value)
value = Email.objects.filter(address__in=addresses).select_related("person")
if isinstance(value, Email):
pks = self.parse_tokenized_value(value)
value = self.model.objects.filter(pk__in=pks).select_related("person")
if isinstance(value, self.model):
value = [value]
self.widget.attrs["data-pre"] = json_emails(value)
self.widget.attrs["data-pre"] = tokeninput_id_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_search_emails")
self.widget.attrs["data-ajax-url"] = urlreverse("ajax_tokeninput_search", kwargs={ "model_name": self.model.__name__.lower() })
if self.only_users:
self.widget.attrs["data-ajax-url"] += "?user=1" # require a Datatracker account
return ",".join(e.address for e in value)
def clean(self, value):
value = super(AutocompletedEmailsField, self).clean(value)
addresses = self.parse_tokenized_value(value)
value = super(AutocompletedPersonsField, self).clean(value)
pks = self.parse_tokenized_value(value)
emails = Email.objects.filter(address__in=addresses).exclude(person=None).select_related("person")
# there are still a couple of active roles without accounts so don't disallow those yet
#if self.only_users:
# emails = emails.exclude(person__user=None)
found_addresses = [e.address for e in emails]
objs = self.model.objects.filter(pk__in=pks)
if self.model == Email:
objs = objs.exclude(person=None).select_related("person")
failed_addresses = [x for x in addresses if x not in found_addresses]
if failed_addresses:
raise forms.ValidationError(u"Could not recognize the following email addresses: %s. You can only input addresses already registered in the Datatracker." % ", ".join(failed_addresses))
# there are still a couple of active roles without accounts so don't disallow those yet
#if self.only_users:
# objs = objs.exclude(person__user=None)
if self.max_entries != None and len(emails) > self.max_entries:
found_pks = [e.pk for e 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 {model_name}s: {pks}. You can only input {model_name}s already registered in the Datatracker.".format(pks=", ".join(failed_pks), model_name=self.model.__name__.lower()))
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 emails
return objs
class AutocompletedPersonField(AutocompletedPersonsField):
"""Version of AutocompletedPersonsField specialized to a single object."""
def __init__(self, *args, **kwargs):
kwargs["max_entries"] = 1
super(AutocompletedPersonField, self).__init__(*args, **kwargs)
def clean(self, value):
return super(AutocompletedPersonField, self).clean(value).first()
class AutocompletedEmailsField(AutocompletedPersonsField):
"""Version of AutocompletedPersonsField with the defaults right for Emails."""
def __init__(self, model=Email, hint_text="Type in name or email to search for person and email address",
*args, **kwargs):
super(AutocompletedEmailsField, self).__init__(model=model, hint_text=hint_text, *args, **kwargs)
class AutocompletedEmailField(AutocompletedEmailsField):
"""Version of AutocompletedEmailsField specialized to a single Email."""
"""Version of AutocompletedEmailsField specialized to a single object."""
def __init__(self, *args, **kwargs):
kwargs["max_entries"] = 1
@ -81,3 +120,5 @@ class AutocompletedEmailField(AutocompletedEmailsField):
def clean(self, value):
return super(AutocompletedEmailField, self).clean(value).first()

View file

@ -11,7 +11,7 @@ class PersonTests(TestCase):
draft = make_test_data()
person = draft.ad
r = self.client.get(urlreverse("ietf.person.views.ajax_search_emails"), dict(q=person.name))
r = self.client.get(urlreverse("ietf.person.views.ajax_tokeninput_search", kwargs={ "model_name": "email"}), dict(q=person.name))
self.assertEqual(r.status_code, 200)
data = json.loads(r.content)
self.assertEqual(data[0]["id"], person.email_address())

View file

@ -2,6 +2,6 @@ from django.conf.urls import patterns
from ietf.person import ajax
urlpatterns = patterns('',
(r'^search/$', "ietf.person.views.ajax_search_emails", None, 'ajax_search_emails'),
(r'^search/(?P<model_name>(person|email))/$', "ietf.person.views.ajax_tokeninput_search", None, 'ajax_tokeninput_search'),
(r'^(?P<personid>[a-z0-9]+).json$', ajax.person_json),
)

View file

@ -1,22 +1,38 @@
from django.http import HttpResponse
from django.db.models import Q
from ietf.person.models import Email
from ietf.person.fields import json_emails
from ietf.person.models import Email, Person
from ietf.person.fields import tokeninput_id_name_json
def ajax_tokeninput_search(request, model_name):
if model_name == "email":
model = Email
else:
model = Person
def ajax_search_emails(request):
q = [w.strip() for w in request.GET.get('q', '').split() if w.strip()]
if not q:
emails = Email.objects.none()
objs = model.objects.none()
else:
query = Q()
for t in q:
query &= Q(person__alias__name__icontains=t) | Q(address__icontains=t)
emails = Email.objects.filter(query).exclude(person=None)
objs = model.objects.filter(query)
if request.GET.get("user") == "1":
emails = emails.exclude(person__user=None) # require an account at the Datatracker
# require an account at the Datatracker
only_users = request.GET.get("user") == "1"
emails = emails.filter(active=True).order_by('person__name').distinct()[:10]
return HttpResponse(json_emails(emails), content_type='application/json')
if model == Email:
objs = objs.filter(active=True).order_by('person__name').exclude(person=None)
if only_users:
objs = objs.exclude(person__user=None)
elif model == Person:
objs = objs.order_by("name")
if only_users:
objs = objs.exclude(user=None)
objs = objs.distinct()[:10]
return HttpResponse(tokeninput_id_name_json(objs), content_type='application/json')

View file

@ -132,7 +132,7 @@ class EditModelForm(forms.ModelForm):
iesg_state = forms.ModelChoiceField(queryset=State.objects.filter(type='draft-iesg'),required=False)
group = GroupModelChoiceField(required=True)
review_by_rfc_editor = forms.BooleanField(required=False)
shepherd = AutocompletedEmailField(required=False)
shepherd = AutocompletedEmailField(required=False, only_users=True)
class Meta:
model = Document