api: free text search on users (#15736)
This commit is contained in:
parent
2bde2b48fb
commit
0876d25dbd
|
@ -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']
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue