datatracker/ietf/person/fields.py
Jennifer Richards 608b8e16a4
feat: only offer IAB/IESG members for bofreq responsible leadership (#4276)
* refactor: avoid using select2 data-* attrs on html elements

Using "data-ajax--url" shadows explicit configuration in our
select2.js wrapper. Use "data-select2-ajax-url" to avoid this.

Also add ability to omit the ajax setup entirely by returning
None from ajax_url().

* chore: hook up a flag to disable ajax for SearchablePersonsField

* refactor: send select2 prefetch data as array and allow config of min input length

* feat: only offer IAB/IESG members for bofreq responsible leadership

* test: area directors/IAB members should be options for bofreq responsible leaders

* test: update tests to match changes to SearchableField

* fix: clean up SearchablePersonsField breakage when searching by email address

* chore: finish incomplete comment
2022-07-26 11:23:00 -05:00

160 lines
5.7 KiB
Python

# Copyright The IETF Trust 2012-2020, All Rights Reserved
# -*- coding: utf-8 -*-
import json
from collections import Counter
from urllib.parse import urlencode
from typing import Type # pyflakes:ignore
import unidecode
from django import forms
from django.core.validators import validate_email
from django.db import models # pyflakes:ignore
from django.urls import reverse as urlreverse
from django.utils.html import escape
import debug # pyflakes:ignore
from ietf.person.models import Email, Person
from ietf.utils.fields import SearchableField
def select2_id_name(objs):
def format_email(e):
return escape("%s <%s>" % (e.person.name, e.address))
def format_person(p):
if p.name_count > 1:
return escape('%s (%s)' % (p.name,p.email().address if p.email() else 'no email address'))
else:
return escape(p.name)
if objs and isinstance(objs[0], Email):
formatter = format_email
else:
formatter = format_person
c = Counter([p.name for p in objs])
for p in objs:
p.name_count = c[p.name]
formatter = format_email if objs and isinstance(objs[0], Email) else format_person
return [{ "id": o.pk, "text": formatter(o) } for o in objs if o]
def select2_id_name_json(objs):
return json.dumps(select2_id_name(objs))
class SearchablePersonsField(SearchableField):
"""Server-based multi-select field for choosing
persons/emails or just persons using select2.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 with some extra attributes used by
the Javascript part.
If the field will be programmatically updated, any model instances
that may be added to the initial set should be included in the extra_prefetch
list. These can then be added by updating val() and triggering the 'change'
event on the select2 field in JavaScript.
If disable_ajax is True, only objects that are prefetched can be selected. This
will be any currently selected items plus any in extra_prefetch.
"""
model = Person # type: Type[models.Model]
default_hint_text = "Type name to search for person."
def __init__(self,
only_users=False, # only select persons who also have a user
all_emails=False, # select only active email addresses
extra_prefetch=None, # extra data records to include in prefetch
disable_ajax=False, # use ajax to search outside of prefetched set
*args, **kwargs):
super(SearchablePersonsField, self).__init__(*args, **kwargs)
self.only_users = only_users
self.all_emails = all_emails
self.extra_prefetch = extra_prefetch or []
assert all([isinstance(obj, self.model) for obj in self.extra_prefetch])
self.disable_ajax = disable_ajax
def validate_pks(self, pks):
"""Validate format of PKs"""
for pk in pks:
if not pk.isdigit():
raise forms.ValidationError("Unexpected value: %s" % pk)
def make_select2_data(self, model_instances):
# Include records needed by the initial value of the field plus any added
# via the extra_prefetch property.
prefetch_set = set(model_instances) if model_instances else set()
prefetch_set = prefetch_set.union(set(self.extra_prefetch)) # eliminate duplicates
return sorted(
select2_id_name(list(prefetch_set)),
key=lambda item: unidecode.unidecode(item['text']),
)
def ajax_url(self):
if self.disable_ajax:
return None
else:
url = urlreverse(
"ietf.person.views.ajax_select2_search",
kwargs={ "model_name": self.model.__name__.lower() }
)
query_args = {}
if self.only_users:
query_args["user"] = "1"
if self.all_emails:
query_args["a"] = "1"
if len(query_args) > 0:
url += '?%s' % urlencode(query_args)
return url
class SearchablePersonField(SearchablePersonsField):
"""Version of SearchablePersonsField specialized to a single object."""
max_entries = 1
class SearchableEmailsField(SearchablePersonsField):
"""Version of SearchablePersonsField with the defaults right for Emails."""
model = Email # type: Type[models.Model]
default_hint_text = "Type name or email to search for person and email address."
def validate_pks(self, pks):
for pk in pks:
validate_email(pk)
def get_model_instances(self, item_ids):
return self.model.objects.filter(pk__in=item_ids).select_related("person")
class SearchableEmailField(SearchableEmailsField):
"""Version of SearchableEmailsField specialized to a single object."""
max_entries = 1
class PersonEmailChoiceField(forms.ModelChoiceField):
"""ModelChoiceField targeting Email and displaying choices with the
person name as well as the email address. Needs further
restrictions, e.g. on role, to useful."""
def __init__(self, *args, **kwargs):
if not "queryset" in kwargs:
kwargs["queryset"] = Email.objects.select_related("person")
self.label_with = kwargs.pop("label_with", None)
super(PersonEmailChoiceField, self).__init__(*args, **kwargs)
def label_from_instance(self, email):
if self.label_with == "person":
return str(email.person)
elif self.label_with == "email":
return email.address
else:
return "{} <{}>".format(email.person, email.address)