- 1. Formulation of the problem
- 2. models
- 3. admin.py
- 4. Templates
- 5. Template tags
- 6. Conclusion
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.