How to manage complex apphook configuration
If you want to attach an application multiple times to different pages, then the class defining the apphook must have an app_name
attribute:
The app_name
does three key things:
- It provides the fallback namespace for views and templates that reverse URLs.
- It exposes the Application instance name field in the page admin when applying an apphook.
- It sets the default apphook instance name (which you’ll see in the Application instance name field).
We’ll explain these with an example. Let’s suppose that your application’s views or templates use reverse('myapp:index')
or {% url 'myapp:index' %}
.
In this case the namespace of any apphooks you apply must match myapp
. If they don’t, your pages using them will throw up a NoReverseMatch
error.
You can set the namespace for the instance of the apphook in the Application instance name field. However, you’ll need to set that to something different if an instance with that value already exists. In this case, as long as app_name = "myapp"
it doesn’t matter; even if the system doesn’t find a match with the name of the instance it will fall back to the one hard-wired into the class.
In other words setting app_name
correctly guarantees that URL-reversing will work, because it sets the fallback namespace appropriately.
Set a namespace at instance-level
On the other hand, the Application instance name will override the app_name
if a match is found.
This arrangement allows you to use multiple application instances and namespaces if that flexibility is required, while guaranteeing a simple way to make it work when it’s not.
Django’s documentation provides more information on how this works, but the simplified version is:
- First, it’ll try to find a match for the Application instance name.
- If it fails, it will try to find a match for the
app_name
.
Namespacing your apphooks also makes it possible to manage additional database-stored apphook configuration, on an instance-by-instance basis.
To capture the configuration that different instances of an apphook can take, a Django model needs to be created - each apphook instance will be an instance of that model, and administered through the Django admin in the usual way.
Once set up, an apphook configuration can be applied to to an apphook instance, in the Advanced settings of the page the apphook instance belongs to:
Creating an application configuration in fact creates an apphook instance namespace. Once created, the namespace of a configuration cannot be changed - if a different namespace is required, a new configuration will also need to be created.
In order to illustrate how this all works, we’ll create a new FAQ application, that provides a simple list of questions and answers, together with an apphook class and an apphook configuration model that allows it to exist in multiple places on the site in multiple configurations.
We’ll assume that you have a working django CMS project running already.
Using helper applications
We’ll use a couple of simple helper applications for this example, just to make our work easier.
Aldryn Apphooks Config
is a helper application that makes it easier to develop configurable apphooks. For example, it provides an AppHookConfig
for you to subclass, and other useful components to save you time.
In this example, we’ll use Aldryn Apphooks Config, as we recommend it. However, you don’t have to use it in your own projects; if you prefer to can build the code you require by hand.
Use pip install aldryn-apphooks-config
to install it.
Aldryn Apphooks Config in turn installs Django AppData, which provides an elegant way for an application to extend another; we’ll make use of this too.
python manage.py startapp faq
Create the FAQ Entry
model
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:
The app_config
field is a ForeignKey
to an apphook configuration model; we’ll create it in a moment. This model will hold the specific namespace configuration, and makes it possible to assign each FAQ Entry to a namespace.
The custom AppHookConfigManager
is there to make it easy to filter the queryset of Entries
using a convenient shortcut: Entry.objects.namespace('foobar')
.
Define the AppHookConfig subclass
In a new file cms_appconfig.py
in the FAQ application:
The implementation can be left completely empty, as the minimal schema is already defined in the abstract parent model provided by Aldryn Apphooks Config.
We also set up a FaqConfigForm
, which uses AppDataForm
to add a field to FaqConfig
without actually touching its model.
The title field could also just be a model field, like paginate_by
. But we’re using the AppDataForm to demonstrate this capability.
Define its admin properties
In admin.py
we need to define all fields we’d like to display:
from django.contrib import admin
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.
.
Define the apphook itself
Now let’s create the apphook, and set it up with support for multiple instances. In 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
@apphook_pool.register
class FaqApp(CMSConfigApp):
name = _("Faq App")
urls = ["faq.urls"]
app_name = "faq"
app_config = FaqConfig
Define a list view for FAQ entries
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. In views.py
:
AppConfigMixin
saves you the work of setting any attributes in your view - it automatically sets, for the view class instance:
- current namespace in
self.namespace
- namespace configuration (the instance of FaqConfig) in
self.config
- current application in the
current_app parameter
passed to theResponse
class
In this case we’re filtering to only show entries assigned to the current namespace in get_queryset
. qs.namespace
, thanks to the model manager we defined earlier, is the equivalent of .
In get_paginate_by
we use the value from our appconfig model.
Define a template
In faq/templates/faq/index.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 %}
URLconf
urls.py
:
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
]
Put it all together
Finally, we add faq
to INSTALLED_APPS
, then create and run migrations:
Now we should be all set.
You can experiment with the different configured behaviours (in this case, only pagination is available), and the way that different Entry
instances can be associated with a specific apphook.