Code

Ticket #2197: generic-search.diff

File generic-search.diff, 12.8 KB (added by Matias Hermanrud Fjeld <mhf@…>, 8 years ago)
Line 
1Index: django/db/models/manager.py
2===================================================================
3--- django/db/models/manager.py (revision 3145)
4+++ django/db/models/manager.py (working copy)
5@@ -99,6 +99,9 @@
6     def values(self, *args, **kwargs):
7         return self.get_query_set().values(*args, **kwargs)
8 
9+    def search(self, *args, **kwargs):
10+        return self.get_query_set().search(*args, **kwargs)
11+
12 class ManagerDescriptor(object):
13     # This class ensures managers aren't accessible via model instances.
14     # For example, Poll.objects works, but poll_obj.objects raises AttributeError.
15Index: django/db/models/options.py
16===================================================================
17--- django/db/models/options.py (revision 3145)
18+++ django/db/models/options.py (working copy)
19@@ -13,7 +13,7 @@
20 
21 DEFAULT_NAMES = ('verbose_name', 'db_table', 'ordering',
22                  'unique_together', 'permissions', 'get_latest_by',
23-                 'order_with_respect_to', 'app_label')
24+                 'order_with_respect_to', 'app_label', 'search_fields')
25 
26 class Options(object):
27     def __init__(self, meta):
28@@ -22,6 +22,7 @@
29         self.verbose_name_plural = None
30         self.db_table = ''
31         self.ordering = []
32+        self.search_fields = []
33         self.unique_together =  []
34         self.permissions =  []
35         self.object_name, self.app_label = None, None
36@@ -197,7 +198,7 @@
37 
38 class AdminOptions(object):
39     def __init__(self, fields=None, js=None, list_display=None, list_filter=None,
40-        date_hierarchy=None, save_as=False, ordering=None, search_fields=None,
41+        date_hierarchy=None, save_as=False, ordering=None,
42         save_on_top=False, list_select_related=False, manager=None, list_per_page=100):
43         self.fields = fields
44         self.js = js or []
45@@ -205,7 +206,6 @@
46         self.list_filter = list_filter or []
47         self.date_hierarchy = date_hierarchy
48         self.save_as, self.ordering = save_as, ordering
49-        self.search_fields = search_fields or []
50         self.save_on_top = save_on_top
51         self.list_select_related = list_select_related
52         self.list_per_page = list_per_page
53Index: django/db/models/query.py
54===================================================================
55--- django/db/models/query.py   (revision 3145)
56+++ django/db/models/query.py   (working copy)
57@@ -233,6 +233,27 @@
58                 "Cannot change a query once a slice has been taken."
59         return self._clone(_limit=1, _order_by=('-'+latest_by,)).get()
60 
61+    def search(self, query, field_names=None):
62+        """
63+        Return all objects who has any the words in the given query
64+        in any of it's fields given in the model's 'search_fields'
65+        option or in the optional argument field_names.
66+        """
67+        search_fields = field_names or self.model._meta.search_fields
68+        assert bool(search_fields), "search_fields() requires either a field_names parameter or 'search_fields' in the model"
69+        assert self._limit is None and self._offset is None, \
70+                "Cannot change a query once a slice has been taken."
71+
72+        or_queries = []
73+        for bit in query.split():
74+            for field_name in search_fields:
75+                or_queries.append(Q(**{'%s__icontains' % field_name: bit}))
76+
77+        if or_queries:
78+            return self._clone().filter(reduce(operator.or_, or_queries))
79+        else:
80+            return self
81+
82     def in_bulk(self, id_list):
83         """
84         Returns a dictionary mapping each of the given IDs to the object with
85Index: django/core/management.py
86===================================================================
87--- django/core/management.py   (revision 3145)
88+++ django/core/management.py   (working copy)
89@@ -930,6 +930,14 @@
90                 except models.FieldDoesNotExist:
91                     e.add(opts, '"ordering" refers to "%s", a field that doesn\'t exist.' % field_name)
92 
93+        # Check search_fields attribute.
94+        if opts.search_fields:
95+            for field_name in opts.search_fields:
96+                try:
97+                    opts.get_field(field_name, many_to_many=False)
98+                except models.FieldDoesNotExist:
99+                    e.add(opts, '"search_fields" refers to "%s", a field that doesn\'t exist.' % field_name)
100+
101         # Check core=True, if needed.
102         for related in opts.get_followed_related_objects():
103             try:
104Index: django/views/generic/list_detail.py
105===================================================================
106--- django/views/generic/list_detail.py (revision 3145)
107+++ django/views/generic/list_detail.py (working copy)
108@@ -7,7 +7,7 @@
109 def object_list(request, queryset, paginate_by=None, page=None,
110         allow_empty=False, template_name=None, template_loader=loader,
111         extra_context=None, context_processors=None, template_object_name='object',
112-        mimetype=None):
113+        mimetype=None, allow_search=True):
114     """
115     Generic list of objects.
116 
117@@ -33,9 +33,16 @@
118             number of pages, total
119         hits
120             number of objects, total
121+        query
122+            the search query string
123     """
124     if extra_context is None: extra_context = {}
125     queryset = queryset._clone()
126+
127+    query = request.GET.get('query', '')
128+    if searchable and queryset.model._meta.search_fields and query:
129+        queryset = queryset.search(query)
130+
131     if paginate_by:
132         paginator = ObjectPaginator(queryset, paginate_by)
133         if not page:
134@@ -59,11 +66,13 @@
135             'previous': page - 1,
136             'pages': paginator.pages,
137             'hits' : paginator.hits,
138+            'query': query,
139         }, context_processors)
140     else:
141         c = RequestContext(request, {
142             '%s_list' % template_object_name: queryset,
143-            'is_paginated': False
144+            'is_paginated': False,
145+            'query': query,
146         }, context_processors)
147         if not allow_empty and len(queryset) == 0:
148             raise Http404
149Index: django/contrib/auth/models.py
150===================================================================
151--- django/contrib/auth/models.py       (revision 3145)
152+++ django/contrib/auth/models.py       (working copy)
153@@ -29,9 +29,11 @@
154         verbose_name = _('group')
155         verbose_name_plural = _('groups')
156         ordering = ('name',)
157-    class Admin:
158         search_fields = ('name',)
159 
160+    class Admin:
161+        pass
162+
163     def __str__(self):
164         return self.name
165 
166@@ -70,6 +72,7 @@
167         verbose_name = _('user')
168         verbose_name_plural = _('users')
169         ordering = ('username',)
170+        search_fields = ('username', 'first_name', 'last_name', 'email')
171     class Admin:
172         fields = (
173             (None, {'fields': ('username', 'password')}),
174@@ -80,7 +83,6 @@
175         )
176         list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff')
177         list_filter = ('is_staff', 'is_superuser')
178-        search_fields = ('username', 'first_name', 'last_name', 'email')
179 
180     def __str__(self):
181         return self.username
182Index: django/contrib/redirects/models.py
183===================================================================
184--- django/contrib/redirects/models.py  (revision 3145)
185+++ django/contrib/redirects/models.py  (working copy)
186@@ -15,10 +15,10 @@
187         db_table = 'django_redirect'
188         unique_together=(('site', 'old_path'),)
189         ordering = ('old_path',)
190+        search_fields = ('old_path', 'new_path')
191 
192     class Admin:
193         list_filter = ('site',)
194-        search_fields = ('old_path', 'new_path')
195 
196     def __str__(self):
197         return "%s ---> %s" % (self.old_path, self.new_path)
198Index: django/contrib/comments/models.py
199===================================================================
200--- django/contrib/comments/models.py   (revision 3145)
201+++ django/contrib/comments/models.py   (working copy)
202@@ -90,6 +90,7 @@
203         verbose_name = _('comment')
204         verbose_name_plural = _('comments')
205         ordering = ('-submit_date',)
206+        search_fields = ('comment', 'user__username')
207     class Admin:
208         fields = (
209             (None, {'fields': ('content_type', 'object_id', 'site')}),
210@@ -100,7 +101,6 @@
211         list_display = ('user', 'submit_date', 'content_type', 'get_content_object')
212         list_filter = ('submit_date',)
213         date_hierarchy = 'submit_date'
214-        search_fields = ('comment', 'user__username')
215 
216     def __repr__(self):
217         return "%s: %s..." % (self.user.username, self.comment[:100])
218@@ -176,6 +176,7 @@
219         verbose_name = _('free comment')
220         verbose_name_plural = _('free comments')
221         ordering = ('-submit_date',)
222+        search_fields = ('comment', 'person_name')
223     class Admin:
224         fields = (
225             (None, {'fields': ('content_type', 'object_id', 'site')}),
226@@ -185,7 +186,6 @@
227         list_display = ('person_name', 'submit_date', 'content_type', 'get_content_object')
228         list_filter = ('submit_date',)
229         date_hierarchy = 'submit_date'
230-        search_fields = ('comment', 'person_name')
231 
232     def __repr__(self):
233         return "%s: %s..." % (self.person_name, self.comment[:100])
234Index: django/contrib/flatpages/models.py
235===================================================================
236--- django/contrib/flatpages/models.py  (revision 3145)
237+++ django/contrib/flatpages/models.py  (working copy)
238@@ -18,13 +18,13 @@
239         verbose_name = _('flat page')
240         verbose_name_plural = _('flat pages')
241         ordering = ('url',)
242+        search_fields = ('url', 'title')
243     class Admin:
244         fields = (
245             (None, {'fields': ('url', 'title', 'content', 'sites')}),
246             ('Advanced options', {'classes': 'collapse', 'fields': ('enable_comments', 'registration_required', 'template_name')}),
247         )
248         list_filter = ('sites',)
249-        search_fields = ('url', 'title')
250 
251     def __str__(self):
252         return "%s -- %s" % (self.url, self.title)
253Index: django/contrib/sites/models.py
254===================================================================
255--- django/contrib/sites/models.py      (revision 3145)
256+++ django/contrib/sites/models.py      (working copy)
257@@ -15,9 +15,9 @@
258         verbose_name = _('site')
259         verbose_name_plural = _('sites')
260         ordering = ('domain',)
261+        search_fields = ('domain', 'name')
262     class Admin:
263         list_display = ('domain', 'name')
264-        search_fields = ('domain', 'name')
265 
266     def __str__(self):
267         return self.domain
268Index: django/contrib/admin/views/main.py
269===================================================================
270--- django/contrib/admin/views/main.py  (revision 3145)
271+++ django/contrib/admin/views/main.py  (working copy)
272@@ -710,12 +710,8 @@
273         qs = qs.order_by((self.order_type == 'desc' and '-' or '') + lookup_order_field)
274 
275         # Apply keyword searches.
276-        if self.lookup_opts.admin.search_fields and self.query:
277-            for bit in self.query.split():
278-                or_queries = [models.Q(**{'%s__icontains' % field_name: bit}) for field_name in self.lookup_opts.admin.search_fields]
279-                other_qs = QuerySet(self.model)
280-                other_qs = other_qs.filter(reduce(operator.or_, or_queries))
281-                qs = qs & other_qs
282+        if self.opts.search_fields and self.query:
283+            qs = qs.search(self.query)
284 
285         if self.opts.one_to_one_field:
286             qs = qs.complex_filter(self.opts.one_to_one_field.rel.limit_choices_to)
287Index: django/contrib/admin/templates/admin/search_form.html
288===================================================================
289--- django/contrib/admin/templates/admin/search_form.html       (revision 3145)
290+++ django/contrib/admin/templates/admin/search_form.html       (working copy)
291@@ -1,6 +1,6 @@
292 {% load adminmedia %}
293 {% load i18n %}
294-{% if cl.lookup_opts.admin.search_fields %}
295+{% if cl.opts.search_fields %}
296 <div id="toolbar"><form id="changelist-search" action="" method="get">
297 <div><!-- DIV needed for valid HTML -->
298 <label for="searchbar"><img src="{% admin_media_prefix %}img/admin/icon_searchbox.png" alt="Search" /></label>
299Index: docs/generic_views.txt
300===================================================================
301--- docs/generic_views.txt      (revision 3145)
302+++ docs/generic_views.txt      (working copy)
303@@ -673,6 +673,10 @@
304     * ``mimetype``: The MIME type to use for the resulting document. Defaults
305       to the value of the ``DEFAULT_MIME_TYPE`` setting.
306 
307+    * ``allow_search``: A boolean specifying whether searching is allowed
308+      for theese objects. If this is ``False``, objects will not be
309+      searchable. By default, this is ``True``.
310+
311 **Template name:**
312 
313 If ``template_name`` isn't specified, this view will use the template
314@@ -713,6 +717,9 @@
315     * ``hits``: The total number of objects across *all* pages, not just this
316       page.
317 
318+    * ``query``: The search query string. if searching is disabled,
319+      this is an empty string.
320+
321 Notes on pagination
322 ~~~~~~~~~~~~~~~~~~~
323