recherche: première ébauche
authorPatrick Hetu <patrick.hetu@auf.org>
Mon, 17 Aug 2015 18:03:45 +0000 (14:03 -0400)
committerPatrick Hetu <patrick.hetu@auf.org>
Mon, 17 Aug 2015 18:03:45 +0000 (14:03 -0400)
21 files changed:
project/aldryn_search/__init__.py [new file with mode: 0644]
project/aldryn_search/base.py [new file with mode: 0644]
project/aldryn_search/cms_app.py [new file with mode: 0644]
project/aldryn_search/conf.py [new file with mode: 0644]
project/aldryn_search/helpers.py [new file with mode: 0644]
project/aldryn_search/locale/de/LC_MESSAGES/django.mo [new file with mode: 0644]
project/aldryn_search/locale/de/LC_MESSAGES/django.po [new file with mode: 0644]
project/aldryn_search/locale/en/LC_MESSAGES/django.mo [new file with mode: 0644]
project/aldryn_search/locale/en/LC_MESSAGES/django.po [new file with mode: 0644]
project/aldryn_search/models.py [new file with mode: 0644]
project/aldryn_search/router.py [new file with mode: 0644]
project/aldryn_search/search_indexes.py [new file with mode: 0644]
project/aldryn_search/templates/aldryn_search/base.html [new file with mode: 0644]
project/aldryn_search/templates/aldryn_search/includes/pagination.html [new file with mode: 0644]
project/aldryn_search/templates/aldryn_search/includes/search_items.html [new file with mode: 0644]
project/aldryn_search/templates/aldryn_search/search_results.html [new file with mode: 0644]
project/aldryn_search/tests.py [new file with mode: 0644]
project/aldryn_search/utils.py [new file with mode: 0644]
project/aldryn_search/views.py [new file with mode: 0644]
project/framonde/search_indexes.py [new file with mode: 0644]
project/settings/60-haystack.py [new file with mode: 0644]

diff --git a/project/aldryn_search/__init__.py b/project/aldryn_search/__init__.py
new file mode 100644 (file)
index 0000000..44b1806
--- /dev/null
@@ -0,0 +1 @@
+__version__ = '0.2.6'
diff --git a/project/aldryn_search/base.py b/project/aldryn_search/base.py
new file mode 100644 (file)
index 0000000..c667dec
--- /dev/null
@@ -0,0 +1,150 @@
+# -*- coding: utf-8 -*-
+import warnings
+from django.utils.translation import override
+
+from haystack import indexes
+
+from .conf import settings
+from .helpers import get_request
+from .utils import clean_join, _get_language_from_alias_func
+
+
+language_from_alias = _get_language_from_alias_func()
+
+
+class AbstractIndex(indexes.SearchIndex):
+    text = indexes.CharField(document=True, use_template=False)
+
+    def _get_backend(self, using):
+        """
+        We set the backend to allow easy access for things like document search.
+        """
+        self._backend = super(AbstractIndex, self)._get_backend(using)
+        self._backend_alias = using
+        return self._backend
+
+    def index_queryset(self, using=None):
+        self._get_backend(using)
+        language = self.get_current_language(using)
+        filter_kwargs = self.get_index_kwargs(language)
+        qs = self.get_index_queryset(language)
+        return (qs.filter(**filter_kwargs))
+
+    def get_index_queryset(self, language):
+        return self.get_model().objects.all()
+
+    def prepare(self, obj):
+        current_language = self.get_current_language(
+            using=self._backend_alias, obj=obj)
+
+        with override(current_language):
+            request = self.get_request_instance(obj, current_language)
+            self.prepared_data = super(AbstractIndex, self).prepare(obj)
+            self.prepared_data['text'] = self.get_search_data(
+                obj, current_language, request)
+            self.prepare_fields(obj, current_language, request)
+            return self.prepared_data
+
+    def get_request_instance(self, obj, language):
+        return get_request(language)
+
+    def get_language(self, obj):
+        """
+        Equivalent to self.prepare_language.
+        """
+        return None
+
+    def get_current_language(self, using=None, obj=None):
+        """
+        Helper method bound to ALWAYS return a language.
+
+        When obj is not None, this calls self.get_language to try and get a language from obj,
+        this is useful when the object itself defines it's language in a "language" field.
+
+        If no language was found or obj is None, then we call self.get_default_language to try and get a fallback language.
+        """
+        language = self.get_language(obj) if obj else None
+        return language or self.get_default_language(using)
+
+    def get_default_language(self, using):
+        """
+        When using multiple languages, this allows us to specify a fallback based on the
+        backend being used.
+        """
+        language = None
+
+        if using and language_from_alias:
+            language = language_from_alias(using)
+        return language or settings.ALDRYN_SEARCH_DEFAULT_LANGUAGE
+
+    def get_index_kwargs(self, language):
+        """
+        This is called to filter the index queryset.
+        """
+        return {}
+
+    def get_search_data(self, obj, language, request):
+        """
+        Returns a string that will be used to populate the text field (primary field).
+        """
+        return ''
+
+    def prepare_fields(self, obj, language, request):
+        """
+        This is called to prepare any extra fields.
+        """
+        pass
+
+
+class AldrynIndexBase(AbstractIndex):
+    # For some apps it makes sense to turn on the title indexing.
+    index_title = False
+
+    language = indexes.CharField()
+    description = indexes.CharField(indexed=False, stored=True, null=True)
+    pub_date = indexes.DateTimeField(null=True)
+    login_required = indexes.BooleanField(default=False)
+    url = indexes.CharField(stored=True, indexed=False)
+    title = indexes.CharField(stored=True, indexed=False)
+    site_id = indexes.IntegerField(stored=True, indexed=True, null=True)
+
+    def __init__(self):
+        if hasattr(self, 'INDEX_TITLE'):
+            warning_message = 'AldrynIndexBase.INDEX_TITLE is deprecated; use AldrynIndexBase.index_title instead'
+            warnings.warn(warning_message, PendingDeprecationWarning)
+        super(AldrynIndexBase, self).__init__()
+
+    def get_url(self, obj):
+        """
+        Equivalent to self.prepare_url.
+        """
+        if not obj.get_absolute_url():
+            print obj
+        return obj.get_absolute_url()
+
+    def get_title(self, obj):
+        """
+        Equivalent to self.prepare_title.
+        """
+        return None
+
+    def get_description(self, obj):
+        """
+        Equivalent to self.prepare_description.
+        """
+        return None
+
+    def prepare_fields(self, obj, language, request):
+        self.prepared_data['language'] = language
+        # We set the following fields here because on some models,
+        # the value of these fields is dependent on the active language
+        # this being the case we extrapolate the language hacks.
+        self.prepared_data['url'] = self.get_url(obj)
+        self.prepared_data['title'] = self.get_title(obj)
+        self.prepared_data['description'] = self.get_description(obj)
+
+        if self.index_title or getattr(self, 'INDEX_TITLE', False):
+            prepared_text = self.prepared_data['text']
+            prepared_title = self.prepared_data['title']
+            self.prepared_data['text'] = clean_join(
+                ' ', [prepared_title, prepared_text])
diff --git a/project/aldryn_search/cms_app.py b/project/aldryn_search/cms_app.py
new file mode 100644 (file)
index 0000000..c6366b1
--- /dev/null
@@ -0,0 +1,20 @@
+from django.conf.urls import patterns, url
+from django.utils.translation import ugettext_lazy as _
+
+from cms.app_base import CMSApp
+from cms.apphook_pool import apphook_pool
+
+from .views import AldrynSearchView
+
+from .conf import settings
+
+
+class AldrynSearchApphook(CMSApp):
+    name = _("aldryn search")
+    urls = [patterns('',
+        url('^$', AldrynSearchView.as_view(), name='aldryn-search'),
+    ),]
+
+
+if settings.ALDRYN_SEARCH_REGISTER_APPHOOK:
+    apphook_pool.register(AldrynSearchApphook)
diff --git a/project/aldryn_search/conf.py b/project/aldryn_search/conf.py
new file mode 100644 (file)
index 0000000..c293732
--- /dev/null
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+from django.conf import settings
+
+from appconf import AppConf
+
+
+class AldrynSearchAppConf(AppConf):
+
+    CMS_PAGE = True
+    DEFAULT_LANGUAGE = settings.LANGUAGE_CODE
+    INDEX_BASE_CLASS = 'aldryn_search.base.AldrynIndexBase'
+    LANGUAGE_FROM_ALIAS = 'aldryn_search.utils.language_from_alias'
+    PAGINATION = 10
+    REGISTER_APPHOOK = True
+
+    class Meta:
+        prefix = 'ALDRYN_SEARCH'
diff --git a/project/aldryn_search/helpers.py b/project/aldryn_search/helpers.py
new file mode 100644 (file)
index 0000000..f6a4638
--- /dev/null
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+from django.contrib.auth.models import AnonymousUser
+from django.template import RequestContext
+from django.test import RequestFactory
+from django.utils.text import smart_split
+try:
+    from django.utils.encoding import force_unicode
+except ImportError:
+    from django.utils.encoding import force_text as force_unicode
+
+from .conf import settings
+from .utils import get_field_value, strip_tags
+
+
+def get_cleaned_bits(data):
+    decoded = force_unicode(data)
+    stripped = strip_tags(decoded)
+    return smart_split(stripped)
+
+
+def get_plugin_index_data(base_plugin, request):
+    text_bits = []
+
+    instance, plugin_type = base_plugin.get_plugin_instance()
+
+    if instance is None:
+        # this is an empty plugin
+        return text_bits
+
+    search_fields = getattr(instance, 'search_fields', [])
+
+    if hasattr(instance, 'search_fulltext'):
+        # check if the plugin instance has search enabled
+        search_contents = instance.search_fulltext
+    elif hasattr(base_plugin, 'search_fulltext'):
+        # now check in the base plugin instance (CMSPlugin)
+        search_contents = base_plugin.search_fulltext
+    elif hasattr(plugin_type, 'search_fulltext'):
+        # last check in the plugin class (CMSPluginBase)
+        search_contents = plugin_type.search_fulltext
+    else:
+        # disabled if there's search fields defined,
+        # otherwise it's enabled.
+        search_contents = not bool(search_fields)
+
+    if search_contents:
+        plugin_contents = instance.render_plugin(
+            context=RequestContext(request))
+
+        if plugin_contents:
+            text_bits = get_cleaned_bits(plugin_contents)
+    else:
+        values = (get_field_value(instance, field) for field in search_fields)
+
+        for value in values:
+            cleaned_bits = get_cleaned_bits(value or '')
+            text_bits.extend(cleaned_bits)
+    return text_bits
+
+
+def get_request(language=None):
+    """
+    Returns a Request instance populated with cms specific attributes.
+    """
+    request_factory = RequestFactory(HTTP_HOST=settings.ALLOWED_HOSTS[0])
+    request = request_factory.get("/")
+    request.session = {}
+    request.LANGUAGE_CODE = language or settings.LANGUAGE_CODE
+    # Needed for plugin rendering.
+    request.current_page = None
+    request.user = AnonymousUser()
+    return request
diff --git a/project/aldryn_search/locale/de/LC_MESSAGES/django.mo b/project/aldryn_search/locale/de/LC_MESSAGES/django.mo
new file mode 100644 (file)
index 0000000..39bab21
Binary files /dev/null and b/project/aldryn_search/locale/de/LC_MESSAGES/django.mo differ
diff --git a/project/aldryn_search/locale/de/LC_MESSAGES/django.po b/project/aldryn_search/locale/de/LC_MESSAGES/django.po
new file mode 100644 (file)
index 0000000..5d53494
--- /dev/null
@@ -0,0 +1,53 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-07-24 16:57+0200\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: cms_app.py:13
+msgid "aldryn search"
+msgstr "Aldryn Suche"
+
+#: templates/aldryn_search/search_results.html:6
+msgid "enter your search term"
+msgstr "Suchbegriffe eingeben"
+
+#: templates/aldryn_search/search_results.html:16
+msgid "results"
+msgstr "Resultaten"
+
+#: templates/aldryn_search/includes/pagination.html:7
+#, python-format
+msgid "%(from)s – %(until)s of %(count)s %(obj_string)s are displayed"
+msgstr "%(from)s – %(until)s von %(count)s %(obj_string)s werden angezeigt"
+
+#: templates/aldryn_search/includes/pagination.html:11
+msgid "previous"
+msgstr ""
+
+#: templates/aldryn_search/includes/pagination.html:25
+msgid "next"
+msgstr ""
+
+#: templates/aldryn_search/includes/search_items.html:12
+msgid "No results found for"
+msgstr ""
+
+#~ msgid "Displaying all %(count)s %(obj_string)s"
+#~ msgstr "Alle %(count)s %(obj_string)s werden angezeigt"
+
+#~ msgid "Displaying all %(count)s objects"
+#~ msgstr "Alle %(count)s Objekte werden angezeigt"
diff --git a/project/aldryn_search/locale/en/LC_MESSAGES/django.mo b/project/aldryn_search/locale/en/LC_MESSAGES/django.mo
new file mode 100644 (file)
index 0000000..31092e8
Binary files /dev/null and b/project/aldryn_search/locale/en/LC_MESSAGES/django.mo differ
diff --git a/project/aldryn_search/locale/en/LC_MESSAGES/django.po b/project/aldryn_search/locale/en/LC_MESSAGES/django.po
new file mode 100644 (file)
index 0000000..1b400f8
--- /dev/null
@@ -0,0 +1,47 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-07-24 16:57+0200\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: cms_app.py:13
+msgid "aldryn search"
+msgstr ""
+
+#: templates/aldryn_search/search_results.html:6
+msgid "enter your search term"
+msgstr ""
+
+#: templates/aldryn_search/search_results.html:16
+msgid "results"
+msgstr ""
+
+#: templates/aldryn_search/includes/pagination.html:7
+#, python-format
+msgid "%(from)s – %(until)s of %(count)s %(obj_string)s are displayed"
+msgstr ""
+
+#: templates/aldryn_search/includes/pagination.html:11
+msgid "previous"
+msgstr ""
+
+#: templates/aldryn_search/includes/pagination.html:25
+msgid "next"
+msgstr ""
+
+#: templates/aldryn_search/includes/search_items.html:12
+msgid "No results found for"
+msgstr ""
diff --git a/project/aldryn_search/models.py b/project/aldryn_search/models.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/project/aldryn_search/router.py b/project/aldryn_search/router.py
new file mode 100644 (file)
index 0000000..9da94c2
--- /dev/null
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+from django.conf import settings
+from django.utils.translation import get_language
+
+from haystack import routers
+from haystack.constants import DEFAULT_ALIAS
+
+
+class LanguageRouter(routers.BaseRouter):
+
+    def for_read(self, **hints):
+        language = get_language()
+        if language not in settings.HAYSTACK_CONNECTIONS:
+            return DEFAULT_ALIAS
+        return language
+
+    def for_write(self, **hints):
+        language = get_language()
+        if language not in settings.HAYSTACK_CONNECTIONS:
+            return DEFAULT_ALIAS
+        return language
diff --git a/project/aldryn_search/search_indexes.py b/project/aldryn_search/search_indexes.py
new file mode 100644 (file)
index 0000000..8e671d0
--- /dev/null
@@ -0,0 +1,89 @@
+from django.db.models import Q
+from django.utils import timezone
+
+from cms.models import CMSPlugin, Title
+
+from .conf import settings
+from .helpers import get_plugin_index_data
+from .utils import clean_join, get_index_base, strip_tags
+
+
+# Backwards compatibility
+_strip_tags = strip_tags
+
+
+class TitleIndex(get_index_base()):
+    index_title = True
+
+    haystack_use_for_indexing = settings.ALDRYN_SEARCH_CMS_PAGE
+
+    def prepare_pub_date(self, obj):
+        return obj.page.publication_date
+
+    def prepare_login_required(self, obj):
+        return obj.page.login_required
+
+    def prepare_site_id(self, obj):
+        return obj.page.site_id
+
+    def get_language(self, obj):
+        return obj.language
+
+    def get_url(self, obj):
+        return obj.page.get_absolute_url()
+
+    def get_title(self, obj):
+        return obj.title
+
+    def get_description(self, obj):
+        return obj.meta_description or None
+
+    def get_plugin_queryset(self, language):
+        queryset = CMSPlugin.objects.filter(language=language)
+        return queryset
+
+    def get_search_data(self, obj, language, request):
+        current_page = obj.page
+        placeholders = current_page.placeholders.all()
+        plugins = self.get_plugin_queryset(
+            language).filter(placeholder__in=placeholders)
+        text_bits = []
+
+        for base_plugin in plugins:
+            plugin_text_content = self.get_plugin_search_text(
+                base_plugin, request)
+            text_bits.append(plugin_text_content)
+
+        page_meta_description = current_page.get_meta_description(
+            fallback=False, language=language)
+
+        if page_meta_description:
+            text_bits.append(page_meta_description)
+
+        page_meta_keywords = getattr(current_page, 'get_meta_keywords', None)
+
+        if callable(page_meta_keywords):
+            text_bits.append(page_meta_keywords())
+
+        return clean_join(' ', text_bits)
+
+    def get_plugin_search_text(self, base_plugin, request):
+        try:
+            plugin_content_bits = get_plugin_index_data(base_plugin, request)
+        except:
+            return ''
+        return clean_join(' ', plugin_content_bits)
+
+    def get_model(self):
+        return Title
+
+    def get_index_queryset(self, language):
+        queryset = Title.objects.public().filter(
+            Q(page__publication_date__lt=timezone.now()) | Q(
+                page__publication_date__isnull=True),
+            Q(page__publication_end_date__gte=timezone.now()) | Q(
+                page__publication_end_date__isnull=True),
+            Q(redirect__exact='') | Q(redirect__isnull=True),
+            language=language
+        ).select_related('page').distinct()
+        return queryset
diff --git a/project/aldryn_search/templates/aldryn_search/base.html b/project/aldryn_search/templates/aldryn_search/base.html
new file mode 100644 (file)
index 0000000..527e7de
--- /dev/null
@@ -0,0 +1,7 @@
+{% extends "base.html" %}
+
+{% block content %}
+<div class="plugin-search">
+       {% block content_search %}{% endblock %}
+</div>
+{% endblock %}
\ No newline at end of file
diff --git a/project/aldryn_search/templates/aldryn_search/includes/pagination.html b/project/aldryn_search/templates/aldryn_search/includes/pagination.html
new file mode 100644 (file)
index 0000000..942b4f7
--- /dev/null
@@ -0,0 +1,29 @@
+{% load i18n spurl %}
+
+{% if page_obj.has_other_pages %}
+
+<div class="pagenav">
+       {% with from=page_obj.start_index until=page_obj.end_index count=page_obj.paginator.count %}
+       <p>{% blocktrans %}{{ from }} – {{ until }} of {{ count }} {{ obj_string }} are displayed{% endblocktrans %}</p>
+       {% endwith %}
+       <ul>
+               {% if page_obj.has_previous %}
+               <li class="prev"><a href="{% spurl base='' query=request.GET set_query='page={{ page_obj.previous_page_number }}' %}">&laquo; {% trans "previous" %}</a></li>
+               {% endif %}
+               {% for page_num in page_obj.page_range %}
+               {% if page_num %}
+               {% if page_obj.number == page_num %}
+               <li class="page active"><span>{{ page_num }}</span></li>
+               {% else %}
+               <li class="page"><a href="{% spurl base='' query=request.GET set_query='page={{ page_num }}' %}">{{ page_num }}</a></li>
+               {% endif %}
+               {% else %}
+               <li class="jumper"><span>...</span></li>
+               {% endif %}
+               {% endfor %}
+               {% if page_obj.has_next %}
+               <li class="prev"><a href="{% spurl base='' query=request.GET set_query='page={{ page_obj.next_page_number }}' %}">{% trans "next" %} &raquo;</a></li>
+               {% endif %}
+       </ul>
+</div>
+{% endif %}
\ No newline at end of file
diff --git a/project/aldryn_search/templates/aldryn_search/includes/search_items.html b/project/aldryn_search/templates/aldryn_search/includes/search_items.html
new file mode 100644 (file)
index 0000000..0d65bff
--- /dev/null
@@ -0,0 +1,13 @@
+{% load i18n highlight %}
+
+{% for result in page_obj.object_list %}
+<li>
+       <h3><a href="{{ result.url }}">{{ result.title }}</a></h3>
+       {% if result.text %}
+       <p>{% highlight result.text with request.GET.q %}</p>
+       {% endif %}
+       <p><a href="{{ result.url }}">{{ result.url }}</a></p>
+</li>
+{% empty %}
+<li>{% trans "No results found for" %} "{{ request.GET.q }}"</li>
+{% endfor %}
\ No newline at end of file
diff --git a/project/aldryn_search/templates/aldryn_search/search_results.html b/project/aldryn_search/templates/aldryn_search/search_results.html
new file mode 100644 (file)
index 0000000..ce5b5a1
--- /dev/null
@@ -0,0 +1,17 @@
+{% extends "aldryn_search/base.html" %}
+{% load i18n standard_form %}
+
+{% block content_search %}
+<form action="." method="get" class="frm frm-search">
+       {% standard_widget form.q options custom_class='input-large' placeholder=_('enter your search term') %}
+       {% standard_submit %}
+</form>
+
+<div class="search-results">
+       <ul>
+               {% include "aldryn_search/includes/search_items.html" %}
+       </ul>
+</div>
+
+{% include "aldryn_search/includes/pagination.html" with obj_string=_('results') %}
+{% endblock %}
\ No newline at end of file
diff --git a/project/aldryn_search/tests.py b/project/aldryn_search/tests.py
new file mode 100644 (file)
index 0000000..dabb191
--- /dev/null
@@ -0,0 +1,123 @@
+from django.template import Template
+from django.test import TestCase
+from django.test.utils import override_settings
+
+from cms.plugin_base import CMSPluginBase
+from cms.plugin_pool import plugin_pool
+from cms.models.placeholdermodel import Placeholder
+from cms.models import CMSPlugin
+
+from aldryn_search.search_indexes import TitleIndex
+from .helpers import get_plugin_index_data, get_request
+
+
+test_settings = {
+    'ALLOWED_HOSTS': ['localhost'],
+    'CMS_LANGUAGES': {1: [{'code': 'en', 'name': 'English'}]},
+    'CMS_TEMPLATES': (("whee.html", "Whee Template"),),
+    'LANGUAGES': (('en', 'English'),),
+    'LANGUAGE_CODE': 'en',
+    'TEMPLATE_LOADERS': ('aldryn_search.tests.FakeTemplateLoader',),
+}
+
+
+class FakeTemplateLoader(object):
+    is_usable = True
+
+    def __init__(self, name, dirs):
+        pass
+
+    def __iter__(self):
+        yield self.__class__
+        yield "{{baz}}"
+
+
+class NotIndexedPlugin(CMSPluginBase):
+    model = CMSPlugin
+    plugin_content = 'rendered plugin content'
+    render_template = Template(plugin_content)
+
+    def render(self, context, instance, placeholder):
+        return context
+
+plugin_pool.register_plugin(NotIndexedPlugin)
+
+
+@override_settings(**test_settings)
+class PluginIndexingTests(TestCase):
+
+    def setUp(self):
+        self.index = TitleIndex()
+        self.request = get_request(language='en')
+
+    def get_plugin(self):
+        instance = CMSPlugin(
+            language='en',
+            plugin_type="NotIndexedPlugin",
+            placeholder=Placeholder(id=1235)
+        )
+        instance.cmsplugin_ptr = instance
+        instance.pk = 1234  # otherwise plugin_meta_context_processor() crashes
+        return instance
+
+    def test_plugin_indexing_is_enabled_by_default(self):
+        cms_plugin = self.get_plugin()
+        indexed_content = self.index.get_plugin_search_text(
+            cms_plugin, self.request)
+        self.assertEqual(NotIndexedPlugin.plugin_content, indexed_content)
+
+    def test_plugin_indexing_can_be_disabled_on_model(self):
+        cms_plugin = self.get_plugin()
+        cms_plugin.search_fulltext = False
+        indexed_content = self.index.get_plugin_search_text(
+            cms_plugin, self.request)
+        self.assertEqual('', indexed_content)
+
+    def test_plugin_indexing_can_be_disabled_on_plugin(self):
+        NotIndexedPlugin.search_fulltext = False
+
+        try:
+            self.assertEqual(
+                '', self.index.get_plugin_search_text(self.get_plugin(), self.request))
+        finally:
+            del NotIndexedPlugin.search_fulltext
+
+    def test_page_title_is_indexed_using_prepare(self):
+        """This tests the indexing path way used by update_index mgmt command"""
+        from cms.api import create_page
+        page = create_page(
+            title="Whoopee", template="whee.html", language="en")
+
+        from haystack import connections
+        from haystack.constants import DEFAULT_ALIAS
+        search_conn = connections[DEFAULT_ALIAS]
+        unified_index = search_conn.get_unified_index()
+
+        from cms.models import Title
+        index = unified_index.get_index(Title)
+
+        title = Title.objects.get(pk=page.title_set.all()[0].pk)
+        index.index_queryset(DEFAULT_ALIAS)  # initialises index._backend_alias
+        indexed = index.prepare(title)
+        self.assertEqual('Whoopee', indexed['title'])
+        self.assertEqual('Whoopee', indexed['text'])
+
+    def test_page_title_is_indexed_using_update_object(self):
+        """This tests the indexing path way used by the RealTimeSignalProcessor"""
+        from cms.api import create_page
+        page = create_page(
+            title="Whoopee", template="whee.html", language="en")
+
+        from haystack import connections
+        from haystack.constants import DEFAULT_ALIAS
+        search_conn = connections[DEFAULT_ALIAS]
+        unified_index = search_conn.get_unified_index()
+
+        from cms.models import Title
+        index = unified_index.get_index(Title)
+
+        title = Title.objects.get(pk=page.title_set.all()[0].pk)
+        index.update_object(title, using=DEFAULT_ALIAS)
+        indexed = index.prepared_data
+        self.assertEqual('Whoopee', indexed['title'])
+        self.assertEqual('Whoopee', indexed['text'])
diff --git a/project/aldryn_search/utils.py b/project/aldryn_search/utils.py
new file mode 100644 (file)
index 0000000..e0a720a
--- /dev/null
@@ -0,0 +1,148 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+import six
+
+from lxml.html.clean import Cleaner as LxmlCleaner
+
+from django.core.exceptions import ImproperlyConfigured
+from django.db import models
+try:
+    from django.utils.encoding import force_unicode
+except ImportError:
+    from django.utils.encoding import force_text as force_unicode
+from django.utils.importlib import import_module
+from django.utils.html import strip_tags as _strip_tags
+
+from haystack import DEFAULT_ALIAS
+from haystack.indexes import SearchIndex
+
+from cms.utils.i18n import get_language_code
+
+from .conf import settings
+
+
+def alias_from_language(language):
+    """
+    Returns alias if alias is a valid language.
+    """
+    language = get_language_code(language)
+
+    if language == settings.ALDRYN_SEARCH_DEFAULT_LANGUAGE:
+        return DEFAULT_ALIAS
+    return language
+
+
+def clean_join(separator, iterable):
+    """
+    Filters out iterable to only join non empty items.
+    """
+    return separator.join(filter(None, iterable))
+
+
+def get_callable(string_or_callable):
+    """
+    If given a callable then it returns it, otherwise it resolves the path
+    and returns an object.
+    """
+    if callable(string_or_callable):
+        return string_or_callable
+    else:
+        module_name, object_name = string_or_callable.rsplit('.', 1)
+        if module_name.startswith('aldryn_search'):
+            module_name = "project." + module_name
+        module = import_module(module_name)
+        return getattr(module, object_name)
+
+
+def _get_language_from_alias_func():
+    path_or_callable = settings.ALDRYN_SEARCH_LANGUAGE_FROM_ALIAS
+
+    if path_or_callable:
+        try:
+            func = get_callable(path_or_callable)
+        except AttributeError as error:
+            raise ImproperlyConfigured(
+                'ALDRYN_SEARCH_LANGUAGE_FROM_ALIAS: %s' % (str(error)))
+        if not callable(func):
+            raise ImproperlyConfigured(
+                'ALDRYN_SEARCH_LANGUAGE_FROM_ALIAS: %s is not callable' % func)
+    else:
+        func = None
+    return func
+
+
+def get_index_base():
+    index_string = settings.ALDRYN_SEARCH_INDEX_BASE_CLASS
+    try:
+        BaseClass = get_callable(index_string)
+    except AttributeError as error:
+        raise ImproperlyConfigured(
+            'ALDRYN_SEARCH_INDEX_BASE_CLASS: %s' % (str(error)))
+
+    if not issubclass(BaseClass, SearchIndex):
+        raise ImproperlyConfigured(
+            'ALDRYN_SEARCH_INDEX_BASE_CLASS: %s is not a subclass of haystack.indexes.SearchIndex' % index_string)
+
+    required_fields = ['text', 'language']
+
+    if not all(field in BaseClass.fields for field in required_fields):
+        raise ImproperlyConfigured('ALDRYN_SEARCH_INDEX_BASE_CLASS: %s must contain at least these fields: %s' % (
+            index_string, required_fields))
+    return BaseClass
+
+
+def language_from_alias(alias):
+    """
+    Returns alias if alias is a valid language.
+    """
+    languages = [language[0] for language in settings.LANGUAGES]
+
+    return alias if alias in languages else None
+
+
+def get_field_value(obj, name):
+    """
+    Given a model instance and a field name (or attribute),
+    returns the value of the field or an empty string.
+    """
+    fields = name.split('__')
+
+    name = fields[0]
+
+    try:
+        obj._meta.get_field(name)
+    except (AttributeError, models.FieldDoesNotExist):
+        # we catch attribute error because obj will not always be a model
+        # specially when going through multiple relationships.
+        value = getattr(obj, name, None) or ''
+    else:
+        value = getattr(obj, name)
+
+    if len(fields) > 1:
+        remaining = '__'.join(fields[1:])
+        return get_field_value(value, remaining)
+    return value
+
+
+def get_model_path(model_or_string):
+    if not isinstance(model_or_string, six.string_types):
+        # it's a model class
+        app_label = model_or_string._meta.app_label
+        model_name = model_or_string._meta.object_name
+        model_or_string = '{0}.{1}'.format(app_label, model_name)
+    return model_or_string.lower()
+
+
+def strip_tags(value):
+    """
+    Returns the given HTML with all tags stripped.
+    We use lxml to strip all js tags and then hand the result to django's strip tags.
+    """
+    # strip any new lines
+    value = value.strip()
+
+    if value:
+        partial_strip = LxmlCleaner().clean_html(value)
+        value = _strip_tags(partial_strip)
+    return value
diff --git a/project/aldryn_search/views.py b/project/aldryn_search/views.py
new file mode 100644 (file)
index 0000000..ea4d75b
--- /dev/null
@@ -0,0 +1,85 @@
+# -*- coding: utf-8 -*-
+import datetime
+
+from django.views.generic import ListView
+from django.views.generic.edit import FormMixin
+
+from django import forms
+
+from haystack.forms import SearchForm
+from haystack.query import SearchQuerySet
+
+
+from aldryn_common.paginator import DiggPaginator
+
+from .conf import settings
+
+
+class AldrynFacetedSearchForm(SearchForm):
+    selected_facets = forms.CharField(required=False, widget=forms.HiddenInput)
+    courant = forms.BooleanField(required=False, widget=forms.HiddenInput)
+    cloture = forms.BooleanField(required=False, widget=forms.HiddenInput)
+
+    def search(self):
+        sqs = SearchQuerySet()
+        sqs = sqs.facet('bureaux').facet(
+            'section').facet('annee').facet('partenaire')
+
+        if self.q:
+            sqs = sqs.filter(content=sqs.query.clean(self.q))
+
+        if self.courant:
+            sqs = sqs.filter(date_fin__gte=datetime.date.today())
+        if self.cloture:
+            sqs = sqs.filter(date_fin__lt=datetime.date.today())
+
+        self.selected_facets = list(
+            set(self.selected_facets.split('&') + self.selected_facets_get))
+
+        for facet in self.selected_facets:
+            if "__" not in facet:
+                continue
+
+            field, value = facet.split("__", 1)
+
+            if value:
+                sqs = sqs.narrow(u'%s:"%s"' % (field, sqs.query.clean(value)))
+
+        return sqs
+
+
+class AldrynSearchView(FormMixin, ListView):
+    form_class = AldrynFacetedSearchForm
+
+    paginate_by = settings.ALDRYN_SEARCH_PAGINATION
+    paginator_class = DiggPaginator
+
+    template_name = 'aldryn_search/search_results.html'
+
+    def get(self, request, *args, **kwargs):
+        self.form = AldrynFacetedSearchForm(self.request.GET)
+        self.form.q = self.request.GET.get('q', '')
+        self.form.courant = self.request.GET.get('courant', '')
+        self.form.cloture = self.request.GET.get('cloture', '')
+        self.form.selected_facets = self.request.GET.get('selected_facets', '')
+        self.form.selected_facets_get = self.request.GET.getlist(
+            'selected_facets', [])
+        return super(AldrynSearchView, self).get(request, *args, **kwargs)
+
+    def get_queryset(self):
+        self.queryset = self.form.search()
+        if not self.request.user.is_authenticated():
+            self.queryset = self.queryset.exclude(login_required=True)
+        self.facet_counts = self.queryset.facet_counts()
+        return self.queryset.order_by('-date_pub')
+
+    def get_context_data(self, **kwargs):
+        context = super(AldrynSearchView, self).get_context_data(**kwargs)
+
+        context['form'] = self.form
+        context['facets'] = self.facet_counts
+        context['selected_facets'] = self.request.GET.getlist(
+            'selected_facets', [])
+        if self.object_list.query.backend.include_spelling:
+            context['suggestion'] = self.form.get_suggestion()
+        return context
diff --git a/project/framonde/search_indexes.py b/project/framonde/search_indexes.py
new file mode 100644 (file)
index 0000000..c05c4f0
--- /dev/null
@@ -0,0 +1,73 @@
+# encoding: utf-8
+import datetime
+
+from django.core.exceptions import ObjectDoesNotExist
+from haystack import indexes
+
+from .models import Communication, Contribution, Offre
+
+
+class AufIndex(indexes.SearchIndex):
+    text = indexes.NgramField(model_attr='texte', document=True, use_template=True)
+    title = indexes.NgramField(model_attr='titre')
+    annee = indexes.FacetField(stored=True, null=True)
+    regions = indexes.FacetMultiValueField(null=True, stored=True)
+    section = indexes.FacetField(stored=True, null=True)
+    date_pub = indexes.DateField(model_attr='date_pub', null=True)
+
+#    def prepare_regions(self, obj):
+#        try:
+#            return [b.nom for b in obj.bureau.all()]
+#        except ObjectDoesNotExist as e:
+#            print(e)
+#            return [u'Non précisé']
+
+    def prepare_annee(self, obj):
+        if obj.date_pub is not None:
+            return str(obj.date_pub.year)
+
+
+class CommunicationIndex(AufIndex, indexes.Indexable):
+
+    def get_model(self):
+        return Communication
+
+    def prepare_section(self, obj):
+        return u"Communication"
+
+    def prepare_date_pub(self, obj):
+        return obj.date_pub.date()
+
+    def index_queryset(self, using=None):
+        """Used when the entire index for model is updated."""
+        return Communication.objects.filter(status=3)
+
+
+class ContributionIndex(AufIndex, indexes.Indexable):
+
+    def get_model(self):
+        return Contribution
+
+    def prepare_section(self, obj):
+        return u"Contribution"
+
+    def prepare_date_pub(self, obj):
+        return obj.date_pub.date()
+
+    def index_queryset(self, using=None):
+        return Contribution.objects.filter(status=3)
+
+
+class OffreIndex(AufIndex, indexes.Indexable):
+
+    def get_model(self):
+        return Offre
+
+    def prepare_section(self, obj):
+        return u"Offre"
+
+    def prepare_date_pub(self, obj):
+        return obj.date_pub.date()
+
+    def index_queryset(self, using=None):
+        return Offre.objects.filter(status=3)
diff --git a/project/settings/60-haystack.py b/project/settings/60-haystack.py
new file mode 100644 (file)
index 0000000..5322daa
--- /dev/null
@@ -0,0 +1,17 @@
+INSTALLED_APPS += (
+    'project.aldryn_search',
+    'haystack',
+    'spurl',
+)
+
+HAYSTACK_CONNECTIONS = {
+    'default': {
+        'ENGINE': 'xapian_backend.XapianEngine',
+        'PATH': os.path.join(PROJECT_ROOT, 'xapian_index')
+    },
+}
+
+HAYSTACK_XAPIAN_LANGUAGE = "french"
+
+#HAYSTACK_ROUTERS = ['project.aldryn_search.router.LanguageRouter',]
+ALDRYN_SEARCH_INDEX_BASE_CLASS = 'project.framonde.search_indexes.AufIndex'