diff --git a/ietf/doc/views_draft.py b/ietf/doc/views_draft.py index ef46b1632..cb96c463a 100644 --- a/ietf/doc/views_draft.py +++ b/ietf/doc/views_draft.py @@ -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""" diff --git a/ietf/group/edit.py b/ietf/group/edit.py index 173597cc7..2d9ce0942 100644 --- a/ietf/group/edit.py +++ b/ietf/group/edit.py @@ -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) diff --git a/ietf/group/views_stream.py b/ietf/group/views_stream.py index 783c282d8..48790beb3 100644 --- a/ietf/group/views_stream.py +++ b/ietf/group/views_stream.py @@ -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) diff --git a/ietf/person/fields.py b/ietf/person/fields.py index a4c2681b5..7f0165a11 100644 --- a/ietf/person/fields.py +++ b/ietf/person/fields.py @@ -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() + + diff --git a/ietf/person/tests.py b/ietf/person/tests.py index 91cea95d1..d044d45f5 100644 --- a/ietf/person/tests.py +++ b/ietf/person/tests.py @@ -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()) diff --git a/ietf/person/urls.py b/ietf/person/urls.py index 27fbe9c20..8392fb51d 100644 --- a/ietf/person/urls.py +++ b/ietf/person/urls.py @@ -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(person|email))/$', "ietf.person.views.ajax_tokeninput_search", None, 'ajax_tokeninput_search'), (r'^(?P[a-z0-9]+).json$', ajax.person_json), ) diff --git a/ietf/person/views.py b/ietf/person/views.py index eee4f4a8f..a51befb4f 100644 --- a/ietf/person/views.py +++ b/ietf/person/views.py @@ -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') diff --git a/ietf/secr/drafts/forms.py b/ietf/secr/drafts/forms.py index cc34554ee..58d5a6ed6 100644 --- a/ietf/secr/drafts/forms.py +++ b/ietf/secr/drafts/forms.py @@ -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