Overview

This guide demonstrates how to build a Django CMS Apphook that
supports:

  • Multiple CMS page instances
  • Per-instance configuration
  • Internationalized models using django-parler
  • Integration with the Django CMS admin

Example application: Artwork Gallery.

Models:

  • ArtworkCategory
  • ArtworkItem

Both models support translations.


Environment

Tested with:

Package Version


Python 2.7.16
Django 1.9.13
django-cms 3.4.4
aldryn-apphooks-config 0.3.3
django-parler 1.6.5

Assumes Django CMS is already installed and configured.


1. Create the CMS Apphook

Define the CMS application and register it with the CMS apphook pool.

# cms_apps.py

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


class ArtworkGalleryApphook(CMSConfigApp):
    app_name = "artwork_gallery"
    name = _("Artwork Gallery")
    urls = ["artwork_gallery.urls"]
    app_config = ArtworkConfig


apphook_pool.register(ArtworkGalleryApphook)

Key points:

  • CMSConfigApp enables per-instance configuration
  • app_config links the apphook to the configuration model

2. Define Models

Models use django-parler to support translations.

# models.py

from parler.models import TranslatableModel, TranslatedFields
from django.db import models
from django.utils.translation import ugettext_lazy as _
from cms.models.fields import PlaceholderField
from django.core.urlresolvers import reverse
from aldryn_apphooks_config.fields import AppHookConfigField

from artwork_gallery.cms_appconfig import ArtworkConfig
from .managers import ArtworkCategoryManager


class ArtworkItem(TranslatableModel):

    class Meta:
        verbose_name = _("Artwork Item")
        verbose_name_plural = _("Artwork Items")
        ordering = ("date_added",)

    translations = TranslatedFields(
        title=models.CharField(max_length=255, verbose_name=_("Title"))
    )

    content_placeholder = PlaceholderField(
        "placeholder_artwork_content",
        related_name="placeholder_artwork_content"
    )

    category = models.ForeignKey(
        "ArtworkCategory",
        verbose_name=_("Category"),
        blank=True,
        null=True
    )

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse(
            "artwork_gallery:artworks-detail",
            kwargs={"pk": self.id}
        )


class ArtworkCategory(TranslatableModel):

    class Meta:
        verbose_name = _("Artwork Category")
        verbose_name_plural = _("Artwork Categories")

    translations = TranslatedFields(
        title=models.CharField(max_length=255, verbose_name=_("Title"))
    )

    app_config = AppHookConfigField(
        ArtworkConfig,
        null=True,
        verbose_name=_("app config")
    )

    objects = ArtworkCategoryManager()

    def __str__(self):
        return self.title

Key concepts:

  • TranslatableModel enables multilingual fields
  • AppHookConfigField links data to a specific app instance
  • PlaceholderField allows CMS plugin content inside models

3. Custom Manager

Managers ensure queries respect the active apphook namespace.

# managers.py

from aldryn_apphooks_config.managers.parler import (
    AppHookConfigTranslatableManager
)


class ArtworkCategoryManager(AppHookConfigTranslatableManager):
    pass

This manager filters objects by the current app configuration
namespace
.


4. Application Configuration Model

Each CMS page using the apphook can have independent configuration.

# cms_appconfig.py

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


class ArtworkConfig(TranslatableModel, AppHookConfig):

    translations = TranslatedFields(
        app_title=models.CharField(
            _("application title"),
            max_length=234
        )
    )

    class Meta:
        verbose_name = _("artwork config")
        verbose_name_plural = _("artwork configs")

    def get_app_title(self):
        return getattr(self, "app_title", _("untitled"))


class ArtworkConfigForm(AppDataForm):

    foo = forms.BooleanField(
        label=_("check for foo"),
        required=False,
        initial=False
    )


setup_config(ArtworkConfigForm, ArtworkConfig)

Responsibilities:

  • Store per-app instance settings
  • Support translations for configuration values

5. URL Configuration

Expose application views through the apphook.

# urls.py

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


urlpatterns = patterns(
    "",
    url(r"^artworks/$", views.ArtworksList.as_view(), name="artworks-list"),
    url(r"^(?P<pk>[\d]+)/$", views.ArtworkDetail.as_view(), name="artworks-detail"),
    url(r"^$", views.CategoryList.as_view(), name="category-list"),
)

6. Apply Migrations

Generate and apply migrations.

python manage.py makemigrations
python manage.py migrate

7. Django Admin Integration

Enable translated admin views and CMS placeholder editing.

# admin.py

from django.contrib import admin
from parler.admin import TranslatableAdmin
from aldryn_translation_tools.admin import AllTranslationsMixin
from cms.admin.placeholderadmin import (
    FrontendEditableAdminMixin,
    PlaceholderAdminMixin
)

from aldryn_apphooks_config.admin import (
    ModelAppHookConfig,
    BaseAppHookConfig
)

from .models import ArtworkItem, ArtworkCategory
from artwork_gallery.cms_appconfig import ArtworkConfig


class ArtworkItemAdmin(
    AllTranslationsMixin,
    PlaceholderAdminMixin,
    TranslatableAdmin
):

    list_display = ("title",)

    fieldsets = (
        (None, {"fields": ("title",)}),
    )


admin.site.register(ArtworkItem, ArtworkItemAdmin)


class ArtworkCategoryAdmin(
    PlaceholderAdminMixin,
    FrontendEditableAdminMixin,
    ModelAppHookConfig,
    TranslatableAdmin
):

    list_display = ("title", "app_config")
    list_filter = ("app_config",)

    fieldsets = (
        (None, {
            "fields": (
                "title",
                "app_config"
            )
        }),
    )


admin.site.register(ArtworkCategory, ArtworkCategoryAdmin)


class ArtworkConfigAdmin(BaseAppHookConfig, TranslatableAdmin):

    @property
    def declared_fieldsets(self):
        return self.get_fieldsets(None)

    def get_fieldsets(self, request, obj=None):
        return [
            (None, {
                "fields": (
                    "type",
                    "namespace",
                    "app_title",
                    "config.foo"
                )
            }),
        ]

    def save_model(self, request, obj, form, change):

        if "config.menu_structure" in form.changed_data:
            from menus.menu_pool import menu_pool
            menu_pool.clear(all=True)

        return super(ArtworkConfigAdmin, self).save_model(
            request, obj, form, change
        )


admin.site.register(ArtworkConfig, ArtworkConfigAdmin)

Admin features:

  • Translated editing
  • CMS placeholders
  • Per-app configuration management

8. Namespace-Aware List View

Views must respect the current apphook namespace.

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

from .models import ArtworkCategory


class CategoryList(AppConfigMixin, generic.ListView):

    model = ArtworkCategory
    template_name = "artwork_gallery/category_list.html"

    http_method_names = ["get"]
    paginate_by = 12

    context_object_name = "categories"

    def get_queryset(self):

        qs = super(CategoryList, self).get_queryset()

        return qs.namespace(self.namespace)

AppConfigMixin provides:

  • access to the active namespace
  • automatic filtering by app_config

Testing the Setup

  1. Create two CMS pages
  2. Attach the Artwork Gallery apphook to both
  3. Assign different namespaces
  4. Configure each instance differently
  5. Create categories linked to each namespace

Each CMS page now serves independent gallery content while sharing
the same application code.