api: free text search on users (#15736)

This commit is contained in:
Emmanuel Cazenave 2018-11-21 14:50:00 +01:00
parent 2bde2b48fb
commit 0876d25dbd
7 changed files with 97 additions and 25 deletions

View File

@ -23,6 +23,8 @@ from rest_framework.exceptions import PermissionDenied, AuthenticationFailed
from rest_framework.fields import CreateOnlyDefault
from rest_framework.decorators import list_route, detail_route
from rest_framework.authentication import SessionAuthentication
from rest_framework.filters import BaseFilterBackend
from rest_framework.settings import api_settings
from django_filters.rest_framework import FilterSet
@ -540,11 +542,21 @@ class ChangeEmailSerializer(serializers.Serializer):
email = serializers.EmailField()
class FreeTextSearchFilter(BaseFilterBackend):
"""
"""
def filter_queryset(self, request, queryset, view):
if 'q' in request.GET:
queryset = queryset.free_text_search(request.GET['q'])
return queryset
class UsersAPI(HookMixin, ExceptionHandlerMixin, ModelViewSet):
ordering_fields = ['username', 'first_name', 'last_name', 'modified', 'date_joined']
lookup_field = 'uuid'
serializer_class = BaseUserSerializer
filter_class = UsersFilter
filter_backends = api_settings.DEFAULT_FILTER_BACKENDS + [FreeTextSearchFilter]
pagination_class = pagination.CursorPagination
ordering = ['modified', 'id']

View File

@ -1,6 +1,35 @@
from django.db import models
from django.utils import timezone
from django.contrib.auth.models import BaseUserManager
from authentic2.models import Attribute
class UserQuerySet(models.QuerySet):
def free_text_search(self, search):
terms = search.split()
searchable_attributes = Attribute.objects.filter(searchable=True)
queries = []
for term in terms:
q = (models.query.Q(username__icontains=term) |
models.query.Q(first_name__icontains=term) |
models.query.Q(last_name__icontains=term) |
models.query.Q(email__icontains=term))
for a in searchable_attributes:
if a.name in ('first_name', 'last_name'):
continue
q = q | models.query.Q(
attribute_values__content__icontains=term, attribute_values__attribute=a)
queries.append(q)
self = self.filter(reduce(models.query.Q.__and__, queries))
# search by attributes can match multiple times
if searchable_attributes:
self = self.distinct()
return self
class UserManager(BaseUserManager):
def _create_user(self, username, email, password,

View File

@ -16,7 +16,7 @@ from authentic2 import utils, validators, app_settings
from authentic2.decorators import errorcollector, RequestCache
from authentic2.models import Service, AttributeValue, Attribute
from .managers import UserManager
from .managers import UserManager, UserQuerySet
@RequestCache
@ -108,7 +108,7 @@ class User(AbstractBaseUser, PermissionMixin):
db_index=True,
auto_now=True)
objects = UserManager()
objects = UserManager.from_queryset(UserQuerySet)()
attributes = AttributesDescriptor()
verified_attributes = AttributesDescriptor(verified=True)

View File

@ -591,7 +591,7 @@ class UserSearchForm(OUSearchForm, CssClass, PrefixFormMixin, FormWithRequest):
def filter(self, qs):
qs = super(UserSearchForm, self).filter(qs)
if self.enough_chars():
qs = utils.filter_user(qs, self.cleaned_data['text'])
qs = qs.free_text_search(self.cleaned_data['text'])
elif self.not_enough_chars():
qs = qs.none()
return qs

View File

@ -1,29 +1,8 @@
from django.db.models.query import Q
from django_rbac.utils import get_ou_model
from authentic2.decorators import GlobalCache
from authentic2.models import Attribute
def filter_user(qs, search):
terms = search.split()
searchable_attributes = Attribute.objects.filter(searchable=True)
queries = []
for term in terms:
q = (Q(username__icontains=term) | Q(first_name__icontains=term) |
Q(last_name__icontains=term) | Q(email__icontains=term))
for a in searchable_attributes:
if a.name in ('first_name', 'last_name'):
continue
q = q | Q(attribute_values__content__icontains=term, attribute_values__attribute=a)
queries.append(q)
qs = qs.filter(reduce(Q.__and__, queries))
# search by attributes can match multiple times
if searchable_attributes:
qs = qs.distinct()
return qs
def label_from_user(user):

View File

@ -194,6 +194,16 @@ def test_api_users_list_by_authorized_service(app, superuser):
assert len(resp.json['results']) == 4
def test_api_users_list_search_text(app, superuser):
app.authorization = ('Basic', (superuser.username, superuser.username))
User = get_user_model()
User.objects.create(username='someuser')
resp = app.get('/api/users/?q=some')
results = resp.json['results']
assert len(results) == 1
assert results[0]['username'] == 'someuser'
def test_api_users_create(settings, app, api_user):
from django.contrib.auth import get_user_model
from authentic2.models import Attribute, AttributeValue

View File

@ -562,6 +562,48 @@ def test_manager_many_ou_auto_admin_role(app, ou1, admin, user_with_auto_admin_r
test_user_listing_auto_admin_role(user_with_auto_admin_role)
def test_manager_search_user(app, admin, simple_role, settings):
User = get_user_model()
OU = get_ou_model()
default_ou = OU.objects.get()
User.objects.create(username='user1', ou=default_ou)
response = login(app, admin, '/manage/users/')
# search without anything specified returns every user
form = response.forms['search-form']
response = form.submit()
query = response.pyquery.remove_namespaces()
assert len(query('table tbody td.username')) == 2
names = {elt.text for elt in query('table tbody td.username')}
assert names == {'admin', 'user1'}
# search a non matching string returns nothing
response = app.get('/manage/users/')
form = response.forms['search-form']
form.set('search-text', 'unkown')
response = form.submit()
query = response.pyquery.remove_namespaces()
assert len(query('table tbody td.username')) == 0
# search a string matching exactly a username returns this user
response = app.get('/manage/users/')
form = response.forms['search-form']
form.set('search-text', 'user1')
response = form.submit()
query = response.pyquery.remove_namespaces()
assert len(query('table tbody td.username')) == 1
assert query('table tbody td.username')[0].text == 'user1'
# search a string matching partially a username returns this user
response = app.get('/manage/users/')
form = response.forms['search-form']
form.set('search-text', 'user')
response = form.submit()
query = response.pyquery.remove_namespaces()
assert len(query('table tbody td.username')) == 1
assert query('table tbody td.username')[0].text == 'user1'
def test_manager_site_export(app, superuser):
response = login(app, superuser, '/manage/site-export/')
assert 'roles' in response.json