Evgenii Legotckoi
Evgenii Legotckoi09 травня 2020 р. 06: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
AD

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

  • Результат:50бали,
  • Рейтинг балів-4
m
  • molni99
  • 26 жовтня 2024 р. 01:37

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

  • Результат:80бали,
  • Рейтинг балів4
m
  • molni99
  • 26 жовтня 2024 р. 01:29

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

  • Результат:20бали,
  • Рейтинг балів-10
Останні коментарі
ИМ
Игорь Максимов22 листопада 2024 р. 11:51
Django - Підручник 017. Налаштуйте сторінку входу до Django Добрый вечер Евгений! Я сделал себе авторизацию аналогичную вашей, все работает, кроме возврата к предидущей странице. Редеректит всегда на главную, хотя в логах сервера вижу запросы на правильн…
Evgenii Legotckoi
Evgenii Legotckoi31 жовтня 2024 р. 14:37
Django - Урок 064. Як написати розширення для Python Markdown Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup
A
ALO1ZE19 жовтня 2024 р. 08:19
Читалка файлів fb3 на Qt Creator Подскажите как это запустить? Я не шарю в программировании и кодинге. Скачал и установаил Qt, но куча ошибок выдается и не запустить. А очень надо fb3 переконвертировать в html
ИМ
Игорь Максимов05 жовтня 2024 р. 07:51
Django - Урок 064. Як написати розширення для Python Markdown Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas505 липня 2024 р. 11:02
QML - Урок 016. База даних SQLite та робота з нею в QML Qt Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
Тепер обговоріть на форумі
Evgenii Legotckoi
Evgenii Legotckoi24 червня 2024 р. 15:11
добавить qlineseries в функции Я тут. Работы оень много. Отправил его в бан.
t
tonypeachey115 листопада 2024 р. 06:04
google domain [url=https://google.com/]domain[/url] domain [http://www.example.com link title]
NSProject
NSProject04 червня 2022 р. 03:49
Всё ещё разбираюсь с кешем. В следствии прочтения данной статьи. Я принял для себя решение сделать кеширование свойств менеджера модели LikeDislike. И так как установка evileg_core для меня не была возможна, ибо он писался…
9
9Anonim25 жовтня 2024 р. 09:10
Машина тьюринга // Начальное состояние 0 0, ,<,1 // Переход в состояние 1 при пустом символе 0,0,>,0 // Остаемся в состоянии 0, двигаясь вправо при встрече 0 0,1,>…

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