From ee607918a1f5d6ffbbb654d89f936e09d72a93b7 Mon Sep 17 00:00:00 2001 From: Emilie Date: Fri, 14 Oct 2016 12:08:28 +0200 Subject: [PATCH] Search : gather selected Pages in search result --- app/local_settings.py | 9 ++ app/organization/core/managers.py | 166 ++++++++++++++++++++++++++++++ app/organization/core/views.py | 29 ++---- app/organization/pages/models.py | 6 +- 4 files changed, 191 insertions(+), 19 deletions(-) create mode 100644 app/organization/core/managers.py diff --git a/app/local_settings.py b/app/local_settings.py index 1f154534..9d4386cb 100644 --- a/app/local_settings.py +++ b/app/local_settings.py @@ -136,6 +136,15 @@ SEARCH_MODEL_CHOICES = ('organization-pages.CustomPage', 'organization-media.Audio', 'organization-media.Video', 'mezzanine_agenda.Event') + + +PAGES_MODELS = ('organization-pages.CustomPage', + 'organization-magazine.Topic', + 'organization-network.DepartmentPage', + 'organization-network.TeamPage', + 'organization-projects.ProjectTopicPage', + 'organization-pages.PageLink') + SEARCH_PER_PAGE = 10 MAX_PAGING_LINKS = 10 diff --git a/app/organization/core/managers.py b/app/organization/core/managers.py new file mode 100644 index 00000000..b16ae9db --- /dev/null +++ b/app/organization/core/managers.py @@ -0,0 +1,166 @@ +from __future__ import unicode_literals + +import django +from future.builtins import int, zip + +from functools import reduce +from operator import ior, iand +from string import punctuation + +from django.apps import apps, AppConfig +from django.core.exceptions import ImproperlyConfigured +from django.db.models import Manager, Q, CharField, TextField +from django.db.models.manager import ManagerDescriptor +from django.db.models.query import QuerySet +from django.contrib.sites.managers import CurrentSiteManager as DjangoCSM +from django.utils.timezone import now +from django.utils.translation import ugettext_lazy as _ + +from mezzanine.conf import settings +from mezzanine.utils.sites import current_site_id +from mezzanine.utils.urls import home_slug +from mezzanine.core.managers import search_fields_to_dict, SearchableQuerySet + +class CustomSearchableManager(Manager): + + """ + Manager providing a chainable queryset. + Adapted from http://www.djangosnippets.org/snippets/562/ + search method supports spanning across models that subclass the + model being used to search. + """ + + def __init__(self, *args, **kwargs): + self._search_fields = kwargs.pop("search_fields", {}) + super(CustomSearchableManager, self).__init__(*args, **kwargs) + + def get_search_fields(self): + """ + Returns the search field names mapped to weights as a dict.s + Used in ``get_queryset`` below to tell ``SearchableQuerySet`` + which search fields to use. Also used by ``DisplayableAdmin`` + to populate Django admin's ``search_fields`` attribute. + Search fields can be populated via + ``SearchableManager.__init__``, which then get stored in + ``SearchableManager._search_fields``, which serves as an + approach for defining an explicit set of fields to be used. + Alternatively and more commonly, ``search_fields`` can be + defined on models themselves. In this case, we look at the + model and all its base classes, and build up the search + fields from all of those, so the search fields are implicitly + built up from the inheritence chain. + Finally if no search fields have been defined at all, we + fall back to any fields that are ``CharField`` or ``TextField`` + instances. + """ + search_fields = self._search_fields.copy() + if not search_fields: + for cls in reversed(self.model.__mro__): + super_fields = getattr(cls, "search_fields", {}) + search_fields.update(search_fields_to_dict(super_fields)) + if not search_fields: + search_fields = [] + for f in self.model._meta.fields: + if isinstance(f, (CharField, TextField)): + search_fields.append(f.name) + search_fields = search_fields_to_dict(search_fields) + return search_fields + + def get_queryset(self): + search_fields = self.get_search_fields() + return SearchableQuerySet(self.model, search_fields=search_fields) + + def contribute_to_class(self, model, name): + """ + Newer versions of Django explicitly prevent managers being + accessed from abstract classes, which is behaviour the search + API has always relied on. Here we reinstate it. + """ + super(CustomSearchableManager, self).contribute_to_class(model, name) + setattr(model, name, ManagerDescriptor(self)) + + def search(self, *args, **kwargs): + """ + Proxy to queryset's search method for the manager's model and + any models that subclass from this manager's model if the + model is abstract. + """ + if not settings.SEARCH_MODEL_CHOICES: + # No choices defined - build a list of leaf models (those + # without subclasses) that inherit from Displayable. + models = [m for m in apps.get_models() + if issubclass(m, self.model)] + parents = reduce(ior, [set(m._meta.get_parent_list()) + for m in models]) + models = [m for m in models if m not in parents] + elif getattr(self.model._meta, "abstract", False): + # When we're combining model subclasses for an abstract + # model (eg Displayable), we only want to use models that + # are represented by the ``SEARCH_MODEL_CHOICES`` setting. + # Now this setting won't contain an exact list of models + # we should use, since it can define superclass models such + # as ``Page``, so we check the parent class list of each + # model when determining whether a model falls within the + # ``SEARCH_MODEL_CHOICES`` setting. + search_choices = set() + models = set() + parents = set() + errors = [] + for name in settings.SEARCH_MODEL_CHOICES: + try: + model = apps.get_model(*name.split(".", 1)) + except LookupError: + errors.append(name) + else: + search_choices.add(model) + if errors: + raise ImproperlyConfigured("Could not load the model(s) " + "%s defined in the 'SEARCH_MODEL_CHOICES' setting." + % ", ".join(errors)) + + for model in apps.get_models(): + # Model is actually a subclasses of what we're + # searching (eg Displayabale) + is_subclass = issubclass(model, self.model) + # Model satisfies the search choices list - either + # there are no search choices, model is directly in + # search choices, or its parent is. + this_parents = set(model._meta.get_parent_list()) + in_choices = not search_choices or model in search_choices + in_choices = in_choices or this_parents & search_choices + if is_subclass and (in_choices or not search_choices): + # Add to models we'll seach. Also maintain a parent + # set, used below for further refinement of models + # list to search. + models.add(model) + parents.update(this_parents) + # Strip out any models that are superclasses of models, + # specifically the Page model which will generally be the + # superclass for all custom content types, since if we + # query the Page model as well, we will get duplicate + # results. + models -= parents + elif self.model.__name__ == "CustomPage": + # gather all pages defined in PAGES_MODELS settings + models = set() + errors = [] + for name in settings.PAGES_MODELS: + try: + models.add(apps.get_model(name)) + except LookupError: + errors.append(name) + if errors: + raise ImproperlyConfigured("Could not load the model(s) " + "%s defined in the 'SEARCH_MODEL_CHOICES' setting." + % ", ".join(errors)) + else: + models = [self.model] + all_results = [] + user = kwargs.pop("for_user", None) + for model in models: + try: + queryset = model.objects.published(for_user=user) + except AttributeError: + queryset = model.objects.get_queryset() + all_results.extend(queryset.search(*args, **kwargs)) + return sorted(all_results, key=lambda r: r.result_count, reverse=True) diff --git a/app/organization/core/views.py b/app/organization/core/views.py index 13718067..2b7bda42 100644 --- a/app/organization/core/views.py +++ b/app/organization/core/views.py @@ -8,6 +8,8 @@ from django.http import QueryDict from mezzanine.conf import settings from mezzanine.utils.views import paginate from organization.core.models import * +from functools import reduce +from operator import ior, iand class SlugMixin(object): @@ -17,18 +19,11 @@ class SlugMixin(object): return get_object_or_404(objects, slug=self.kwargs['slug']) -# class CustomDisplayableView(SlugMixin, DetailView): -# -# model = CustomDisplayable - - class CustomSearchView(TemplateView): template_name='search_results.html' - def get(self, request, *args, **kwargs): - """ Display search results. Takes an optional "contenttype" GET parameter in the form "app-name.ModelName" to limit search results to a single model. @@ -49,23 +44,23 @@ class CustomSearchView(TemplateView): search_type = search_model._meta.verbose_name_plural.capitalize() results = search_model.objects.search(query, for_user=request.user) - print("----------------------------") - print(results) - print("----------------------------") + # count objects filter_dict = dict() for result in results: classname = result.__class__.__name__ app_label = result._meta.app_label - + full_classname = app_label+"."+classname + verbose_name = result._meta.verbose_name # aggregate all Page types : CustomPage, TeamPage, Topic etc... if result._meta.get_parent_list() : parent_class = result._meta.get_parent_list()[0] - if parent_class.__name__ == 'Page': - classname = parent_class.__name__ - app_label = parent_class._meta.app_label + if full_classname in settings.PAGES_MODELS: + classname = "CustomPage" + verbose_name = "Page" + app_label = "organization-pages" elif "Video" in parent_class.__name__: classname = "Video" app_label = parent_class._meta.app_label @@ -77,18 +72,16 @@ class CustomSearchView(TemplateView): filter_dict[classname]['count'] += 1 else: filter_dict[classname] = {'count' : 1} - filter_dict[classname].update({'verbose_name' : classname}) + filter_dict[classname].update({'verbose_name' : verbose_name}) filter_dict[classname].update({'app_label' : app_label}) - - # get url param current_query = QueryDict(mutable=True) current_query = request.GET.copy() # generate filter url for key, value in filter_dict.items(): - current_query['type'] = value['app_label']+'.'+key + current_query['type'] = value['app_label']+'.'+ key filter_dict[key].update({'url' : request.path+"?"+current_query.urlencode(safe='/')}) # pagination diff --git a/app/organization/pages/models.py b/app/organization/pages/models.py index 387f92a6..f38143f2 100644 --- a/app/organization/pages/models.py +++ b/app/organization/pages/models.py @@ -5,10 +5,13 @@ from mezzanine.core.models import Displayable, Slugged, Orderable from mezzanine.pages.models import Link as MezzanineLink from organization.core.models import * from organization.media.models import * +from organization.core.managers import * class CustomPage(Page, SubTitled, RichText): + objects = CustomSearchableManager() + class Meta: verbose_name = 'custom page' @@ -56,7 +59,8 @@ class PageVideo(Video): class PageLink(Link): page = models.ForeignKey(Page, verbose_name=_('page'), related_name='links', blank=True, null=True, on_delete=models.SET_NULL) - + objects = CustomSearchableManager() + class Meta: verbose_name = _("link") verbose_name_plural = _("links") -- 2.39.5