Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
beniwohli committed Mar 30, 2011
0 parents commit 01c6a38
Show file tree
Hide file tree
Showing 14 changed files with 386 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
@@ -0,0 +1,2 @@
*.pyc
.idea
76 changes: 76 additions & 0 deletions 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 changes: 2 additions & 0 deletions cms_search/__init__.py
@@ -0,0 +1,2 @@
__author__ = 'benjamin.wohlwend'
__version__ = '0.1'
15 changes: 15 additions & 0 deletions 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 changes: 2 additions & 0 deletions cms_search/helpers/__init__.py
@@ -0,0 +1,2 @@
__author__ = 'benjamin.wohlwend'

22 changes: 22 additions & 0 deletions 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 changes: 52 additions & 0 deletions 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
Empty file added cms_search/helpers/models.py
Empty file.
2 changes: 2 additions & 0 deletions cms_search/helpers/templatetags/__init__.py
@@ -0,0 +1,2 @@
__author__ = 'benjamin.wohlwend'

69 changes: 69 additions & 0 deletions 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 changes: 53 additions & 0 deletions 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 changes: 49 additions & 0 deletions 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 changes: 8 additions & 0 deletions 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'

0 comments on commit 01c6a38

Please sign in to comment.