Как управлять сложной конфигурацией apphook

В Как создать apphooks мы обсудили некоторые основные моменты использования apphooks. В этом документе мы рассмотрим некоторые более сложные возможности реализации.

Прикрепление приложения несколько раз

Определение пространства имен на уровне класса

Если вы хотите прикрепить приложение несколько раз к разным страницам, то класс, определяющий apphook, должен иметь атрибут app_name:

class MyApphook(CMSApp):
    name = _("My Apphook")
    app_name = "myapp"

    def get_urls(self, page=None, language=None, **kwargs):
        return ["myapp.urls"]

app_name делает три ключевые вещи:

  • Оно предоставляет пространство имен fallback для представлений и шаблонов, обращающих URL.

  • Он раскрывает поле Имя экземпляра приложения в админке страницы при применении apphook.

  • Он устанавливает имя экземпляра apphook по умолчанию (которое вы увидите в поле Имя экземпляра приложения).

Мы объясним их на примере. Предположим, что представления или шаблоны вашего приложения используют reverse('myapp:index') или {% url 'myapp:index' %}.

В этом случае пространство имен всех применяемых вами apphooks должно соответствовать myapp. Если это не так, то использующие их страницы будут выдавать ошибку NoReverseMatch.

Вы можете задать пространство имен для экземпляра apphook в поле Имя экземпляра приложения. Однако, если экземпляр с таким значением уже существует, вам нужно будет задать что-то другое. В данном случае, пока app_name = "myapp", это не имеет значения; даже если система не найдет совпадения с именем экземпляра, она вернется к тому, которое жестко заложено в классе.

Другими словами, установка app_name правильно гарантирует, что URL-реверсирование будет работать, поскольку оно устанавливает пространство имен отката соответствующим образом.

Установите пространство имен на уровне экземпляра

С другой стороны, Имя экземпляра приложения будет переопределять app_name если будет найдено совпадение.

Такая схема позволяет использовать несколько экземпляров приложений и пространств имен, если требуется такая гибкость, гарантируя при этом простой способ заставить их работать, когда это не так.

Документация Django Reversing namespaced URLs содержит больше информации о том, как это работает, но упрощенная версия такова:

  1. Сначала он попытается найти соответствие для Имя экземпляра приложения.

  2. Если это не удается, он попытается найти совпадение для app_name.

Конфигурации Apphook

Расстановка имен ваших apphooks также позволяет управлять дополнительной конфигурацией apphook, хранящейся в базе данных, на основе каждого экземпляра.

Основные понятия

Чтобы передать конфигурацию, которую могут принимать различные экземпляры apphook, необходимо создать модель Django - каждый экземпляр apphook будет экземпляром этой модели и будет управляться через администратора Django обычным способом.

После настройки конфигурация apphook может быть применена к экземпляру apphook в Дополнительных настройках страницы, к которой принадлежит экземпляр apphook:

selecting an apphook configuration application

Конфигурация затем загружается в представления приложения для этого пространства имен и будет использоваться для определения его поведения.

Создание конфигурации приложения фактически создает пространство имен экземпляра apphook. После создания пространство имен конфигурации не может быть изменено - если требуется другое пространство имен, необходимо создать новую конфигурацию.

Пример конфигурации apphook

Чтобы проиллюстрировать, как все это работает, мы создадим новое приложение FAQ, которое предоставляет простой список вопросов и ответов, вместе с классом apphook и моделью конфигурации apphook, которая позволяет ему существовать в нескольких местах на сайте в различных конфигурациях.

Мы будем считать, что у вас уже есть работающий проект django CMS.

Использование вспомогательных приложений

В этом примере мы будем использовать пару простых вспомогательных приложений, просто чтобы облегчить нашу работу.

Aldryn Apphooks Config

Aldryn Apphooks Config - это вспомогательное приложение, которое облегчает разработку конфигурируемых apphooks. Например, оно предоставляет AppHookConfig для подкласса и другие полезные компоненты для экономии вашего времени.

В этом примере мы будем использовать Aldryn Apphooks Config, так как мы рекомендуем его. Однако вы не обязаны использовать его в своих проектах; при желании вы можете собрать необходимый вам код вручную.

Для его установки используйте pip install aldryn-apphooks-config.

Aldryn Apphooks Config в свою очередь устанавливает Django AppData, который обеспечивает элегантный способ расширения одного приложения другим; мы воспользуемся и этим.

Создайте новое приложение FAQ

python manage.py startapp faq

Создайте модель FAQ Entry

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'

Поле app_config является ForeignKey для модели конфигурации apphook; мы создадим ее в ближайшее время. Эта модель будет хранить конкретную конфигурацию пространства имен и позволит назначить каждый FAQ Entry пространству имен.

Пользовательский AppHookConfigManager существует для того, чтобы облегчить фильтрацию набора запросов Entries с помощью удобного ярлыка: Entry.objects.namespace('foobar').

Определите подкласс AppHookConfig

В новом файле cms_appconfig.py в приложении FAQ:

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 gettext_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)

Реализацию можно оставить совершенно пустой, поскольку минимальная схема уже определена в абстрактной родительской модели, предоставляемой Aldryn Apphooks Config.

Здесь мы определяем дополнительное поле в модели, paginate_by. Мы будем использовать его позже, чтобы контролировать количество записей, отображаемых на странице.

Мы также установили FaqConfigForm, который использует AppDataForm для добавления поля в FaqConfig, не касаясь его модели.

Поле title также может быть просто полем модели, например paginate_by. Но мы используем AppDataForm для демонстрации этой возможности.

Определите его свойства администратора

В admin.py нам нужно определить все поля, которые мы хотели бы отобразить:

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 определяет поля, которые должны быть отображены. Любые поля, использующие формы AppData, должны иметь префикс config..

Определите сам apphook

Теперь давайте создадим apphook и настроим его на поддержку нескольких экземпляров. В cms_apps.py:

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


@apphook_pool.register
class FaqApp(CMSConfigApp):
    name = _("Faq App")
    app_name = "faq"
    app_config = FaqConfig

    def get_urls(self, page=None, language=None, **kwargs):
        return ["faq.urls"]

Определите вид списка для записей FAQ

У нас есть все основы. Теперь мы добавим представление списка для записей FAQ, которое будет отображать записи только для текущего используемого пространства имен. В 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().get_queryset()
        return qs.namespace(self.namespace)

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

AppConfigMixin избавляет вас от необходимости устанавливать какие-либо атрибуты в представлении - они автоматически устанавливаются для экземпляра класса представления:

  • текущее пространство имен в self.namespace

  • конфигурация пространства имен (экземпляр FaqConfig) в self.config

  • текущее приложение в current_app parameter, переданное классу Response

В данном случае мы фильтруем, чтобы показать только записи, назначенные текущему пространству имен в get_queryset. qs.namespace, благодаря менеджеру моделей, который мы определили ранее, является эквивалентом qs.filter(app_config__namespace=self.namespace).

В get_paginate_by мы используем значение из нашей модели appconfig.

Определите шаблон

В 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 %}
URLconf

urls.py:

from django.urls import re_path
from . import views


urlpatterns = [
    re_path(r'^$', views.IndexView.as_view(), name='index'),
]

Соедините все вместе

Наконец, мы добавляем faq к INSTALLED_APPS, затем создаем и запускаем миграции:

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

Теперь все должно быть готово.

Создайте две страницы с помощью apphook faq (не забудьте опубликовать их), с разными пространствами имен и разными конфигурациями. Также создайте несколько записей, назначенных двум пространствам имен.

Вы можете экспериментировать с различными конфигурациями поведения (в данном случае доступна только пагинация), а также с тем, как различные экземпляры Entry могут быть связаны с определенным apphook.

Вернуться на верх