Evgenii Legotckoi
Evgenii Legotckoi9. Mai 2020 06:09

Django - Tutorial 054. So erstellen Sie ein polymorphes System dynamischer Widgets

Neulich habe ich das Problem gelöst, der Website dynamische Widgets hinzuzufügen, mit der Möglichkeit, verschiedene Arten von Widgets hinzuzufügen, sowie mit der Möglichkeit, den Widget-Satz weiter unbegrenzt zu erweitern, indem Modelle spezifischer Implementierungen hinzugefügt werden.

Das System ist so aufgebaut, dass es eine Widget -Klasse gibt, die sich je nach Typ, der bei ihrer Erstellung gewählt wurde, auf ein bestimmtes Implementierungsmodell bezieht, zum Beispiel HTMLWidgetImpl , das einige spezielle Parameter enthält, und auch kennt seine Rendering-Vorlage. Widget in einer Websitevorlage, wenn die render() -Methode aufgerufen wird bezieht sich über den Typ auf die spezifische Implementierung und ihre render()**-Methode.

Dieser Ansatz ermöglicht es Ihnen, extrem einfachen Code für die Vorlage zu schreiben und den Code so zu organisieren, dass das Hinzufügen neuer Widgets darin besteht, ein neues Datenmodell hinzuzufügen sowie eine Reihe von Stellen im Code zu bearbeiten, oder besser gesagt aktualisieren sie. Was in der Tat zu einer Routineaufgabe wird und es einem Programmierer überflüssig macht, sich etwas Neues einfallen zu lassen.

Das heißt, in diesem Artikel möchte ich nicht nur die Lösung einiger Probleme auf der Ebene des Programmcodes zeigen, sondern auch einen Ansatz zur Architekturplanung bei der Lösung solcher Probleme zeigen.


Angabe der Aufgabe

Die Website muss also ein System dynamischer Widgets implementieren, die in der Seitenleiste der Website angezeigt werden. Gleichzeitig möchten wir mit zwei Arten von Widgets beginnen, eines enthält nur den zu rendernden HTML-Code und das zweite enthält den Titel und den HTML-Code für den Inhalt, der in eine vordefinierte Vorlage gerendert werden soll. Gleichzeitig müssen wir zum Einzeichnen der Seitenleiste in der Lage sein, die Reihenfolge der Widgets festzulegen.

Daher erstellen wir eine neue Anwendung

python manage.py startapp evileg_widgets

Und dann erstellen wir alle Modelle, die wir brauchen, und wir brauchen die folgenden Modelle

  • SideBar - Sidebar-Modell, werde ich übrigens als Singleton-Modell entwerfen
  • Widget - polymorphes Widget-Modell
  • HTMLWidgetImpl - Konkrete Widget-Implementierung zum Einfügen von einfachem HTML-Code
  • StandardWidgetImpl - spezifische Widget-Implementierung zum Einfügen von Rendering-Code
  • SideBarToWidget - Modell für ManyToMany-Felder zwischen SideBar-Modell und Widget-Modell

Modelle

Beginnen wir mit den Modellen, die wir erstellen.

Seitenleiste

Für das SideBar-Modell verwende ich das SingletonModel aus der Django-Solo-Drittanbieteranwendung, die darauf ausgelegt ist, Anpassungsobjekte zu bilden, die in einer einzelnen Instanz vorhanden sein sollten.
Das SideBar-Modell enthält das Widgets-Feld, das für die mit der Website verbundenen Widgets verantwortlich ist, aber die ManyToMany-Beziehung wird durch das SideBarToWidget-Zwischenmodell gebildet, das die Reihenfolge der Widgets zur SideBar hinzufügt.

# -*- 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

Im Wesentlichen setzen wir dieses Modell explizit für ein einzelnes Zahlen -Feld, mit dem wir die Reihenfolge der Widgets festlegen können.
Wenn Sie dieses Modell nicht explizit angeben, wird eine Tabelle erstellt, die Fremdschlüssel für die ManyToMany-Beziehungsobjekte enthält.

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

Und jetzt das interessanteste Modell mit polymorphem Charakter. Dieses Modell unterscheidet sich dadurch, dass es sein Verhalten durch den Typ definiert, der bei seiner Erstellung ausgewählt wurde.
Der Widget-Typ wird durch das Feld widget_type bestimmt.

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

Die Auswahl des Feldtyps ist mithilfe der folgenden Klassenvariablen konfigurierbar

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

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

Dank dieses Typs definiere ich klar, auf welche Tabelle ich mich beziehen soll, um eine bestimmte Implementierung zu erhalten.
Gleichzeitig halte ich mich an bestimmte Regeln, um den Namen der Beziehung zu bilden.
Dies kann bereits in dieser Variable korrekt nachvollzogen werden.

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

In diesem Wörterbuch entspricht der Widget-Typ dem Namen der Eigenschaft, die für den Zugriff auf eine bestimmte Implementierung verantwortlich ist.

  • htmlwidgetimpl -> HTMLWidgetimpl
  • StandardWidgetImpl -> StandardWidgetImpl

Die Beschreibung des Widgets, also der Methode str , wird wie folgt gebildet

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

Wie Sie sehen können, verwende ich entweder eine gegebene Beschreibung für das Widget, die der Einfachheit halber hinzugefügt wird, oder eine Beschreibung des Widget-Typs, die ich der Klassenvariablen WIDGET_TYPE_CHOICES bis zum aktuellen Wert von * widget_type entnehme. *

Am interessantesten ist jedoch die Methode render() , die für das Rendern des Widgets verantwortlich ist.

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

Die Render-Methode verwendet bereits Metaprogrammierung, da ich beim aktuellen Widget-Typ auf die spezifische Implementierung per Eigenschaftsname verweise.
Dazu wird die Python-Funktion getattr verwendet. Und wenn eine bestimmte Implementierung existiert, man weiß nie, etwas ist schief gelaufen, dann rufe ich die Methode render() der bestimmten Implementierung auf.

Vollständiger Code

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

Um einige Teile des Codes zu verallgemeinern, verwende ich ein abstraktes Modell für konkrete Implementierungen.

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')

Mit Hilfe dieses Modells bilde ich eine Eins-zu-Eins-Beziehung einer bestimmten Implementierung zu einem Widget.
Am interessantesten sind hier die folgenden zwei Dinge

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

Eigentlich habe ich in mehreren Artikeln nur wenige solcher Artikel gesehen, aber Tatsache ist, dass, wenn ich ein Modell schreibe, das nicht abstrakt ist, der Eintrag % (Klasse) s den Modellnamen nach dem folgenden Prinzip umwandelt

  • HTMLWidgetImpl -> htmlwidgetimpl

Daher definiere ich Namenskonventionen für implementierungsspezifische Eigenschaften.

HTMLWidgetImpl

Und jetzt sind wir endlich bei konkreten Implementierungen angelangt. Beispielsweise hat HTMLWidgetImpl ein Feld content und HTMLWidgetImpl kennt seine Rendering-Vorlage evileg_widgets/htmlwidgetimpl.html .

Daher haben wir hier unsere eigene Implementierung der Methode 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

Ebenso haben wir eine spezifische Implementierung für StandardWidgetImpl bereitgestellt.

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

Werfen wir nun einen Blick auf das Admin-Panel und wie es konfiguriert ist. Hier wird der Code bereits etwas einfacher sein, also werde ich ihn gleich geben und dann beschreiben, was er tut.

# -*- 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)

Im Einklang

Zuerst erstelle ich Inline-Formulare für jede spezifische Implementierung und binde sie auch an Widget-Typen.

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

Dann bilde ich die Widget-Formularverwaltung, in der ich zwei Phasen der Anzeige des Widget-Formulars definiere.

Beim Erstellen eines Widgets erlaube ich Ihnen, nur seinen Typ festzulegen

Und dann erlaube ich je nach Typ schon das Editieren bestimmter Eigenschaften. Übrigens wird auch eine Vorschau dieses Widgets angezeigt. Für die Vorschau müssen Sie Ihr eigenes CSS hinzufügen, dies gilt jedoch nicht für den Artikel. Und auch der Screenshot zeigt Mehrsprachigkeit, aber das gilt auch nicht für den Artikel, ich kann nur sagen, dass Sie für Mehrsprachigkeit django-modeltranslation verwenden müssen

Auswahl und Konfiguration der angezeigten Felder

Die Auswahl der anzuzeigenden Felder in Abhängigkeit davon, ob das Widget erstellt wurde, erfolgt durch Überschreiben der Methode get_fields

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

Dasselbe gilt für readonly_fields . Ich schränke ausdrücklich die Möglichkeit ein, das Feld widget_type zu bearbeiten, wenn das Widget-Objekt bereits in der Datenbank vorhanden ist. Andernfalls kann es die Logik des Widget-Verwaltungscodes verkomplizieren. Ich will es nicht.

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

Richten Sie ein Inline-Formular ein

Aber das Wichtigste ist, je nach ausgewähltem Widget-Typ die Inline -Form zu wählen. Wie Sie dem Code entnehmen können, wähle ich das Formular mithilfe eines vorgefertigten Wörterbuchs von Inline-Objekten aus, die dem gewünschten Typ der spezifischen Implementierung entsprechen.

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 []

Seitenleiste

Nun, am einfachsten ist die Anzeige von Sidebar-Modellen

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


class SideBarAdmin(SingletonModelAdmin):
    inlines = [SideBarToWidgetInline]

Registrierung im Verwaltungsmodell

Es bleibt nur noch, Modelle im Verwaltungsbereich zu registrieren. Und beachten Sie, dass ich bestimmte Implementierungen nicht separat registriere. Ich bin der Meinung, dass solche Informationen nicht direkt verfügbar sein sollten, sondern nur über Inline-Formulare.

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

Vorlagen

So sehen in meinem Fall die Vorlagen bestimmter Implementierungen aus

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>

Vorlagen-Tags

Abschließend zeige ich Ihnen, wie Sie eine Seitenleiste mithilfe eines Template-Tags anzeigen.

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 %}

Darstellung der Seitenleiste

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

Fazit

  • Dieser Ansatz ist nicht nur für dynamische Widgets nützlich, sondern kann im Allgemeinen eine sehr gute architektonische Lösung für eine Reihe von Aufgaben sein.
  • Der Vorteil dieses Ansatzes besteht darin, dass Sie den Websitevorlagencode erheblich vereinfachen und die tatsächliche Implementierung hinter einem polymorphen Modell verstecken können, das als Adapter fungiert.
  • Es ist auch sehr wichtig, dass es zur Implementierung weiterer Objekttypen nur ausreicht, ein neues Implementierungsmodell hinzuzufügen und alle erforderlichen Variablen im Verwaltungsmodell zu aktualisieren, in einigen Fällen Formulare für das Frontend hinzuzufügen.
  • Aber das Wichtigste ist, dass es wirklich eine Entscheidung ist, die die Architektur des Projekts bildet. Und die Architektur bedeutet, dass der weitere Ausbau bereits mehr oder weniger standardisiert ist, und wenn mehrere Programmierer an einem Projekt arbeiten, müssen Neueinsteiger nicht etwas Neues schreiben. Es genügt ihnen zu verstehen, wie diese Architektur funktioniert, und die Einführung neuer Typen wird für sie zur einfachen Routine.
Рекомендуємо хостинг TIMEWEB
Рекомендуємо хостинг TIMEWEB
Stabiles Hosting des sozialen Netzwerks EVILEG. Wir empfehlen VDS-Hosting für Django-Projekte.

Magst du es? In sozialen Netzwerken teilen!

Kommentare

Nur autorisierte Benutzer können Kommentare posten.
Bitte Anmelden oder Registrieren
Letzte Kommentare
ИМ
Игорь Максимов5. Oktober 2024 07:51
Django – Lektion 064. So schreiben Sie eine Python-Markdown-Erweiterung Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas55. Juli 2024 11:02
QML - Lektion 016. SQLite-Datenbank und das Arbeiten damit in QML Qt Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
k
kmssr8. Februar 2024 18:43
Qt Linux - Lektion 001. Autorun Qt-Anwendung unter Linux как сделать автозапуск для флэтпака, который не даёт создавать файлы в ~/.config - вот это вопрос ))
Qt WinAPI - Lektion 007. Arbeiten mit ICMP-Ping in Qt Без строки #include <QRegularExpressionValidator> в заголовочном файле не работает валидатор.
EVA
EVA25. Dezember 2023 10:30
Boost - statisches Verknüpfen im CMake-Projekt unter Windows Ошибка LNK1104 часто возникает, когда компоновщик не может найти или открыть файл библиотеки. В вашем случае, это файл libboost_locale-vc142-mt-gd-x64-1_74.lib из библиотеки Boost для C+…
Jetzt im Forum diskutieren
J
JacobFib17. Oktober 2024 03:27
добавить qlineseries в функции Пользователь может получить любые разъяснения по интересующим вопросам, касающимся обработки его персональных данных, обратившись к Оператору с помощью электронной почты https://topdecorpro.ru…
JW
Jhon Wick1. Oktober 2024 15:52
Indian Food Restaurant In Columbus OH| Layla’s Kitchen Indian Restaurant If you're looking for a truly authentic https://www.laylaskitchenrestaurantohio.com/ , Layla’s Kitchen Indian Restaurant is your go-to destination. Located at 6152 Cleveland Ave, Colu…
КГ
Кирилл Гусарев27. September 2024 09:09
Не запускается программа на Qt: точка входа в процедуру не найдена в библиотеке DLL Написал программу на C++ Qt в Qt Creator, сбилдил Release с помощью MinGW 64-bit, бинарнику напихал dll-ки с помощью windeployqt.exe. При попытке запуска моей сбилженной программы выдаёт три оши…
F
Fynjy22. Juli 2024 04:15
при создании qml проекта Kits есть но недоступны для выбора Поставил Qt Creator 11.0.2. Qt 6.4.3 При создании проекта Qml не могу выбрать Kits, они все недоступны, хотя настроены и при создании обычного Qt Widget приложения их можно выбрать. В чем может …

Folgen Sie uns in sozialen Netzwerken