diff --git a/django_typesense/admin.py b/django_typesense/admin.py index f77d30d..7d27d89 100644 --- a/django_typesense/admin.py +++ b/django_typesense/admin.py @@ -6,9 +6,8 @@ from django.forms import forms from django.http import JsonResponse -from django_typesense.mixins import TypesenseModelMixin -from django_typesense.utils import typesense_search, export_documents from django_typesense.paginator import TypesenseSearchPaginator +from django_typesense.utils import typesense_search logger = logging.getLogger(__name__) @@ -17,9 +16,8 @@ class TypesenseSearchAdminMixin(admin.ModelAdmin): typesense_search_fields = [] def get_typesense_search_fields(self, request): - """ - Return a sequence containing the fields to be searched whenever - somebody submits a search query. + """Return a sequence containing the fields to + be searched wheneversomebody submits a search query. """ return self.typesense_search_fields @@ -33,9 +31,7 @@ def media(self): @csrf_protect_m def changelist_view(self, request, extra_context=None): - """ - The 'change list' admin view for this model. - """ + """The 'change list' admin view for this model.""" template_response = super().changelist_view(request, extra_context) is_ajax = request.META.get("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest" @@ -46,13 +42,16 @@ def changelist_view(self, request, extra_context=None): return template_response def get_sortable_by(self, request): - """ - Get sortable fields; these are fields that sort is defaulted or set to True. + """Get sortable fields; these are fields that sort is defaulted or set to True. - Args: - request: the HttpRequest + Parameters + ---------- + request: HttpRequest + The current request object. - Returns: + Returns + ------- + list A list of field names """ @@ -62,13 +61,17 @@ def get_sortable_by(self, request): ) def get_results(self, request): - """ - Get all indexed data without any filtering or specific search terms. Works like `ModelAdmin.get_queryset()` + """Get all indexed data without any filtering or specific + search terms. Works like `ModelAdmin.get_queryset()`. - Args: - request: the HttpRequest + Parameters + ---------- + request: HttpRequest + The current request object. - Returns: + Returns + ------- + list A list of the typesense results """ @@ -79,9 +82,7 @@ def get_results(self, request): ) def get_changelist(self, request, **kwargs): - """ - Return the ChangeList class for use on the changelist page. - """ + """Return the ChangeList class for use on the changelist page.""" from django_typesense.changelist import TypesenseChangeList return TypesenseChangeList @@ -108,16 +109,25 @@ def get_typesense_search_results( sort_by: str = "", ): """ - Get the results from typesense with the provided filtering, sorting, pagination and search parameters applied - - Args: - search_term: The search term provided in the search form - request: the current request object - page_num: The requested page number - filter_by: The filtering parameters - sort_by: The sort parameters - - Returns: + Get the results from typesense with the provided filtering, + sorting, pagination and search parameters applied. + + Parameters + ---------- + request: HttpRequest + The current request object. + search_term: str + The search term provided in the search form. + page_num: int + The requested page number. + filter_by: str + The filtering parameters. + sort_by: str + The sort parameters. + + Returns + ------- + list A list of typesense results """ diff --git a/django_typesense/changelist.py b/django_typesense/changelist.py index 3a15862..8ef0e7e 100644 --- a/django_typesense/changelist.py +++ b/django_typesense/changelist.py @@ -65,7 +65,7 @@ def __init__( list_editable, model_admin, sortable_by, - search_help_text, + search_help_text="", ): self.model = model self.opts = model._meta diff --git a/tests/admin.py b/tests/admin.py new file mode 100644 index 0000000..bb8eba1 --- /dev/null +++ b/tests/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin + +from django_typesense.admin import TypesenseSearchAdminMixin +from tests.models import Song + +admin.site.register(Song, TypesenseSearchAdminMixin) diff --git a/tests/collections.py b/tests/collections.py index 740de6e..7776743 100644 --- a/tests/collections.py +++ b/tests/collections.py @@ -4,6 +4,7 @@ class SongCollection(TypesenseCollection): query_by_fields = "title,artist_names,genre_name" + sortable_fields = ["id", "title"] title = fields.TypesenseCharField() genre_name = fields.TypesenseCharField(value="genre.name") diff --git a/tests/settings.py b/tests/settings.py index bea0939..f5ad32b 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -51,3 +51,5 @@ USE_TZ = True DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +ROOT_URLCONF = "tests.urls" diff --git a/tests/test_typesense_admin_mixin.py b/tests/test_typesense_admin_mixin.py index e69de29..846ff95 100644 --- a/tests/test_typesense_admin_mixin.py +++ b/tests/test_typesense_admin_mixin.py @@ -0,0 +1,104 @@ +import json +from http import HTTPStatus + +from django.contrib.admin.sites import AdminSite +from django.contrib.auth import get_user_model +from django.core.paginator import Paginator +from django.test import TestCase +from django.test.client import RequestFactory +from django.urls import reverse + +from django_typesense.admin import TypesenseSearchAdminMixin, TypesenseSearchPaginator +from django_typesense.mixins import TypesenseQuerySet + +from .factories import SongFactory +from .models import Song + +User = get_user_model() + + +class TestTypesenseSearchAdminMixin(TestCase): + def setUp(self): + self.site = AdminSite() + self.factory = RequestFactory() + self.url = reverse( + f"admin:{Song._meta.app_label}_{Song._meta.model_name}_changelist" + ) + + self.songs_count = 10 + SongFactory.create_batch(size=self.songs_count) + + def _create_user(self, username, email, is_superuser=True): + user = User.objects.create(username=username, email=email, password="password") + if is_superuser: + user.is_staff = True + user.is_superuser = True + user.save(update_fields=["is_staff", "is_superuser"]) + + return user + + def _create_mock_request(self, url, user, request_type="GET", headers={}, data={}): + if request_type == "GET": + request = self.factory.get(url, data, **headers) + + if request_type == "POST": + request = self.factory.post(url, data, **headers) + + request.user = user + return request + + def test_model_admin_changelist_view(self): + model_admin = TypesenseSearchAdminMixin(Song, self.site) + + admin_user = self._create_user("admin", "admin@email.com") + request = self._create_mock_request(self.url, admin_user) + + response = model_admin.changelist_view(request) + self.assertEqual(response.status_code, HTTPStatus.OK) + response.render() + self.assertIn(f"{self.songs_count} songs", str(response.content)) + + headers = {"HTTP_X_REQUESTED_WITH": "XMLHttpRequest"} + request = self._create_mock_request(self.url, admin_user, headers=headers) + + response = model_admin.changelist_view(request) + self.assertEqual(response.status_code, HTTPStatus.OK) + response_data = json.loads(response.content) + self.assertCountEqual(response_data.keys(), ["html"]) + self.assertIn(f"{self.songs_count} songs", response_data["html"]) + + def test_get_paginator(self): + model_admin = TypesenseSearchAdminMixin(Song, self.site) + + admin_user = self._create_user("admin", "admin@email.com") + request = self._create_mock_request(self.url, admin_user) + + songs = Song.objects.all().order_by("pk") + response = model_admin.get_paginator(request, songs, self.songs_count) + self.assertFalse(isinstance(response, TypesenseSearchPaginator)) + self.assertTrue(isinstance(response, Paginator)) + + def test_get_search_results(self): + model_admin = TypesenseSearchAdminMixin(Song, self.site) + + admin_user = self._create_user("admin", "admin@email.com") + request = self._create_mock_request(self.url, admin_user, request_type="POST") + + songs = Song.objects.all().order_by("pk") + query_set, may_have_duplicates = model_admin.get_search_results( + request, songs, "*" + ) + self.assertTrue(isinstance(query_set, TypesenseQuerySet)) + self.assertEqual(query_set.count(), self.songs_count) + self.assertFalse(may_have_duplicates) + + request = self._create_mock_request( + self.url, admin_user, request_type="POST", data={"action": "delete"} + ) + query_set, may_have_duplicates = model_admin.get_search_results( + request, songs, "*" + ) + + self.assertTrue(isinstance(query_set, TypesenseQuerySet)) + self.assertEqual(query_set.count(), self.songs_count) + self.assertFalse(may_have_duplicates) diff --git a/tests/urls.py b/tests/urls.py new file mode 100644 index 0000000..4096fa2 --- /dev/null +++ b/tests/urls.py @@ -0,0 +1,4 @@ +from django.contrib import admin +from django.urls import path + +urlpatterns = [path("admin/", admin.site.urls)]