Evgenii Legotckoi
Evgenii Legotckoi09 травня 2020 р. 16: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 хостинг.

Вам це подобається? Поділіться в соціальних мережах!

Коментарі

Only authorized users can post comments.
Please, Log in or Sign up
B
  • Bogdannn
  • 27 березня 2024 р. 19:21

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

  • Результат:16бали,
  • Рейтинг балів-10
B
  • Bogdannn
  • 27 березня 2024 р. 19:15

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

  • Результат:46бали,
  • Рейтинг балів-6
FL

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

  • Результат:80бали,
  • Рейтинг балів4
Останні коментарі
k
kmssr08 лютого 2024 р. 18:43
Qt Linux - Урок 001. Автозапуск програми Qt під Linux как сделать автозапуск для флэтпака, который не даёт создавать файлы в ~/.config - вот это вопрос ))
АК
Анатолий Кононенко05 лютого 2024 р. 01:50
Qt WinAPI - Урок 007. Робота з ICMP Ping в Qt Без строки #include <QRegularExpressionValidator> в заголовочном файле не работает валидатор.
EVA
EVA25 грудня 2023 р. 10:30
Boost - статичне зв&#39;язування в проекті CMake під Windows Ошибка LNK1104 часто возникает, когда компоновщик не может найти или открыть файл библиотеки. В вашем случае, это файл libboost_locale-vc142-mt-gd-x64-1_74.lib из библиотеки Boost для C+…
J
JonnyJo25 грудня 2023 р. 08:38
Boost - статичне зв&#39;язування в проекті CMake під Windows Сделал всё по-как у вас, но выдаёт ошибку [build] LINK : fatal error LNK1104: не удается открыть файл "libboost_locale-vc142-mt-gd-x64-1_74.lib" Хоть убей, не могу понять в чём дел…
G
Gvozdik18 грудня 2023 р. 21:01
Qt/C++ - Урок 056. Підключення бібліотеки Boost в Qt для компіляторів MinGW і MSVC Для решения твой проблемы добавь в файл .pro строчку "LIBS += -lws2_32" она решит проблему , лично мне помогло.
Тепер обговоріть на форумі
P
Pisych27 лютого 2023 р. 04:04
Как получить в массив значения из связанной модели? Спасибо, разобрался:))
AC
Alexandru Codreanu19 січня 2024 р. 11:57
QML Обнулить значения SpinBox Доброго времени суток, не могу разобраться с обнулением значение SpinBox находящего в делегате. import QtQuickimport QtQuick.ControlsWindow { width: 640 height: 480 visible: tr…
BlinCT
BlinCT27 грудня 2023 р. 08:57
Растягивать Image на парент по высоте Ну и само собою дял включения scrollbar надо чтобы был Flickable. Так что выходит как то так Flickable{ id: root anchors.fill: parent clip: true property url linkFile p…
Дмитрий
Дмитрий10 січня 2024 р. 04:18
Qt Creator загружает всю оперативную память Проблема решена. Удалось разобраться с помощью утилиты strace. Запустил ее: strace ./qtcreator Начал выводиться весь лог работы креатора. В один момент он начал считывать фай…
Evgenii Legotckoi
Evgenii Legotckoi12 грудня 2023 р. 06:48
Побуквенное сравнение двух строк Добрый день. Там случайно не высылается этот сигнал textChanged ещё и при форматировани текста? Если решиать в лоб, то можно просто отключать сигнал/слотовое соединение внутри слота и …

Слідкуйте за нами в соціальних мережах