Как расширить модели Page и Title

Вы можете расширить модели cms.models.Page и cms.models.Title своими собственными полями (например, добавить иконку для каждой страницы), используя модели расширения: cms.extensions.PageExtension и cms.extensions.TitleExtension, соответственно.

Заголовок в сравнении с расширением страницы

Разница между расширением страницы и расширением заголовка связана с разницей между моделями cms.models.Page и cms.models.Title.

  • PageExtension: используется для добавления полей, которые должны иметь одинаковые значения для разных языковых версий страницы - например, иконка.

  • TitleExtension: используется для добавления полей, которые должны иметь языковые значения для различных языковых версий страницы - например, ключевые слова.

Реализовать базовое расширение

Необходимо выполнить три основных действия:

  • добавить расширение модель

  • добавить расширение admin

  • добавить пункт меню панели инструментов для расширения

Пример расширения модели страницы

Модель

Чтобы добавить поле в модель Page, создайте класс, который наследуется от cms.extensions.PageExtension. Ваш класс должен находиться в одном из models.py (или модуле) вашего приложения.

Примечание

Поскольку PageExtensionTitleExtension) наследуются от django.db.models.Model, вы можете добавлять любое поле, но убедитесь, что вы не используете уникальное ограничение для любого из ваших добавленных полей, поскольку уникальность препятствует корректной работе механизма копирования расширения. Это означает, что вы не можете использовать отношения один-к-одному в модели расширения.

Наконец, вам нужно зарегистрировать модель с помощью extension_pool.

Вот простой пример, который добавляет поле icon на страницу:

from django.db import models
from cms.extensions import PageExtension
from cms.extensions.extension_pool import extension_pool


class IconExtension(PageExtension):
    image = models.ImageField(upload_to='icons')


extension_pool.register(IconExtension)

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

Администратор

Чтобы сделать ваше расширение редактируемым, вы должны сначала создать класс admin, который является подклассом cms.extensions.PageExtensionAdmin. Этот администратор управляет правами доступа к страницам.

Примечание

Если вы хотите использовать собственный класс администратора, убедитесь, что вы исключили живые версии расширений, используя filter(extended_object__publisher_is_draft=True) в наборе запросов.

Продолжая пример модели выше, вот простой соответствующий PageExtensionAdmin класс:

from django.contrib import admin
from cms.extensions import PageExtensionAdmin

from .models import IconExtension


class IconExtensionAdmin(PageExtensionAdmin):
    pass

admin.site.register(IconExtension, IconExtensionAdmin)

Поскольку PageExtensionAdmin наследуется от ModelAdmin, вы сможете использовать обычный набор свойств Django ModelAdmin, соответствующий вашим потребностям.

Примечание

Обратите внимание, что поле, в котором указывается связь между расширением и страницей CMS, является нередактируемым, поэтому оно не отображается непосредственно в представлениях администратора страницы. Это может быть решено в будущем обновлении, а пока панель инструментов обеспечивает доступ к нему.

Элемент панели инструментов

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

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

Вот простая версия для нашего примера. Этот пример добавляет узел к существующему меню Страница, называемый Иконка страницы. При его выборе открывается модальный диалог, в котором можно отредактировать поле Иконка страницы.

from cms.toolbar_pool import toolbar_pool
from cms.extensions.toolbar import ExtensionToolbar
from django.utils.translation import gettext_lazy as _
from .models import IconExtension


@toolbar_pool.register
class IconExtensionToolbar(ExtensionToolbar):
    # defines the model for the current toolbar
    model = IconExtension

    def populate(self):
        # setup the extension toolbar with permissions and sanity checks
        current_page_menu = self._setup_extension_toolbar()

        # if it's all ok
        if current_page_menu:
            # retrieves the instance of the current extension (if any) and the toolbar item URL
            page_extension, url = self.get_page_extension_admin()
            if url:
                # adds a toolbar item in position 0 (at the top of the menu)
                current_page_menu.add_modal_item(_('Page Icon'), url=url,
                    disabled=not self.toolbar.edit_mode_active, position=0)

Пример расширения модели титула

В этом примере мы создадим поле расширения Rating, которое может быть применено к каждому Title, другими словами, к каждой языковой версии каждого Page.

Примечание

Пожалуйста, обратитесь к более подробному обсуждению примера расширения модели Page выше и, в частности, к специальным примечаниям.

Модель

from django.db import models
from cms.extensions import TitleExtension
from cms.extensions.extension_pool import extension_pool


class RatingExtension(TitleExtension):
    rating = models.IntegerField()


extension_pool.register(RatingExtension)

Администратор

from django.contrib import admin
from cms.extensions import TitleExtensionAdmin
from .models import RatingExtension


class RatingExtensionAdmin(TitleExtensionAdmin):
    pass


admin.site.register(RatingExtension, RatingExtensionAdmin)

Элемент панели инструментов

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

from cms.toolbar_pool import toolbar_pool
from cms.extensions.toolbar import ExtensionToolbar
from django.utils.translation import gettext_lazy as _
from .models import RatingExtension
from cms.utils import get_language_list  # needed to get the page's languages
@toolbar_pool.register
class RatingExtensionToolbar(ExtensionToolbar):
    # defines the model for the current toolbar
    model = RatingExtension

    def populate(self):
        # setup the extension toolbar with permissions and sanity checks
        current_page_menu = self._setup_extension_toolbar()

        # if it's all ok
        if current_page_menu and self.toolbar.edit_mode_active:
            # create a sub menu labelled "Ratings" at position 1 in the menu
            sub_menu = self._get_sub_menu(
                current_page_menu, 'submenu_label', 'Ratings', position=1
                )

            # retrieves the instances of the current title extension (if any)
            # and the toolbar item URL
            urls = self.get_title_extension_admin()

            # we now also need to get the titleset (i.e. different language titles)
            # for this page
            page = self._get_page()
            titleset = page.title_set.filter(language__in=get_language_list(page.node.site_id))

            # create a 3-tuple of (title_extension, url, title)
            nodes = [(title_extension, url, title.title) for (
                (title_extension, url), title) in zip(urls, titleset)
                ]

            # cycle through the list of nodes
            for title_extension, url, title in nodes:

                # adds toolbar items
                sub_menu.add_modal_item(
                    'Rate %s' % title, url=url, disabled=not self.toolbar.edit_mode_active
                    )

Использование расширений

В шаблонах

Для доступа к расширению страницы в шаблонах страниц вы можете просто обратиться к соответствующему полю related_name, которое теперь доступно на объекте Page.

Расширения страниц

В соответствии с обычным механизмом именования related_name, соответствующее поле для доступа будет таким же, как и имя вашей модели PageExtension, но в нижнем регистре. Если предположить, что класс вашей модели Page Extension - IconExtension, то связь с моделью расширения страницы будет доступна в page.iconextension. Отсюда вы можете получить доступ к дополнительным полям, которые вы определили в своем расширении, поэтому вы можете использовать что-то вроде:

{% load static %}

{# rest of template omitted ... #}

{% if request.current_page.iconextension %}
    <img src="{% static request.current_page.iconextension.image.url %}">
{% endif %}

где request.current_page - это обычный способ доступа к текущей странице, которая рендерит шаблон.

Важно помнить, что если оператор не назначил расширение страницы для каждой страницы, страница может не иметь отношения iconextension, поэтому выше используется {% if ... %}...{% endif %}.

Расширения названия

Чтобы получить расширение заголовка в шаблоне, получите объект Title с помощью request.current_page.get_title_obj. Используя приведенный выше пример, мы могли бы использовать:

{{ request.current_page.get_title_obj.ratingextension.rating }}

С меню

Как и большинство других атрибутов страницы, расширения не представлены в меню NavigationNodes, и поэтому шаблоны меню не будут иметь к ним доступа по умолчанию.

Чтобы сделать расширение доступным, вам нужно создать menu modifier (см. приведенный пример), который будет это делать.

Каждый экземпляр расширения страницы имеет связь один-к-одному со своей страницей. Получите расширение, используя обратное отношение, по типу extension = page.yourextensionlowercased, и поместите этот атрибут page на узел - как (например) node.extension.

Таким образом, в шаблоне меню расширение иконки, которое мы создали выше, будет доступно как child.extension.icon.

Работа с отношениями

Если ваш PageExtension или TitleExtension включает ForeignKey из другой модели или включает ManyToManyField, вы также должны переопределить метод copy_relations(self, oldinstance, language), чтобы эти поля копировались соответствующим образом, когда CMS делает копию вашего расширения для поддержки версионности и т.д.

Вот пример, в котором используется ManyToManyField:

from django.db import models
from cms.extensions import PageExtension
from cms.extensions.extension_pool import extension_pool


class MyPageExtension(PageExtension):

    page_categories = models.ManyToManyField(Category, blank=True)

    def copy_relations(self, oldinstance, language):
        for page_category in oldinstance.page_categories.all():
            page_category.pk = None
            page_category.mypageextension = self
            page_category.save()

extension_pool.register(MyPageExtension)

Полный API панели инструментов

В приведенном выше примере используется Упрощенный API панели инструментов.

Если вам необходим полный контроль над расположением элементов панели инструментов расширения, вы можете использовать низкоуровневый API для редактирования панели инструментов в соответствии с вашими потребностями:

from cms.api import get_page_draft
from cms.toolbar_pool import toolbar_pool
from cms.toolbar_base import CMSToolbar
from cms.utils import get_cms_setting
from cms.utils.page_permissions import user_can_change_page
from django.urls import reverse, NoReverseMatch
from django.utils.translation import gettext_lazy as _
from .models import IconExtension


@toolbar_pool.register
class IconExtensionToolbar(CMSToolbar):
    def populate(self):
        # always use draft if we have a page
        self.page = get_page_draft(self.request.current_page)

        if not self.page:
            # Nothing to do
            return

        if user_can_change_page(user=self.request.user, page=self.page):
            try:
                icon_extension = IconExtension.objects.get(extended_object_id=self.page.id)
            except IconExtension.DoesNotExist:
                icon_extension = None
            try:
                if icon_extension:
                    url = reverse('admin:myapp_iconextension_change', args=(icon_extension.pk,))
                else:
                    url = reverse('admin:myapp_iconextension_add') + '?extended_object=%s' % self.page.pk
            except NoReverseMatch:
                # not in urls
                pass
            else:
                not_edit_mode = not self.toolbar.edit_mode_active
                current_page_menu = self.toolbar.get_or_create_menu('page')
                current_page_menu.add_modal_item(_('Page Icon'), url=url, disabled=not_edit_mode)

Теперь, когда оператор вызывает «Редактировать эту страницу…» из панели инструментов, появится дополнительный пункт меню Page Icon ... (в данном случае), который можно использовать для открытия модального диалога, где оператор может воздействовать на новое поле icon.

Обратите внимание, что при сохранении расширения соответствующая страница помечается как имеющая неопубликованные изменения. Чтобы увидеть новые значения расширения, опубликуйте страницу.

Упрощенный API панели инструментов

Упрощенный API панели инструментов работает путем создания класса панели инструментов на основе ExtensionToolbar, который предоставляет следующий API:

  • ExtensionToolbar.get_page_extension_admin(): для расширений страниц, извлекает правильный URL администратора для соответствующего элемента панели инструментов; возвращает экземпляр расширения (или None, если его не существует) и URL администратора для элемента панели инструментов

  • ExtensionToolbar.get_title_extension_admin(): для расширений заголовков, извлекает правильный URL администратора для соответствующего элемента панели инструментов; возвращает список экземпляров расширений (или None, если таковых не существует) и URL администратора для каждого заголовка текущей страницы

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