Coverage for src/django_global_search/views.py: 93%
103 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-06 22:25 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-06 22:25 +0000
1"""Global Search Views."""
3from __future__ import annotations
5from collections import defaultdict
6from dataclasses import asdict, dataclass
8from django.apps import apps
9from django.contrib.admin.sites import AdminSite
10from django.contrib.admin.views.decorators import staff_member_required
11from django.contrib.contenttypes.models import ContentType
12from django.http import HttpRequest
13from django.shortcuts import render
14from django.utils.decorators import method_decorator
15from django.views import View
17from django_global_search.searcher import GlobalSearch, GlobalSearchResult, ModelSearchResult
20@method_decorator(staff_member_required, name="dispatch")
21class GlobalSearchView(View):
22 """Global Search View."""
24 template_name = "global_search/search.html"
25 admin_site = None
27 @dataclass
28 class SearchItemContext:
29 """Search result item for template context."""
31 url: str
32 display_text: str
34 @dataclass
35 class ModelResultContext:
36 """Model search result for template context."""
38 content_type_id: int
39 model_name: str
40 verbose_name: str
41 verbose_name_plural: str
42 has_more: bool
43 changelist_url: str | None
44 items: list[GlobalSearchView.SearchItemContext]
46 @dataclass
47 class AppResultContext:
48 """App search result for template context."""
50 app_label: str
51 app_verbose_name: str
52 models: list[GlobalSearchView.ModelResultContext]
54 @dataclass
55 class SearchContext:
56 """Template context for search view."""
58 query: str
59 apps_data: dict
60 selected_content_type_ids: list[int]
61 search_results: list[GlobalSearchView.AppResultContext]
62 elapsed_time: float | None
63 error_message: str | None
65 def get(self, request, *args, **kwargs):
66 """Handle GET request."""
67 query = request.GET.get("q", "").strip()
68 selected_ct_ids = self._get_selected_content_type_ids(request)
69 admin_site = self.admin_site
71 # Build context
72 context = self.SearchContext(
73 query=query,
74 apps_data=self._get_apps_data(request, admin_site),
75 selected_content_type_ids=selected_ct_ids,
76 search_results=[],
77 elapsed_time=None,
78 error_message=None,
79 )
81 # Execute search if query is provided
82 if query:
83 try:
84 searcher = GlobalSearch(admin_site)
85 result = searcher.search(
86 request=request,
87 query=query,
88 content_type_ids=selected_ct_ids or None,
89 )
91 context.search_results = self._convert_search_results(result)
92 context.elapsed_time = result.elapsed_time_ms / 1000.0
94 if result.is_timeout:
95 context.error_message = "Search timeout exceeded. Please refine your query."
97 except ValueError:
98 context.error_message = "Invalid query"
99 except Exception:
100 context.error_message = "Search error"
102 # Merge with admin site context for proper URL resolution
103 template_context = {
104 **self.admin_site.each_context(request),
105 **asdict(context),
106 }
107 return render(request, self.template_name, template_context)
109 def _convert_search_results(self, result: GlobalSearchResult) -> list[AppResultContext]:
110 return [
111 self.AppResultContext(
112 app_label=app_result.app_label,
113 app_verbose_name=app_result.app_verbose_name,
114 models=[
115 self._convert_model_result(model_result) for model_result in app_result.models
116 ],
117 )
118 for app_result in result.apps
119 ]
121 def _convert_model_result(self, model_result: ModelSearchResult) -> ModelResultContext:
122 return self.ModelResultContext(
123 content_type_id=model_result.content_type_id,
124 model_name=model_result.model_name,
125 verbose_name=model_result.verbose_name,
126 verbose_name_plural=model_result.verbose_name_plural,
127 has_more=model_result.has_more,
128 changelist_url=model_result.changelist_url,
129 items=[
130 self.SearchItemContext(url=item.url, display_text=item.display_text)
131 for item in model_result.items
132 ],
133 )
135 def _get_selected_content_type_ids(self, request: HttpRequest):
136 content_type_ids = set()
138 # Process 'apps' parameter (select all models in the app)
139 apps_param = request.GET.get("apps", "")
140 if apps_param:
141 app_labels = [a.strip() for a in apps_param.split(",") if a.strip()]
142 for app_label in app_labels:
143 content_type_ids.update(self._get_content_type_ids_for_app(request, app_label))
145 # Process 'content_type' parameter (individual model selection)
146 content_types_param = request.GET.get("content_type", "")
147 if content_types_param:
148 try:
149 ids = [int(cid) for cid in content_types_param.split(",") if cid]
150 content_type_ids.update(ids)
151 except ValueError:
152 pass
154 return list(content_type_ids) if content_type_ids else []
156 def _get_content_type_ids_for_app(self, request: HttpRequest, app_label: str) -> list[int]:
157 """Get all searchable content type IDs for a given app."""
158 searcher = GlobalSearch(self.admin_site)
159 searchable_admins = searcher.get_searchable_model_admins(request)
161 content_type_ids = []
162 for model_admin in searchable_admins:
163 model = model_admin.model
164 if model._meta.app_label == app_label:
165 content_type = ContentType.objects.get_for_model(model)
166 content_type_ids.append(content_type.id)
168 return content_type_ids
170 def _get_apps_data(self, request: HttpRequest, admin_site: AdminSite):
171 """Get apps and models data for sidebar."""
172 searcher = GlobalSearch(admin_site)
173 searchable_admins = searcher.get_searchable_model_admins(request)
175 # Build apps data from searchable models
176 # {app_label: {"verbose_name": "", "models": []}}
177 apps_data = defaultdict(lambda: {"verbose_name": "", "models": []})
179 for model_admin in searchable_admins:
180 model = model_admin.model
181 app_label = model._meta.app_label
182 app_config = apps.get_app_config(app_label)
184 content_type = ContentType.objects.get_for_model(model)
186 if not apps_data[app_label]["verbose_name"]:
187 apps_data[app_label]["verbose_name"] = app_config.verbose_name
189 apps_data[app_label]["models"].append(
190 {
191 "content_type_id": content_type.id,
192 "verbose_name_plural": model._meta.verbose_name_plural,
193 "model_name": model._meta.model_name,
194 }
195 )
197 # Return in app_label alphabetical order
198 return dict(sorted(apps_data.items()))