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:
ArtworkCategoryArtworkItem
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:
CMSConfigAppenables per-instance configurationapp_configlinks 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:
TranslatableModelenables multilingual fieldsAppHookConfigFieldlinks data to a specific app instancePlaceholderFieldallows 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
- Create two CMS pages
- Attach the Artwork Gallery apphook to both
- Assign different namespaces
- Configure each instance differently
- Create categories linked to each namespace
Each CMS page now serves independent gallery content while sharing
the same application code.
Member discussion: