Browse files

initial commit

  • Loading branch information...
0 parents commit 01c6a38b0e179bcaef9b45cf8b1040dc82f8673d @piquadrat committed Mar 30, 2011
2 .gitignore
@@ -0,0 +1,2 @@
+*.pyc
+.idea
76 README.rst
@@ -0,0 +1,76 @@
+=================
+django-cms-search
+=================
+
+This package provides multilingual search indexes for easy Haystack integration with django CMS.
+
+Usage
+=====
+
+After installing django-cms-search through your package manager of choice, add ``cms_search`` to your
+``INSTALLED_APPS``. That's it.
+
+For setting up Haystack, please refer to their `documentation <http://docs.haystacksearch.org/dev/>`_.
+
+Customizing the Index
+---------------------
+
+You can customize what parts of a ``CMSPlugin`` end up in the index with two class attributes on ``CMSPlugin``
+subclasses:
+
+* ``search_fields``: a list of field names to index.
+* ``search_fulltext``: if ``True``, the index renders the plugin and adds the result (sans HTML tags) to the index.
+
+Helpers
+=======
+
+django-cms-search provides a couple of useful helpers to deal with multilingual content.
+
+``cms_search.helpers.indexes.MultiLanguageIndex``
+-------------------------------------------------
+
+A ``SearchIndex`` that dynamically adds translated fields to the search index. An example for when this is useful is the
+app hook infrastructure from django CMS. When a model's ``get_absolute_url`` uses a url pattern that is attached to an
+app hook, the URL varies depending on the language. A usage example::
+
+ from haystack import indexes
+ from cms_search.helpers.indexes import MultiLanguageIndex
+
+ class NewsIndex(MultiLanguageIndex):
+ text = indexes.CharField(document=True, use_template=True)
+ title = indexes.CharField(model_attr='title')
+ url = indexes.CharField(stored=True)
+
+ def prepare_url(self, obj):
+ return obj.get_absolute_url()
+
+ class HaystackTrans:
+ fields = ('url', 'title')
+
+A few things to note:
+
+* ``MultiLanguageIndex`` dynamically creates translated fields. The name of the dynamic fields is a concatenation of the
+ original field name, an underscore and the language code.
+* If you define a ``prepare`` method for a translated field, that method will be called multiple times, with changing
+ active language.
+* In the above example, you might want to catch ``NoReverseMatch`` exceptions if you don't have activated the app hook
+ for all languages defined in ``settings.LANGUAGES``.
+* The ``model_attr`` attribute is handled somewhat specially. The index tries to find a field on the model called
+ ``model_attr + '_' + language_code``. If it exists, it is used as the translated value. But it isn't possible to supply
+ the name of a model method and let the index call it with varying activated languages. Use ``prepare_myfieldname`` for
+ that case.
+
+``{% get_translated_value %}`` template tag
+-------------------------------------------
+
+This template tag is most useful in combination with the ``MultiLanguageIndex``. You can use it while looping through
+search results, and it will automatically pick up the translated field for the current language or fall back to another
+available language (in the order defined in ``settings.LANGUAGES``). Example::
+
+ {% load cms_search %}
+
+ <ul class="search-results">
+ {% for result in page.object_list %}
+ <li><a href="{% get_translated_value result "url" %}">{% get_translated_value result "title" %}</a></li>
+ {% endfor %}
+ </ul>
2 cms_search/__init__.py
@@ -0,0 +1,2 @@
+__author__ = 'benjamin.wohlwend'
+__version__ = '0.1'
15 cms_search/cms_app.py
@@ -0,0 +1,15 @@
+from django.conf.urls.defaults 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 haystack.views import search_view_factory
+
+class HaystackSearchApphook(CMSApp):
+ name = _("search apphook")
+ urls = [patterns('',
+ url('^$', search_view_factory(), name='haystack-search'),
+ ),]
+
+apphook_pool.register(HaystackSearchApphook)
2 cms_search/helpers/__init__.py
@@ -0,0 +1,2 @@
+__author__ = 'benjamin.wohlwend'
+
22 cms_search/helpers/fields.py
@@ -0,0 +1,22 @@
+from django.conf import settings
+from haystack import indexes
+from django.utils.translation import get_language, activate
+
+class MultiLangTemplateField(indexes.CharField):
+
+ def __init__(self, **kwargs):
+ kwargs['use_template'] = True
+ super(MultiLangTemplateField, self).__init__(**kwargs)
+
+ def prepare_template(self, obj):
+ content = []
+ current_lang = get_language()
+ try:
+ for lang, lang_name in settings.LANGUAGES:
+ activate(lang)
+ content.append(super(MultiLangTemplateField, self).prepare_template(obj))
+ finally:
+ activate(current_lang)
+
+ return '\n'.join(content)
+
52 cms_search/helpers/indexes.py
@@ -0,0 +1,52 @@
+import inspect
+
+from django.conf import settings
+from haystack import indexes
+from django.utils.translation import activate, get_language
+
+
+class MultiLangPrepareDecorator(object):
+ def __init__(self, language):
+ self.language = language
+
+ def __call__(self, func):
+ def wrapped(*args):
+ old_language = get_language()
+ activate(self.language)
+ try:
+ return func(*args)
+ finally:
+ activate(old_language)
+ return wrapped
+
+
+class MultiLanguageIndexBase(indexes.DeclarativeMetaclass):
+
+ @classmethod
+ def _get_field_copy(cls, field, language):
+ model_attr = field.model_attr
+ if model_attr:
+ model_attr += '_%s' % language.replace('-', '_')
+ arg_names = inspect.getargspec(indexes.SearchField.__init__)[0][2:]
+ kwargs = dict((arg_name, getattr(field, arg_name)) for arg_name in arg_names)
+ kwargs['model_attr'] = model_attr
+ copy = field.__class__(**kwargs)
+ copy.null = True
+ return copy
+
+ def __new__(cls, name, bases, attrs):
+ if 'HaystackTrans' in attrs:
+ for field in getattr(attrs['HaystackTrans'], 'fields', []):
+ if field not in attrs:
+ continue
+ for lang_tuple in settings.LANGUAGES:
+ lang = lang_tuple[0]
+ safe_lang = lang.replace('-', '_')
+ attrs['%s_%s' % (field, safe_lang)] = cls._get_field_copy(attrs[field], lang)
+ if 'prepare_' + field in attrs:
+ attrs['prepare_%s_%s' % (field, safe_lang)] = MultiLangPrepareDecorator(lang)(attrs['prepare_' + field])
+ return super(MultiLanguageIndexBase, cls).__new__(cls, name, bases, attrs)
+
+
+class MultiLanguageIndex(indexes.SearchIndex):
+ __metaclass__ = MultiLanguageIndexBase
0 cms_search/helpers/models.py
No changes.
2 cms_search/helpers/templatetags/__init__.py
@@ -0,0 +1,2 @@
+__author__ = 'benjamin.wohlwend'
+
69 cms_search/helpers/templatetags/cms_search.py
@@ -0,0 +1,69 @@
+from classytags.arguments import Argument
+from classytags.core import Options
+from classytags.helpers import AsTag
+import haystack
+from django import template
+from django.conf import settings
+from django.utils.translation import get_language
+
+register = template.Library()
+
+class GetTransFieldTag(AsTag):
+ """
+ Templatetag to access translated attributes of a `haystack.models.SearchResult`.
+
+ By default, it looks for a translated field at `field_name`_`language`. To
+ customize this, subclass `GetTransFieldTag` and override `get_translated_value`.
+
+ """
+ EMPTY_VALUE = ''
+ FALLBACK = True
+ name = 'get_translated_value'
+ options = Options(
+ Argument('obj'),
+ Argument('field_name'),
+ 'as',
+ Argument('varname', resolve=False, required=False)
+ )
+
+ def get_value(self, context, obj, field_name):
+ """
+ gets the translated value of field name. If `FALLBACK`evaluates to `True` and the field
+ has no translation for the current language, it tries to find a fallback value, using
+ the languages defined in `settings.LANGUAGES`.
+
+ """
+ language = get_language()
+ value = self.get_translated_value(obj, field_name, language)
+ if value:
+ return value
+ if self.FALLBACK:
+ for lang, lang_name in settings.LANGUAGES:
+ if lang == language:
+ # already tried this one...
+ continue
+ value = self.get_translated_value(obj, field_name, lang)
+ if value:
+ return value
+ untranslated = getattr(obj, field_name)
+ if self._is_truthy(untranslated):
+ return untranslated
+ else:
+ return self.EMPTY_VALUE
+
+ def get_translated_value(self, obj, field_name, language):
+ value = getattr(obj, '%s_%s' % (field_name, language), '')
+ if self._is_truthy(value):
+ return value
+ else:
+ return self.EMPTY_VALUE
+
+ def _is_truthy(self, value):
+ if isinstance(value, haystack.fields.NOT_PROVIDED):
+ return False
+ elif isinstance(value, basestring) and value.startswith('<haystack.fields.NOT_PROVIDED instance at '): #UUUGLY!!
+ return False
+ return bool(value)
+
+
+register.tag(GetTransFieldTag)
53 cms_search/models.py
@@ -0,0 +1,53 @@
+from cms.models import Page
+from cms.models.managers import PageManager
+from django.conf import settings
+from django.utils.translation import ugettext_lazy, string_concat, activate, get_language
+
+def proxy_name(language_code):
+ safe_code = language_code.replace('-', ' ').title().replace(' ', '_')
+ return 'Page_%s' % safe_code
+
+
+def page_proxy_factory(language_code, language_name):
+ def get_absolute_url(self):
+ if 'cms.middleware.multilingual.MultilingualURLMiddleware' in settings.MIDDLEWARE_CLASSES:
+ old_language = get_language()
+ try:
+ activate(language_code)
+ return '/%s%s' % (language_code, Page.get_absolute_url(self))
+ finally:
+ activate(old_language)
+ else:
+ return Page.get_absolute_url(self)
+
+ class Meta:
+ proxy = True
+ app_label = 'cms_search'
+ if len(settings.LANGUAGES) > 1:
+ verbose_name = string_concat(Page._meta.verbose_name, ' (', language_name, ')')
+ verbose_name_plural = string_concat(Page._meta.verbose_name_plural, ' (', language_name, ')')
+ else:
+ verbose_name = Page._meta.verbose_name
+ verbose_name_plural = Page._meta.verbose_name_plural
+
+ attrs = {'__module__': Page.__module__,
+ 'Meta': Meta,
+ 'objects': PageManager(),
+ 'get_absolute_url': get_absolute_url}
+
+ _PageProxy = type(proxy_name(language_code), (Page,), attrs)
+
+ _PageProxy._meta.parent_attr = 'parent'
+ _PageProxy._meta.left_attr = 'lft'
+ _PageProxy._meta.right_attr = 'rght'
+ _PageProxy._meta.tree_id_attr = 'tree_id'
+
+ return _PageProxy
+
+module_namespace = globals()
+
+for language_code, language_name in settings.LANGUAGES:
+ if isinstance(language_name, basestring):
+ language_name = ugettext_lazy(language_name)
+ proxy_model = page_proxy_factory(language_code, language_name)
+ module_namespace[proxy_model.__name__] = proxy_model
49 cms_search/search_indexes.py
@@ -0,0 +1,49 @@
+from django.conf import settings
+from django.utils.encoding import force_unicode
+from django.utils.html import strip_tags
+
+from haystack import indexes, site
+
+from cms.models.pluginmodel import CMSPlugin
+
+from cms_search import models as proxy_models
+
+def page_index_factory(language_code, proxy_model):
+
+ class _PageIndex(indexes.SearchIndex):
+ language = language_code
+
+ text = indexes.CharField(document=True, use_template=False)
+ pub_date = indexes.DateTimeField(model_attr='publication_date')
+ login_required = indexes.BooleanField(model_attr='login_required')
+ url = indexes.CharField(stored=True, indexed=False, model_attr='get_absolute_url')
+ title = indexes.CharField(stored=True, indexed=False, model_attr='get_title')
+
+ def prepare(self, obj):
+ self.prepared_data = super(_PageIndex, self).prepare(obj)
+ plugins = CMSPlugin.objects.filter(language=language_code, placeholder__in=obj.placeholders.all())
+ text = ''
+ for plugin in plugins:
+ instance, _ = plugin.get_plugin_instance()
+ if hasattr(instance, 'search_fields'):
+ text += u''.join(force_unicode(strip_tags(getattr(instance, field, ''))) for field in instance.search_fields)
+ if getattr(instance, 'search_fulltext', False):
+ text += strip_tags(instance.render_plugin())
+ self.prepared_data['text'] = text
+ return self.prepared_data
+
+ def get_queryset(self):
+ qs = proxy_model.objects.published().filter(title_set__language=language_code).distinct()
+ if 'publisher' in settings.INSTALLED_APPS:
+ qs = qs.filter(publisher_is_draft=True)
+ return qs
+
+ return _PageIndex
+
+for language_code, language_name in settings.LANGUAGES:
+ proxy_model = getattr(proxy_models, proxy_models.proxy_name(language_code))
+ index = page_index_factory(language_code, proxy_model)
+ if proxy_model:
+ site.register(proxy_model, index)
+ else:
+ print "no page proxy model found for language %s" % language_code
8 metadata.py
@@ -0,0 +1,8 @@
+package_name = 'cms_search'
+name = 'django-cms-search'
+author = 'Divio GmbH'
+author_email = 'developers@divio.ch'
+description = "An extension to django CMS to provide multilingual Haystack indexes"
+version = __import__(package_name).__version__
+project_url = 'http://github.com/divio/%s' % name
+license = 'BSD'
34 setup.py
@@ -0,0 +1,34 @@
+import metadata as m
+
+from setuptools import setup, find_packages
+
+install_requires = [
+ 'setuptools',
+]
+
+setup(
+ name = m.name,
+ version = m.version,
+ url = m.project_url,
+ license = m.license,
+ platforms=['OS Independent'],
+ description = m.description,
+ author = m.author,
+ author_email = m.author_email,
+ packages=find_packages(),
+ install_requires = install_requires,
+ include_package_data = True, #Accept all data files and directories matched by MANIFEST.in or found in source control.
+ package_dir = {
+ m.package_name:m.package_name,
+ },
+ zip_safe=False,
+ classifiers = [
+ 'Development Status :: 4 - Beta',
+ 'Framework :: Django',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: BSD License',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Topic :: Internet :: WWW/HTTP',
+ ]
+)

0 comments on commit 01c6a38

Please sign in to comment.