Евгений Легоцкой9 мая 2020 г. 6:09

Django - Урок 054. Как создать полиморфную систему динамических виджетов

Содержание

На днях решал задачу добавления на сайте динамических виджетов с возможностью добавления разных видов виджетов, а также с возможностью дальнейшего неограниченного расширения набора виджетов за счёт добавления моделей специфических реализаций.

Система такова, что существует один класс Widget , который в зависимости от выбранного при создании типа обращается к модели специфической реализации, например, HTMLWidgetImpl , которая содержит какие-то специальные параметры, а также знает о своём шаблоне для рендеринга. Widget в шаблоне сайта при вызове метода render() обращается через тип к специфической реализациий и уже её методу render() .

Подобный подход позволяет написать крайне простой код для шаблона, а также организовать код так, что добавление новых виджетов будет заключается к в добавлении новой модели данных, а также исправлению ряда мест в коде, вернее их обновления. Которое по сути станет рутинной задачей и снимет с программиста необходимость придумывать что-то новое.

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

Постановка задачи

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

Поэтому создаём новое приложение

python manage.py startapp evileg_widgets

И далее создаём все необходимые нам модели, а понадобятся нам следующие модели

  • SideBar - модель боковой панели, к слову я будут проектировать её как Singleton модель
  • Widget - модель полиморфного виджета
  • HTMLWidgetImpl - специфическая реализация виджета для вставки простого html кода
  • StandardWidgetImpl - специфическая реализация виджета для вставки отрендериного кода
  • SideBarToWidget - модель для ManyToMany поля между SideBar моделью и моделью Widget

models

Начнём с моделей, которые мы создаём.

SideBar

Для SideBar модели я использую SingletonModel из третьестороннего приложения django-solo, которое предназначено для формирования настроечных объектов, которые должны существовать в одном единственном экземпляре.
Модель SideBar содержит поле widgets, которое будет отвечать за комплект подключённых у сайта виджетов, но ManyToMany отношение будет формироваться через промежуточную модель SideBarToWidget, которая позволит добавить порядок виджетов в SideBar.

# -*- coding: utf-8 -*-

from django.db import models
from django.template.loader import render_to_string
from django.utils.translation import ugettext, ugettext_lazy as _
from solo.models import SingletonModel


class SideBar(SingletonModel):
    class Meta:
        verbose_name = _('Sidebar')
        verbose_name_plural = _('Sidebars')

    widgets = models.ManyToManyField('Widget', through='SideBarToWidget')

    def __str__(self):
        return ugettext('Sidebar')

SideBarToWidget

По сути данную модель мы задаем явно ради одного поля number , которое позволит задать порядок виджетов.
Если не задавать эту модель явно, то будет сформирована таблица которая содержит внешние ключи на объекты ManyToMany отношений.

class SideBarToWidget(models.Model):
    sidebar = models.ForeignKey(SideBar, verbose_name=_('Sidebar'), on_delete=models.CASCADE)
    widget = models.ForeignKey(Widget, verbose_name=_('Widget'), on_delete=models.CASCADE)

    number = models.PositiveIntegerField(verbose_name=_('Ordering'))

    def __str__(self):
        return self.widget.__str__()

    class Meta:
        ordering = ('number',)

Widget

А вот теперь самая интересная модель с полиморфным характером. Данная модель отличается тем, что определяет своё поведение выбранным при создании типом.
Тип виджета определяется полем widget_type

widget_type = models.IntegerField(
    verbose_name=_('Widget type'),
    choices=WIDGET_TYPE_CHOICES,
    default=HTML
)

Выборы типа поля конфигурируются с помощью следующих классовых переменных

HTML = 1
STANDARD = 2
WIDGET_TYPE_CHOICES = (
    (HTML, _('HTML Widget')),
    (STANDARD, _('Standard Widget')),
)

WIDGET_TYPE_TO_IMPL = {
    HTML: 'htmlwidgetimpl',
    STANDARD: 'standardwidgetimpl'
}

Благодаря этому типу я чётко определяю, к какой таблице мне следует обратиться, чтобы получить специфическую реализацию.
При этом я следую определённым правилам формирования имени отношения.
Это правильно прослеживается уже в этой переменной

WIDGET_TYPE_TO_IMPL = {
    HTML: 'htmlwidgetimpl',
    STANDARD: 'standardwidgetimpl'
}

В данном словаре тип виджета соответствует имени свойства, которое отвечает за доступ к специфической реализации

  • htmlwidgetimpl -> HTMLWidgetImpl
  • standardwidgetimpl -> StandardWidgetImpl

Описание виджета, то есть метод __str__ формируется так

def __str__(self):
    return self.description or str(dict(self.WIDGET_TYPE_CHOICES)[self.widget_type])

Как видите я использую либо заданное описание для виджета, которое добавлено для удобства, либо описание типа виджета, которое забираю из классовой переменной WIDGET_TYPE_CHOICES через текущее значение widget_type

Но самым интересным будет метод render() , который отвечает за рендеринг виджета

def render(self):
    impl = getattr(self, self.WIDGET_TYPE_TO_IMPL[self.widget_type])
    return impl.render() if impl else ''

В методе рендеринга уже используется мета-программирование, поскольку с помощью текущего типа виджета я обращаюсь к специфической реализации по имени свойства.
Для этого используется встроенная в python функция getattr . И если специфическая реализация существует, мало ли, что-то пошло не так, то вызываю метод render() специфической реализации.

Полный код

class Widget(models.Model):
    HTML = 1
    STANDARD = 2
    WIDGET_TYPE_CHOICES = (
        (HTML, _('HTML Widget')),
        (STANDARD, _('Standard Widget')),
    )

    WIDGET_TYPE_TO_IMPL = {
        HTML: 'htmlwidgetimpl',
        STANDARD: 'standardwidgetimpl'
    }

    class Meta:
        verbose_name = _('Widget')
        verbose_name_plural = _('Widgets')

    widget_type = models.IntegerField(
        verbose_name=_('Widget type'),
        choices=WIDGET_TYPE_CHOICES,
        default=HTML
    )

    description = models.CharField(verbose_name=_('Description'), max_length=255, null=True, blank=True)

    def __str__(self):
        return self.description or str(dict(self.WIDGET_TYPE_CHOICES)[self.widget_type])

    def render(self):
        impl = getattr(self, self.WIDGET_TYPE_TO_IMPL[self.widget_type])
        return impl.render() if impl else ''

AbstractWidgetImpl

Для обобщения некоторых частей кода я использую абстрактную модель для специфических реализаций.

class AbstractWidgetImpl(models.Model):
    class Meta:
        abstract = True

    parent = models.OneToOneField(Widget, verbose_name=_('Widget'), on_delete=models.CASCADE, related_name='%(class)s',
                                  related_query_name='%(class)s')

С помощью этой модели я формирую OneToOne отношение специфической реализации к виджету.
Самое интересно здесь заключается в двух следующих вещах

  • related_name='%(class)s'
  • related_query_name='%(class)s'

На самом деле я мало в каких статьях встречал подобные записи, но суть заключается в том, что когда я пишу модель, которая не является абстрактной, то запись %(class)s преобразует имя модели по следующему принципу

  • HTMLWidgetImpl -> htmlwidgetimpl

Таким образом я определяю правила формирования имён для свойств специфических реализаций.

HTMLWidgetImpl

И вот мы наконец добрались до специфических реализаций. Например для HTMLWidgetImpl существует поле content , а также HTMLWidgetImpl знаёт о своём шаблоне для рендеринга evileg_widgets/htmlwidgetimpl.html .

Поэтому мы здесь имеем свою реализацию метода render()

class HTMLWidgetImpl(AbstractWidgetImpl):
    class Meta:
        verbose_name = _('HTML Widget')
        verbose_name_plural = _('HTML Widgets')

    content = models.TextField(verbose_name=_('HTML'))

    def render(self):
        return render_to_string(
            template_name='evileg_widgets/htmlwidgetimpl.html',
            context={'content': self.content}
        )

StandardWidgetImpl

Аналогичным образом у нас реализована специфическая реализация для StandardWidgetImpl

class StandardWidgetImpl(AbstractWidgetImpl):
    class Meta:
        verbose_name = _('Standard Widget')
        verbose_name_plural = _('Standard Widgets')

    title = models.CharField(verbose_name=_('Title'), max_length=255)
    content = models.TextField(verbose_name=_('HTML'))

    def render(self):
        return render_to_string(
            template_name='evileg_widgets/standardwidgetimpl.html',
            context={
                'title': self.title,
                'content': self.content
            }
        )

admin.py

А теперь посмотрим на панель администрирования и то, как она настраивается. Здесь код будет уже немного проще, поэтому приведу его сразу, а потом буду описывать, что он делает.

# -*- coding: utf-8 -*-

from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
from solo.admin import SingletonModelAdmin

from evileg_widgets.models import (
    SideBar,
    Widget,
    SideBarToWidget,
    HTMLWidgetImpl,
    StandardWidgetImpl
)


class AbstractWidgetImplInline(admin.StackedInline):
    verbose_name_plural = _('Configuration')
    can_delete = False


class HTMLWidgetImplInline(AbstractWidgetImplInline):
    model = HTMLWidgetImpl


class StandardWidgetImplInline(AbstractWidgetImplInline):
    model = StandardWidgetImpl


WIDGET_TYPE_TO_INLINE = {
    Widget.HTML: HTMLWidgetImplInline,
    Widget.STANDARD: StandardWidgetImplInline
}


class WidgetAdmin(admin.ModelAdmin):
    fields = ['widget_type', 'description', 'preview']
    readonly_fields = ['preview']

    def preview(self, obj):
        return obj.render()

    preview.short_description = _('Preview')

    def get_fields(self, request, obj=None):
        if obj is not None:
            return super().get_fields(request, obj)
        return ['widget_type']

    def get_readonly_fields(self, request, obj=None):
        readonly_fields = super().get_readonly_fields(request, obj)
        if obj is not None:
            readonly_fields.append('widget_type')
        return readonly_fields

    def get_inline_instances(self, request, obj=None):
        if obj is not None:
            return [WIDGET_TYPE_TO_INLINE[obj.widget_type](self.model, self.admin_site)]
        return []


class SideBarToWidgetInline(admin.TabularInline):
    model = SideBarToWidget
    fields = ['number', 'widget']
    verbose_name_plural = _('Widgets')
    extra = 1


class SideBarAdmin(SingletonModelAdmin):
    inlines = [SideBarToWidgetInline]


admin.site.register(SideBar, SideBarAdmin)
admin.site.register(Widget, WidgetAdmin)

Inline

Во-первых я формирую Inline формсеты для каждой специфической реализации, а также привязываю их к типам виджетов

class AbstractWidgetImplInline(admin.StackedInline):
    verbose_name_plural = _('Configuration')
    can_delete = False


class HTMLWidgetImplInline(AbstractWidgetImplInline):
    model = HTMLWidgetImpl


class StandardWidgetImplInline(AbstractWidgetImplInline):
    model = StandardWidgetImpl


WIDGET_TYPE_TO_INLINE = {
    Widget.HTML: HTMLWidgetImplInline,
    Widget.STANDARD: StandardWidgetImplInline
}

WidgetAdmin

После чего формирую администрирование формы виджета, в которой определяю два этапа отображения формы виджеты.

При создании виджета я позволяю установить только его тип

А потом в зависимости от типа уже позволяю редактировать конкретные свойства. К слову ещё и отображая предпросмотр этого виджета. Для предпросмотра вам понадобиться добавить ваши css, но это к статье не относится. А также на скриншоте отображается и мультиязычность, но это тоже не отоносится к статье, скажу только, что для мультиязычности следует использовать django-modeltranslation

Выбор и настройка отображаемых полей

Выбор отображаемых полей в зависимости от того, был ли создан виджет производится с помощью переопределения метода get_fields

def get_fields(self, request, obj=None):
    if obj is not None:
        return super().get_fields(request, obj)
    return ['widget_type']

Тоже самое относится и к readonly_fields . Я специально ограничиваю возможность редактирования поля widget_type , когда объект виджета уже существует в базе данных. В противном случае это может привести к усложнению логики программного кода для администрирования виджетов. А этого я не хочу.

def get_readonly_fields(self, request, obj=None):
    readonly_fields = super().get_readonly_fields(request, obj)
    if obj is not None:
        readonly_fields.append('widget_type')
    return readonly_fields

Настройка Inline формы

Но самым важным является выбор Inline формы в зависимости от выбранног типа виджета. Как видно из кода, я выбираю форму с помощью заранее подготовленного словаря Inline объектов, которые соответствуют нужному типу специфической реализации.

def get_inline_instances(self, request, obj=None):
    if obj is not None:
        return [WIDGET_TYPE_TO_INLINE[obj.widget_type](self.model, self.admin_site)]
    return []

SideBar

Ну и самым простым является отображение SideBar модели

class SideBarToWidgetInline(admin.TabularInline):
    model = SideBarToWidget
    fields = ['number', 'widget']
    verbose_name_plural = _('Widgets')
    extra = 1


class SideBarAdmin(SingletonModelAdmin):
    inlines = [SideBarToWidgetInline]

Регистрация в административной модели

Остаётся только зарегистрировать модели в административной панели. И заметьте, я не регистрирую отдельно специфические реализации. Я считаю, что подобная информация не должна быть доступна напрямую, а только через Inline формы.

admin.site.register(SideBar, SideBarAdmin)
admin.site.register(Widget, WidgetAdmin)

Шаблоны

Так будут выглядеть шаблоны специфических реализаций в моём случае

htmlwidgetimpl.html

{{ content|safe }}

standardwidgetimpl.html

<div class="card box-shadow m-2">
  <h5 class="card-header text-center">{{ title }}</h5>
  <div class="card-body">
    {{ content|safe }}
  </div>
</div>

Шаблонные теги

И в заключение покажу, как отобразить SideBar с помощью шаблонного тега

templatetags/evileg_widgets.py

# -*- coding: utf-8 -*-

from django import template
from evileg_widgets.models import SideBar

register = template.Library()


@register.inclusion_tag('evileg_widgets/sidebar.html')
def sidebar():
    return {'widgets': SideBar.get_solo().widgets.order_by('sidebartowidget__number')}

evilge_widgets/sidebar.html

{% for widget in widgets %}
  {{ widget.render }}
{% endfor %}

Рендеринг SideBar

{% load sidebar from evileg_widgets %}
{% sidebar %}

Заключение

  • Подобный подход полезен не только для динамических виджетов, но и в целом может быть очень хорошим архитектурным решением для ряда задач.
  • Преимущество такого подхода заключается в том, что вы можете значительно упростить код шаблонов сайта, а также скрыть реальную реализацию за полиморфной моделью, которая выступает в роли адаптера.
  • Также очень важным является то, что для внедрения дальнейших типов объекта достаточно будет только добавить новую модель реализации и обновить все необходимые переменныю в административной модели, в некоторых случаях дописать формы для фронтенда.
  • Но самое главное то, что это действительно является решением, которое формирует архитектуру проекта. А архитектура означает то, что дальнейшее расширение уже будет более менее стандартизировано и при работе нескольких программистов в проекте не потребует для новичков писать что-то новое. Им достаточно будет понять как функционирует данная архитектура, а внедрение новых типов станет для лёгкой рутинной задачей.
Рекомендуем хостинг TIMEWEB
Рекомендуем хостинг TIMEWEB
Стабильный хостинг, на котором располагается социальная сеть EVILEG. Для проектов на Django рекомендуем VDS хостинг.
- блог компании
Поддержать автора Donate

Комментарии

Только авторизованные пользователи могут публиковать комментарии.
Пожалуйста, авторизуйтесь или зарегистрируйтесь
Timeweb

Позвольте мне порекомендовать вам отличный хостинг, на котором расположен EVILEG.

В течение многих лет Timeweb доказывает свою стабильность.

Для проектов на Django рекомендую VDS хостинг

Посмотреть Хостинг
k
  • knobu
  • 23 сентября 2020 г. 2:34

C++ - Тест 006. Перечисления

  • Результат:60баллов,
  • Очки рейтинга-1
k
  • knobu
  • 23 сентября 2020 г. 2:21

C++ - Тест 005. Структуры и Классы

  • Результат:91баллов,
  • Очки рейтинга8
k
  • knobu
  • 23 сентября 2020 г. 2:16

C++ - Тест 004. Указатели, Массивы и Циклы

  • Результат:80баллов,
  • Очки рейтинга4
Последние комментарии

Qt/C++ - Урок 006. QSqlQueryModel - Таблицы в Qt с помощью SQL-запросов

QSqlTableModel выполняет ряд стандартных операций для одной таблицы из базы данных. Поэтому там и реализован функционал по удалению и редактированию. QSqlQueryModel позволяет выполнить запр…
VB

Qt/C++ - Урок 006. QSqlQueryModel - Таблицы в Qt с помощью SQL-запросов

Добрый день. Хотел спросить вот что. Создал проект на основе QAbstractTableModel. В MainWindow cоответственно создал модель и связал с представлением. Поиск веду по списку элементов модели,…

QCheckBox в качестве делегата QTableView

До тех пор, пока у вас проект содержит только одну таблицу, или несколько то может быть. Когда их будет 1000 и чекбоксы в разных колонках, то без делегатов и переопределения возвращаемых ре…
D
  • Damir
  • 20 сентября 2020 г. 15:34

QCheckBox в качестве делегата QTableView

bool Node::setData(const QModelIndex& index, const QVariant& value, int role){ switch (index.column()) { case 0: switch (role) { case Qt::CheckStateRole:// <- т…
VB

Qt/C++ - Урок 004. QSqlTableModel или Как представить таблицу из БД в Qt?

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

Как в Qt в qmenu добавить scrollarea

Вот это наследованный класс меню. Но посути это обычное меню. #pragma once#include <QtWidgets>class TransMenu : public QMenu { Q_OBJECTpublic: TransMenu(QWidget* parent = …

Как в qml работать с динамически созданными потомками?

В QML есть сборщик мусора, он может удалять объекты не сразу а по ппрошествии времени. Попробуйте при удалении вызывать сборщик мусора принудительно через gc()
VB

Как запустить программу с базой данных PostgreSQL на другом компьютере

Не требует никакую библиотеку, запускается на других компьютерах, где не установлена PostgreSQL, но создать элемент невозможно, тем более отредактировать или удалить.
p
  • prod1s
  • 24 сентября 2020 г. 7:12

через QT не могу открыть файл SQLite

Вирішення знайшов. Вказав замість назви БД об'єкт класу QSqlDataBase для QSqlQuery. QSqlQuery m_query = QSqlQuery(qSqlDataBase); Після двох днів пошуку рішення, все-таки знайшов…
U

как скрыть елемент с копии виджета

Дело в том, что ui класса находится в private-секции... И из-вне доступ получить, не нарушая канонов - не получится) Можно конечно сделать что-то в духе #define private public, но это для истинн…
О нас
Услуги
© EVILEG 2015-2020
Рекомендует хостинг TIMEWEB