Evgenij LegotskojMay 9, 2020, 6:09 a.m.

Django - Tutorial 054. How to create a polymorphic system of dynamic widgets

Content

The other day, I solved the problem of adding dynamic widgets on the site with the ability to add different types of widgets, as well as with the possibility of further unlimited expansion of the widget set by adding models of specific implementations.

The system is such that there is one class Widget , which, depending on the type selected when creating it, refers to a specific implementation model, for example HTMLWidgetImpl , which contains some special parameters, and also knows about its template for rendering. Widget in the site template when calling the render() method refers to the specific implementation and its method render()** through the type.

This approach allows you to write an extremely simple code for the template, as well as organize the code so that adding new widgets will consist in adding a new data model, as well as correcting a number of places in the code, or rather updating them. Which in essence will become a routine task and will remove from the programmer the need to come up with something new.

That is, in this article I want to show not only the solution to some problem at the level of program code, but also to show the approach to architecture planning when solving such problems.

Formulation of the problem

So on the site you need to implement a system of dynamic widgets that will be displayed in the sidebar of the site. At the same time, we want to start with two types of widgets, one will contain just html code for rendering, and the second will contain a header and html code for content that will be rendered into a predefined template. At the same time, for rendering in the sidebar, we need to be able to set the sequence of widgets.

Therefore, we create a new application

python manage.py startapp evileg_widgets

And then create all the models we need, and we need the following models

  • SideBar - sidebar model, by the way I will design it as a Singleton model
  • Widget - polymorphic widget model
  • HTMLWidgetImpl - specific widget implementation for inserting simple html code
  • StandardWidgetImpl - specific widget implementation for inserting rendering code
  • SideBarToWidget - model for ManyToMany fields between SideBar model and Widget model

models

Let's start with the models we create.

SideBar

For the SideBar model, I use SingletonModel from the third-party django-solo application, which is intended to form tuning objects that must exist in one single instance.
The SideBar model contains a widgets field that will be responsible for the set of widgets connected to the site, but ManyToMany relationship will be formed through an intermediate SideBarToWidget model, which will add widget order to 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

In essence, we explicitly set this model for the sake of a single field number , which allows us to set the order of widgets.
If you do not explicitly specify this model, a table will be generated that contains foreign keys to the ManyToMany relationship objects.

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

And now the most interesting model with a polymorphic character. This model is different in that it determines its behavior by the type selected during creation.
The type of widget is determined by the widget_type field

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

Field type selections are configured using the following class variables

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

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

Thanks to this type, I clearly define which table I should refer to in order to get a specific implementation.
In this case, I follow certain rules for the formation of the name of the relationship.
This can already be traced correctly in this variable.

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

In this dictionary, the widget type corresponds to the name of the property, which is responsible for access to a specific implementation

  • htmlwidgetimpl -> HTMLWidgetImpl
  • standardwidgetimpl -> StandardWidgetImpl

Description of the widget, that is, the method __str__ is formed as follows

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

As you can see, I use either the specified description for the widget, which is added for convenience, or the description of the widget type, which I take from the class variable WIDGET_TYPE_CHOICES through the current value widget_type

But the most interesting is the render() method, which is responsible for rendering the widget

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

The rendering method already uses meta-programming, because with the help of the current type of widget I refer to a specific implementation by the name of the property.
For this, the python getattr function is used. And if a specific implementation exists, you never know, something went wrong, then I call the render() method of the specific implementation.

Полный код

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

To generalize some parts of the code, I use an abstract model for specific implementations.

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

With the help of this model, I form OneToOne relation of a specific implementation to a widget.
The most interesting thing here is the following two things

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

Actually, I have seen few such articles in a few articles, but the point is that when I write a model that is not abstract, the record %(class)s converts the model name according to the following principle

  • HTMLWidgetImpl -> htmlwidgetimpl

Thus, I define the rules for the formation of names for the properties of specific implementations.

HTMLWidgetImpl

And now we finally got to specific implementations. For example, for HTMLWidgetImpl there is a content field, and also HTMLWidgetImpl knows about its template for rendering evileg_widgets/htmlwidgetimpl.html .

Therefore, here we have our own implementation of the render() method

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

Similarly, we have implemented a specific implementation for 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

Now let's look at the administration panel and how it is configured. Here, the code will already be a little easier, so I will give it right away, and then I will describe what it does.

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

First, I create Inline formets for each specific implementation, and also bind them to widget types

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

After which I form the administration of the widget form, in which I define two stages of displaying the widget form.

When creating a widget, I allow to set only its type

And then, depending on the type, I already allow editing specific properties. By the way, also displaying a preview of this widget. For preview you need to add your css, but this does not apply to the article. And also the multilanguage is displayed on the screenshot, but this also does not apply to the article, I will only say that django-modeltranslation should be used for multilanguage

Selecting and customizing the displayed fields

The selection of the displayed fields depending on whether the widget was created is done by overriding the get_fields method

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

The same goes for readonly_fields . I specifically restrict the ability to edit the widget_type field when the widget object already exists in the database. Otherwise, this may complicate the logic of the program code for administering widgets. I don’t want that.

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

Setting up an inline form

But the most important is the choice of Inline form depending on the selected widget type. As you can see from the code, I select the form using a pre-prepared dictionary of Inline objects that correspond to the desired type of specific implementation.

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

Well and the simplest is to display Sidebar models

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


class SideBarAdmin(SingletonModelAdmin):
    inlines = [SideBarToWidgetInline]

Registration in the administrative model

All that remains is to register the models in the administrative panel. And note, I do not separately register specific implementations. I believe that such information should not be available directly, but only through inline forms.

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

Templates

So templates of specific implementations will look in my case

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>

Template tags

In conclusion, I’ll show you how to display SideBar using a template tag.

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 rendering

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

Conclusion

  • This approach is useful not only for dynamic widgets, but in general it can be a very good architectural solution for a number of tasks.
  • The advantage of this approach is that you can greatly simplify the code of the site templates, and also hide the real implementation behind the polymorphic model, which acts as an adapter.
  • It is also very important that for the introduction of further types of objects it will be enough only to add a new implementation model and update all the necessary variables in the administrative model, in some cases, add forms for the frontend.
  • But the most important thing is that it really is a solution that forms the architecture of the project. And architecture means that further expansion will already be more or less standardized, and when several programmers work in a project, it will not require newcomers to write something new. It will be enough for them to understand how this architecture works, and the introduction of new types will become an easy routine for them.
We recommend hosting TIMEWEB
We recommend hosting TIMEWEB
Stable hosting, on which the social network EVILEG is located. For projects on Django we recommend VDS hosting.
- company blog
Support the author Donate

Comments

Only authorized users can post comments.
Please, Log in or Sign up
Timeweb

Let me recommend you the excellent hosting on which EVILEG is located.

For many years, Timeweb has been proving his stability.

For projects on Django I recommend VDS hosting

View Hosting
VD

C++ - Test 001. The first program and data types

  • Result:73points,
  • Rating points1
Ds

C++ - Тест 003. Условия и циклы

  • Result:64points,
  • Rating points-1
o

C++ - Test 001. The first program and data types

  • Result:86points,
  • Rating points6
Last comments
RG

QML - Lesson 016. SQLite database and the working with it in QML Qt

Добрый день! можно как то обойтись без метода updateModel()? После вызова этого метода происходит перерисовка страницы(если я правильно понимаю), и все элементы, например, CheckBox перерисовываю…
D:

QML - Lesson 016. SQLite database and the working with it in QML Qt

Добрый день, пытаюсь разобраться и подргнать пример под себя. Есть бд с огромным количеством полей. В приложении на виджетах при использовании QTableView все работает и путем простого sql запрос…

Django - Tutorial 039. Adding private messages and chats on the site - Part 2 (Dialogue and chat counter with unread messages)

Добавляйте поле файла в модель сообщения. И в форме сообщения указывайте, что поле с файлом.
s

Django - Tutorial 023. Like Dislike system using GenericForeignKey

все, я со всем разобрался!) Извините!)
Now discuss on the forum

Наследование QWidget

Это утверждение ничего не значит. Наличие методов и т.д. не делает обязательным наследование в том виде, в котором вы его изначально попытались сделать. Тем более, если у вас будет два видж…

Динамическое заполнение StackLayout в qml

Всем привет. Пытаюсь решить такую задачку, есть TabBar и его кнопки. StackLayout{ currentIndex: tabBar.currentIndex A {id: tabA} B {id: tabB} C {id: tabC} D {id: ta…
M

QML: изменение стиля при наведении и при нажатии на кнопку

enabled = false перестанет быть активной и не будет ни на что реагировать) Хм.. по-моему пробовал такое. Проверю ещё раз после работы. Ура, спасибо большо…
U

Динамическое наполнение StackView QML

Во затупил))) Спасибо за все))) StackView.push("ModuleTip1.qml") ну или в сложной иерархии StackView.push("qrc:/folder/ModuleTip1.qml") и всего делов... Не пойму, почему сра…

QEventLoop тормозит при удалении экземпляра

Думаю, что нет. Лучше вообще без исключений, но не всегда возможно.
About
Services
© EVILEG 2015-2020
Recommend hosting TIMEWEB