Евгений Легоцкой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

Комментарии

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

Внесите вклад в развитие сообщества EVILEG.

Узнайте, как стать автором сайта.

Изучить
Donate

Добрый день, Дорогие Пользователи !!!

Я Евгений Легоцкой, разработчик EVILEG. И это мой хобби-проект, который помогает учиться программированию другим программистам и разработчикам

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

PayPalYandex.Money
Timeweb

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

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

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

Посмотреть Хостинг
R

C++ - Тест 002. Константы

  • Результат:75баллов,
  • Очки рейтинга2
R

C++ - Тест 001. Первая программа и типы данных

  • Результат:73баллов,
  • Очки рейтинга1
MS

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

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

Django - Урок 027. Добавление Google reCAPTCHA

Спасибо. Только использую декоратор не в urls.py а перед views
R

Qt WinAPI - Урок 001. Как собрать все DLL, используемые в Qt-проекте?

Вы меня не совсем правильно поняли, но все равно спасибо, принял все к сведению. Все сделал как вы сказали, все отлично работает, еще раз огромнейшее спасибо) Разве что только что были опять про…

Qt WinAPI - Урок 001. Как собрать все DLL, используемые в Qt-проекте?

Стоило перед использованием что ли инструкцию прочитать https://www.cyberforum.ru/blogs/131347/blog2457.html "После сборки при запуске требовались dll," Ясное дело стоило задепло…
R
R

Qt WinAPI - Урок 001. Как собрать все DLL, используемые в Qt-проекте?

Да, собралось. После сборки при запуске требовались dll, перекинул всю папки bin, plugins(не знаю как можно было сделать более умно). Как я понял в первой строке путь к екзешнику вставляю, втор…
Сейчас обсуждают на форуме
_
  • _focus
  • 5 июля 2020 г. 1:50

Не работают слоты/сигналы

Помогите разобраться. MainWindow::on_push_autorisation_clicked() - при нажатии на кнопку отправляется сигнал. В слоте выводим текст и отправляем сигнал дальше. Если не отправлять сигна…

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

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

Qt C++ и Python

Красиво/некрасиво - это скорее моё личное отношение. Если есть возможность ограничить количество интсрументов, то лучше ограничить. Но не зацикливайтесь на этом. Если у вас есть скрипты Py…

Qt + OpenGL glDeleteVertexArrays

Я не уверен, поскольку с OpenGL очень мало работал. Но может быть OpenGL контекст виджета нужно переинициализовывать. И ещё виджет стоит удалять через метод deleteLater() а не п…

QWebEngineView не запускается если к ПК подключено несколько мониторов

Ну я имел ввиду посмотреть на другом ПК с другой графикой и парой мониторов. Как моей программе назначить использовать определенный граф. адаптер? Вот тут понятия не имею.
О нас
Услуги
© EVILEG 2015-2020
Рекомендует хостинг TIMEWEB