Namespaced Apphooks

Namespaced configuration for apphooks allows to have multiple instances of the same app be used in different locations in the page tree. This also provides the building blocks needed to have some extra configuration in the database to control some aspects of each instance of the app.

Basic concepts

The concept of apphook configuration is to store all the configuration in an applications-specific model, and let the developer specify the desired options in a form. In the views the configuration model instance specific for the current application namespace is loaded (through a mixin) and thus it is available in the view to provide the configuration for the current namespace.

Namespaces can be created on the fly in the Page admin Advanced settings.

When creating an application configuration, you are in fact defining a namespace, which is saved in the same field in the Page model as the plain namespaces.

step-by-step implementation

We’re going to create a new application called FAQ. It is a simple list of Frequently asked questions. And we’ll make it possible to setup multiple sets of FAQ Entries at different locations of the page tree, each with its individual set of entries.

Lets create our new FAQ app:

python manage.py startapp faq

models.py:

from aldryn_apphooks_config.fields import AppHookConfigField
from aldryn_apphooks_config.managers import AppHookConfigManager
from django.db import models
from faq.cms_appconfig import FaqConfig


class Entry(models.Model):
    app_config = AppHookConfigField(FaqConfig)
    question = models.TextField(blank=True, default='')
    answer = models.TextField()

    objects = AppHookConfigManager()

    def __unicode__(self):
        return self.question

    class Meta:
        verbose_name_plural = 'entries'

The app_config field is essentially a ForeignKey to a model we’ll define in the next step. That model will hold the specific namespace configuration and allows to assign an Entry to a namespace.

The custom AppHookConfigManager simply makes the default queryset easily filterable by the namespace like this: Entry.objects.namespace('foobar').

Next lets define the AppHookConfig model (in cms_appconfig.py):

from aldryn_apphooks_config.models import AppHookConfig
from aldryn_apphooks_config.utils import setup_config
from app_data import AppDataForm
from django.db import models
from django import forms
from django.utils.translation import ugettext_lazy as _


class FaqConfig(AppHookConfig):
    paginate_by = models.PositiveIntegerField(
        _('Paginate size'),
        blank=False,
        default=5,
    )


class FaqConfigForm(AppDataForm):
    title = forms.CharField()
setup_config(FaqConfigForm, FaqConfig)

The implementation can be completely empty as the minimal schema is defined in the parent (abstract) model.

In this case we’re defining a few extra fields though. We’re defining paginate_by as a normal model field. We’ll use it later to control how many entries should be displayed per page. For the title, we’re using a AppDataForm (see django-appdata). These forms can also be extended from other applications by just registering them. So other apps can add fields without altering the model (it’s saved in a json field). The title field could also just be a model field, like paginate_by. But we’re using the AppDataForm to demonstrate this capability.

In admin.py we need to define all fields we’d like to display:

from django.contrib import admin
from .cms_appconfig import FaqConfig
from .models import Entry
from aldryn_apphooks_config.admin import ModelAppHookConfig, BaseAppHookConfig


class EntryAdmin(ModelAppHookConfig, admin.ModelAdmin):
    list_display = (
        'question',
        'answer',
        'app_config',
    )
    list_filter = (
        'app_config',
    )
admin.site.register(Entry, EntryAdmin)


class FaqConfigAdmin(BaseAppHookConfig, admin.ModelAdmin):
    def get_config_fields(self):
        return (
            'paginate_by',
            'config.title',
        )
admin.site.register(FaqConfig, FaqConfigAdmin)

get_config_fields defines the fields that should be displayed. Any fields using the AppData forms need to be prefixed by config..

Now lets create the apphook with appconfig support (cms_apps.py):

from aldryn_apphooks_config.app_base import CMSConfigApp
from cms.apphook_pool import apphook_pool
from django.utils.translation import ugettext_lazy as _
from .cms_appconfig import FaqConfig


class FaqApp(CMSConfigApp):
    name = _("Faq App")
    urls = ["faq.urls"]
    app_name = "faq"
    app_config = FaqConfig

apphook_pool.register(FaqApp)

We have all the basics in place. Now we’ll add a list view for the FAQ entries that only displays entries for the currently used namespace (views.py):

from aldryn_apphooks_config.mixins import AppConfigMixin
from django.views import generic
from .models import Entry


class IndexView(AppConfigMixin, generic.ListView):
    model = Entry
    template_name = 'faq/index.html'

    def get_queryset(self):
        qs = super(IndexView, self).get_queryset()
        return qs.namespace(self.namespace)

    def get_paginate_by(self, queryset):
        try:
            return self.config.paginate_by
        except AttributeError:
            return 10

AppConfigMixin provides a complete support to namespaces, so the view is not required to set anything specific to support them; the following attributes are set for the view class instance:

  • current namespace in self.namespace

  • namespace configuration (the instance of NewsBlogConfig) in self.config

  • current application in the current_app parameter passed to the Response class

In this case we’re filtering to only show entries assigned to the current namespace in get_queryset. There is no magic behind qs.namespace, it could have also been written as qs.filter(app_config__namespace=self.namespace).

In get_paginate_by we use the value from our appconfig model.

And now for the rest of the missing files of the FAQ app.

And the template (faq/templates/faq/index.html):

{% extends 'base.html' %}

{% block content %}
    <h1>{{ view.config.title }}</h1>
    <p>Namespace: {{ view.namespace }}</p>
    <dl>
        {% for entry in object_list %}
            <dt>{{ entry.question }}</dt>
            <dd>{{ entry.answer }}</dd>
        {% endfor %}
    </dl>

    {% if is_paginated %}
        <div class="pagination">
            <span class="step-links">
                {% if page_obj.has_previous %}
                    <a href="?page={{ page_obj.previous_page_number }}">previous</a>
                {% else %}
                    previous
                {% endif %}

                <span class="current">
                    Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
                </span>

                {% if page_obj.has_next %}
                    <a href="?page={{ page_obj.next_page_number }}">next</a>
                {% else %}
                    next
                {% endif %}
            </span>
        </div>
    {% endif %}
{% endblock %}

urls.py:

from django.conf.urls import patterns, url
from . import views


urlpatterns = patterns('',
    url(r'^$', views.IndexView.as_view(), name='index'),
)

Finally, lets add faq to INSTALLED_APPS and create a migrations:

python manage.py makemigrations faq
python manage.py migrate faq

Now we should be all set. Create two pages with the faq apphook with different namespaces and different configurations. Also create some entries assigned to the two namespaces. Don’t forget to publish the pages with the apphook and restart the server.