Evgenii Legotckoi
Evgenii LegotckoiMay 9, 2020, 6:09 a.m.

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

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.

Do you like it? Share on social networks!

Comments

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

C ++ - Test 004. Pointers, Arrays and Loops

  • Result:50points,
  • Rating points-4
m

C ++ - Test 004. Pointers, Arrays and Loops

  • Result:80points,
  • Rating points4
m

C ++ - Test 004. Pointers, Arrays and Loops

  • Result:20points,
  • Rating points-10
Last comments
ИМ
Игорь МаксимовNov. 22, 2024, 7:51 p.m.
Django - Tutorial 017. Customize the login page to Django Добрый вечер Евгений! Я сделал себе авторизацию аналогичную вашей, все работает, кроме возврата к предидущей странице. Редеректит всегда на главную, хотя в логах сервера вижу запросы на правильн…
Evgenii Legotckoi
Evgenii LegotckoiOct. 31, 2024, 9:37 p.m.
Django - Lesson 064. How to write a Python Markdown extension Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup
A
ALO1ZEOct. 19, 2024, 3:19 p.m.
Fb3 file reader on Qt Creator Подскажите как это запустить? Я не шарю в программировании и кодинге. Скачал и установаил Qt, но куча ошибок выдается и не запустить. А очень надо fb3 переконвертировать в html
ИМ
Игорь МаксимовOct. 5, 2024, 2:51 p.m.
Django - Lesson 064. How to write a Python Markdown extension Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas5July 5, 2024, 6:02 p.m.
QML - Lesson 016. SQLite database and the working with it in QML Qt Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
Now discuss on the forum
Evgenii Legotckoi
Evgenii LegotckoiJune 24, 2024, 10:11 p.m.
добавить qlineseries в функции Я тут. Работы оень много. Отправил его в бан.
t
tonypeachey1Nov. 15, 2024, 2:04 p.m.
google domain [url=https://google.com/]domain[/url] domain [http://www.example.com link title]
NSProject
NSProjectJune 4, 2022, 10:49 a.m.
Всё ещё разбираюсь с кешем. В следствии прочтения данной статьи. Я принял для себя решение сделать кеширование свойств менеджера модели LikeDislike. И так как установка evileg_core для меня не была возможна, ибо он писался…
9
9AnonimOct. 25, 2024, 4:10 p.m.
Машина тьюринга // Начальное состояние 0 0, ,<,1 // Переход в состояние 1 при пустом символе 0,0,>,0 // Остаемся в состоянии 0, двигаясь вправо при встрече 0 0,1,>…

Follow us in social networks