Skip to content

Commit

Permalink
Add get_model_queryset() method
Browse files Browse the repository at this point in the history
Rather than hard-coding model.objects.all(), defer to a get_model_queryset() method to allow for easier customisation.
  • Loading branch information
ahmedaljawahiry committed Nov 11, 2024
1 parent af098b1 commit cdaaf17
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 18 deletions.
22 changes: 14 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,31 +105,37 @@ class MyAdminSite(AdminSiteSearchView, admin.AdminSite):
### Methods

```python
def match_app(self, request, query: str, name: str) -> bool:
def match_app(
self, request, query: str, name: str
) -> bool:
"""DEFAULT: case-insensitive match the app name"""
...

def match_model(
self, request, query: str, name: str, object_name: str, fields: List[Field]
) -> bool:
"""DEFAULT: case-insensitive match the model and field attributes"""
...

def match_objects(
self, request, query: str, model_class: Model, model_fields: List[Field]
) -> QuerySet:
"""DEFAULT: Returns the QuerySet after performing an OR filter across all Char fields in the model."""
...

def filter_field(self, request, query: str, field: Field) -> Optional[Q]:
def filter_field(
self, request, query: str, field: Field
) -> Optional[Q]:
"""DEFAULT: Returns a Q 'icontains' filter for Char fields, otherwise None
Note: this method is only invoked if model_char_fields is the site_search_method."""
...

def get_model_class(self, request, app_label: str, model_dict: dict) -> Optional[Model]:
def get_model_queryset(
self, request, model_class: Model, model_admin: Optional[ModelAdmin]
) -> QuerySet:
"""DEFAULT: Returns the model class' .objects.all() queryset."""

def get_model_class(
self, request, app_label: str, model_dict: dict
) -> Optional[Model]:
"""DEFAULT: Retrieve the model class from the dict created by admin.AdminSite"""
...
```

#### Example
Expand Down
59 changes: 49 additions & 10 deletions admin_site_search/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from django.apps import apps
from django.conf import settings
from django.contrib.admin import ModelAdmin
from django.db.models import CharField, Field, Model, Q, QuerySet
from django.http import HttpRequest, JsonResponse
from django.urls import path
Expand Down Expand Up @@ -36,7 +37,9 @@ def get_urls(self):
def search(self, request: HttpRequest) -> JsonResponse:
"""Returns a JsonResponse containing results from matching the "q" query parameter to
application names, model names, and all instance CharFields. Only apps/models that the
user has permission to view are searched."""
user has permission to view are searched.
:param request: The HTTPRequest object."""
query = request.GET.get("q", "")

results = {"apps": []}
Expand Down Expand Up @@ -124,7 +127,11 @@ def search(self, request: HttpRequest) -> JsonResponse:
return JsonResponse({"results": results, "counts": counts, "errors": errors})

def match_app(self, request: HttpRequest, query: str, name: str) -> bool:
"""Case-insensitive match the app name"""
"""Case-insensitive match the app name.
:param request: The HTTPRequest object.
:param query: The search query string.
:param name: The name of the app."""
return query.lower() in name.lower()

def match_model(
Expand All @@ -135,7 +142,13 @@ def match_model(
object_name: str,
fields: List[Field],
) -> bool:
"""Case-insensitive match the model and field attributes"""
"""Case-insensitive match the model and field attributes.
:param request: The HTTPRequest object.
:param query: The search query string.
:param name: The (verbose) name of the model.
:param object_name: The name of the model class.
:param fields: A list of the model's fields."""
_query = query.lower()
if _query in name.lower() or _query in object_name.lower():
# return early if we match a name
Expand All @@ -159,8 +172,15 @@ def match_objects(
- model_char_fields: OR filter across all Char fields in the model.
- admin_search_fields: delegates search to the model's corresponding admin search_fields.
:param request: The HTTPRequest object.
:param query: The search query string.
:param model_class: The model class.
:param model_fields: A list of the model's fields.
"""
results = model_class.objects.none()
model_admin = self._registry.get(model_class)
queryset = self.get_model_queryset(request, model_class, model_admin)

if self.site_search_method == "model_char_fields":
filters = Q()
Expand All @@ -171,14 +191,11 @@ def match_objects(
filters |= filter_

if filters:
results = model_class.objects.filter(filters)
results = queryset.filter(filters)
elif self.site_search_method == "admin_search_fields":
model_admin = self._registry.get(model_class)
if model_admin and model_admin.search_fields:
results, may_have_duplicates = model_admin.get_search_results(
request=request,
queryset=model_class.objects.all(),
search_term=query,
request=request, queryset=queryset, search_term=query
)

if may_have_duplicates:
Expand All @@ -193,21 +210,43 @@ def filter_field(
"""Returns a Q 'icontains' filter for Char fields, otherwise None.
Note: this method is only invoked if model_char_fields is the site_search_method.
:param request: The HTTPRequest object.
:param query: The search query string.
:param field: The model field to (optionally) filter on.
"""
_query = query.lower()
if isinstance(field, CharField):
return Q(**{f"{field.name}__icontains": _query})

def get_model_queryset(
self,
request: HttpRequest,
model_class: Model,
model_admin: Optional[ModelAdmin],
) -> QuerySet:
"""Returns the model class' .objects.all() queryset.
:param request: The HTTPRequest object.
:param model_class: The model class.
:param model_admin: The model admin, which is non-None for all registered models.
"""
return model_class.objects.all()

def get_model_class(
self, request: HttpRequest, app_label: str, model_dict: dict
) -> Optional[Model]:
"""Retrieve the model class from the dict created by admin.AdminSite, which (by default) contains:
"""Retrieves the model class from the dict created by admin.AdminSite, which (by default) contains:
- "model": the class instance (only available in Django 4.x),
- "name": capitalised verbose_name_plural,
- "object_name": the class name,
- "perms": dict of user permissions for this model,
- other (e.g. url) fields."""
- other (e.g. url) fields.
:param request: The HTTPRequest object.
:param app_label: The label/name of the model's app.
:param model_dict: A dict containing model information."""
model_class = model_dict.get("model")
if not model_class:
# model_dict["model"] only available in django 4.x
Expand Down
18 changes: 18 additions & 0 deletions tests/server/test_override.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
from django.http import HttpRequest
from django.test import Client

from dev.football.stadiums.admin import StadiumAdmin
from dev.football.stadiums.models import Stadium
from dev.football.teams.admin import TeamAdmin
from dev.football.teams.models import Team
from tests import request_search

Expand Down Expand Up @@ -118,6 +120,22 @@ def test_filter_field_not_invoked(request_with_patch):
assert patch_field_fields.call_count == 0


def test_get_model_queryset(request_with_patch):
"""Verify that the get_model_queryset method is correctly invoked for each model that the user
has access to"""
patch_get_model_queryset = request_with_patch(method_name="get_model_queryset")
call_args_list = [c[0] for c in patch_get_model_queryset.call_args_list]

assert len(call_args_list) == 2
_assert_request_first_arg(call_args_list)

assert call_args_list[0][1] == Stadium
assert isinstance(call_args_list[0][2], StadiumAdmin)

assert call_args_list[1][1] == Team
assert isinstance(call_args_list[1][2], TeamAdmin)


def test_get_model_class(request_with_patch):
"""Verify that the get_model_class method is correctly invoked for each model that the
user has access to"""
Expand Down

0 comments on commit cdaaf17

Please sign in to comment.