Forms

forms module is split into forms package with the following submodules:

Renderers

See renderers module for source code.

django-jinja-knockout uses Renderer derived classes to display Django model forms and inline formsets. Recent versions of Django utilize renderers with templates to display form field widgets. There are some packages that use renderers with templates to generate the whole forms. In addition to that, django-jinja-knockout uses renderers to generate the formsets with the related forms, which follows Django DRY approach. It’s possible to override the displayed HTML partially or completely.

The base Renderer class is located in tpl module and is not tied to any field / form / formset. It may be used in any template context.

The instance of Renderer holds the following related data:

  • self.obj - a Python object that should be displayed (converted to HTML / string);

  • .get_template_name() - method to obtain a DTL or Jinja2 template name, which could be hardcoded via .template class attribute, or can be dynamically generated depending on the current value of self.obj.

    See FieldRenderer for example of dynamic template name generation based on self.obj value, where self.obj is an instance of model form field;

  • self.context - a Python dict with template context that will be passed to the rendered template;

The instance of Renderer encapsulates the object and the template with it’s context to convert self.obj to string / HTML representation. Basically, it’s an extensible string formatter. See .__str__() and .__call__() methods of Renderer class for the implementation.

The built-in renderers support both ordinary input fields POST forms (non-AJAX and AJAX versions) and the display forms (read-only forms): Displaying read-only “forms”.

Multiple objects should not re-use the same Renderer derived instance: instead the renderers of nested objects are nested into each other (object composition). Here is the complete list of nested hierarchies of the built-in renderers:

FieldRenderer

Renders model form field.

Default FieldRenderer class instance attached to each form field will apply bootstrap HTML to the form fields / form field labels. It supports both input fields POST forms (non-AJAX and AJAX versions) and the display forms (read-only forms): Displaying read-only “forms”. Templates are chosen dynamically depending on the field type.

The instance of FieldRenderer class is attached to each visible form field. By default the form fields are rendered by FormFieldsRenderer this way:

{% for field in form.visible_fields() -%}
    {{ field.djk_renderer() }}
{% endfor -%}

It’s possible to render “raw” field with:

{{ field }}

or formatted field as:

<div>{{ form.my_field.djk_renderer() }}</div>

and to render the list of selected fields with:

{{ render_fields(form, 'field1', 'fieldN') }}

FormBodyRenderer

Renders only the form body, no <form> tag, similar to how Django converts form to string.

In Jinja2 template call render_form() template context function:

{{ render_form(request, 'body', form, opts) }}

See FormBodyRenderer class. See opts argument.

FormFieldsRenderer

Renders the form fields, by default one by one via their specified order. See Rendering customization how to override the default layout of fields.

In Jinja2 template to call form fields renderer:

{{ form.djk_renderer['fields']() }}

or:

{{ render_form(request, 'fields', form, opts) }}

See FormFieldsRenderer class. See opts argument.

StandaloneFormRenderer

Standalone form renderer includes the whole form with the body (fields, field labels), <form> tag, wrapped into bootstrap card tags. It’s a complete HTML form with separate visual look which could be directly submitted to view.

Renders the instance of model form:

In Jinja2 template call bs_form() macro or call render_form() template context function:

{{ render_form(request, 'standalone', form, {
    'action': action,
    'opts': opts,
    'method': method,
}) }}

Rendering FormWithInlineFormsets

To render FormWithInlineFormsets class, in Jinja2 template call bs_inline_formsets() macro, which calls the following hierarchy of renderers:

Note that is the composition hierarchy of instances, not a class inheritance hierarchy.

Single formset is rendered with the following call:

{{ formset.djk_renderer() }}

opts argument

opts dict argument optionally passed to bs_form() / bs_inline_formsets() macros / render_form() template context function / form renderers support the following keys:

  • class - CSS class of bootstrap panel form wrapper;
  • is_ajax - bool, whether the form should be submitted via AJAX - by default is False; see AJAX forms processing for more info;
  • layout_classes - change default Bootstrap grid layout width for field labels / field inputs. See Changing bootstrap grid layout for more details;
  • submit_text - text of form submit button; if not defined, no button will be displayed;
  • title - text of bootstrap panel title form wrapper; if not defined, no title will be displayed;

Some attributes are used only by some renderers:

  • inline_title - the title of inline form, which could be different from title of related / standalone form;
  • table_classes - CSS classes of form table wrapper for Displaying read-only “forms”;

Rendering customization

The most simplest way to customize form is to override / extend one of the default model form templates via overriding RendererModelForm template attributes, for example to change inline form wrapper:

class EquipmentForm(RendererModelForm):

    inline_template = 'inline_equipment_form.htm'

To change field templates one should override RendererModelForm Meta class field_templates dict attribute:

class ClubMemberDisplayForm(WidgetInstancesMixin, RendererModelForm, metaclass=DisplayModelMetaclass):

    inline_template = 'inline_form_chevron.htm'
    fields_template = 'form_fields_club_group_member_display.htm'

    class Meta:

        model = ClubMember

        fields = [
            'role',
            'profile',
            'note',
        ]
        field_templates = {
            'role': 'field_items.htm',
            'note': 'field_items.htm',
        }

Since v2.2.0, it’s possible to customize fields layout without altering default form body by overriding:

Custom fields_template can include non-modelform fields and / or custom Components.

To change formset template, one should set the value of formset class attribute like this:

ClubEquipmentFormSet = ko_inlineformset_factory(
    Club, Equipment, form=EquipmentForm, extra=0, min_num=1, max_num=5, can_delete=True
)
ClubEquipmentFormSet.template = 'club_equipment_formset.htm'

It’s also possible to use raw built-in rendering, which does not uses Jinja2 templates. To achieve that, set the template name value to empty string ‘’. In such case renderer instance .render_raw() method will be called to convert self.obj with it’s current context to the string. For more complex cases one may override .render_raw() method via inherited renderer class.

To use custom renderer classes with model forms, one may override BootstrapModelForm Meta class default renderer attributes with the extended classes:

class MyModelForm(BootstrapModelForm):

    class Meta(BootstrapModelForm.Meta):
        render_fields_cls = MyFormFieldsRenderer
        # render_body_cls = MyFormBodyRenderer
        # render_inline_cls = MyInlineFormRenderer
        # render_related_cls = MyRelatedFormRenderer
        render_standalone_cls = MyStandaloneFormRenderer

but in most of the cases overriding the template names is enough.

See renderer template samples in djk-sample project for the example of simple customization of default templates.

Forms base module

See base module source code.

RendererModelForm

While it’s possible to use renderers with ordinary Django ModelForm class, the recommended way is to derive model form class from RendererModelForm class:

from django_jinja_knockout.forms import RendererModelForm

class ProfileForm(RendererModelForm):

    class Meta:
        model = Profile
        exclude = ('age',)
        fields = '__all__'

By default, in case there are no custom templates / no custom renderers specified, render_form() will use the default renderers from BootstrapModelForm Meta class, which would stylize model form with Bootstrap attributes.

RendererModelForm class .has_saved_instance() method used to check whether current Django ModelForm has the bound and saved instance.

AJAX forms processing

django_jinja_knockout includes bs_form() and bs_inline_formsets() Jinja2 macros, which generate Bootstrap styled Django ModelForms. Usual form generation syntax is:

{% extends 'base_min.htm' %}
{% from 'bs_form.htm' import bs_form with context %}

{% block main %}

{{ bs_form(form=form, action=url('my_url_name'), opts={
    'class': 'form_css_class',
    'title': page_context.get_view_title(),
    'submit_text': 'My button'
}) }}

{% endblock main %}

If your class-based views extends one of the following view classes:

django_jinja_knockout.views.FormWithInlineFormsetsMixin
django_jinja_knockout.views.InlineCreateView
# Next view is suitable both for updating ModelForms with inline formsets
# as well for displaying read-only forms with forms.DisplayModelMetaclass.
django_jinja_knockout.views.InlineCrudView

then, in order to have the form processed as AJAX form, add 'is_ajax': True key to bs_form() / bs_inline_formsets() Jinja2 macro call:

{{ bs_form(form=form, action=url('my_url_name'), opts={
    'class': 'form_css_class',
    'is_ajax': True,
    'title': page_context.get_view_title(),
    'submit_text': 'My button'
}) }}

AJAX response and success URL redirection will be automatically generated. Form errors will be displayed in case there is any. Such form will behave very similarly to usual non-AJAX submitted form with the following advantages:

  1. AJAX response saves HTTP traffic.
  2. Instead of just redirecting to success_url, one may perform custom actions, including displaying BootstrapDialog alerts and confirmations.
  3. ajaxform.js includes Bootstrap progress bar when the form has file inputs. So when large files are uploaded, the progress indicator will be updated, instead of just waiting until the request completes.

At client-side both successful submission of form and form errors are handled by lists of client-side viewmodels. See Client-side viewmodels and AJAX response routing for more detail.

At server-side (Django), the following code of FormWithInlineFormsetsMixin is used to process AJAX-submitted form errors:

def get_form_error_viewmodel(self, form):
    for bound_field in form:
        return {
            'view': 'form_error',
            'class': 'danger',
            'id': bound_field.auto_id,
            'messages': list((escape(message) for message in form.errors['__all__']))
        }
    return None

def get_field_error_viewmodel(self, bound_field):
    return {
        'view': 'form_error',
        'id': bound_field.auto_id,
        'messages': list((escape(message) for message in bound_field.errors))
    }

and the following code returns success viewmodels:

def get_success_viewmodels(self):
    # @note: Do not just remove 'redirect_to', otherwise deleted forms will not be refreshed
    # after successful submission. Use as callback for view: 'alert' or make your own view.
    return vm_list({
        'view': 'redirect_to',
        'url': self.get_success_url()
    })

In instance of FormWithInlineFormsetsMixin, self.forms_vms and self.fields_vms are the instances of vm_list() defined in viewmodels.py. These instances accumulate viewmodels (each one is a simple Python dict with 'view' key) during ModelForm / inline formsets validation.

Actual AJAX ModelForm response success / error viewmodels can be overridden in child class, if needed.

These examples shows how to generate dynamic lists of client-side viewmodels at server-side. viewmodels.py defines methods to alter viewmodels in already existing vm_list() instances.

Displaying read-only “forms”

If form instance was instantiated from ModelForm class with DisplayModelMetaclass metaclass:

from django_jinja_knockout.forms import BootstrapModelForm, DisplayModelMetaclass

from my_app.models import Profile

class ProfileDisplayForm(BootstrapModelForm, metaclass=DisplayModelMetaclass):

    class Meta:
        model = Profile
        exclude = ('age',)
        fields = '__all__'

one may use empty string as submit url value of action='' argument, to display ModelForm instance as read-only Bootstrap table:

{% extends 'base_min.htm' %}
{% from 'bs_inline_formsets.htm' import bs_inline_formsets with context %}

{{
    bs_inline_formsets(related_form=form, formsets=[], action='', opts={
        'class': 'project',
        'title': form.get_title(),
    })
}}

Such “forms” do not contain <input> elements and thus cannot be submitted. Additionally you may inherit from UnchangeableModelMixin:

from django_jinja_knockout.forms import UnchangeableModelMixin

to make sure bound model instances cannot be updated via custom script submission (eg. Greasemonkey).

In case related many to one inline formset ModelForms should be included into read-only “form”, define their ModelForm class with metaclass=DisplayModelMetaclass and specify that class as form kwarg of inlineformset_factory():

from django_jinja_knockout.forms import BootstrapModelForm, DisplayModelMetaclass, set_empty_template

from my_app.models import Profile

class MemberDisplayForm(BootstrapModelForm, metaclass=DisplayModelMetaclass):

    class Meta:
        model = Profile
        fields = '__all__'

MemberDisplayFormSet = inlineformset_factory(
    Project, Member,
    form=MemberDisplayForm, extra=0, min_num=1, max_num=2, can_delete=False
)
MemberDisplayFormSet.set_knockout_template = set_empty_template

DisplayText read-only field widget automatically supports lists as values of models.ManyToManyField fields, rendering these as Bootstrap “list groups”.

Custom rendering of DisplayText form widgets

Sometimes read-only “form” fields contain complex values, such as dates, files and foreign keys. In such case default rendering of DisplayText form widgets, set up by DisplayModelMetaclass, can be customized via manual ModelForm field definition with get_text_method argument callback:

from django_jinja_knockout.forms import BootstrapModelForm, DisplayModelMetaclass, WidgetInstancesMixin
from django_jinja_knockout.widgets import DisplayText
from django.utils.html import format_html
from django.forms.utils import flatatt

from my_app.models import ProjectMember

class ProjectMemberDisplayForm(WidgetInstancesMixin, BootstrapModelForm, metaclass=DisplayModelMetaclass):

    class Meta:

        def get_profile(self, value):
            return format_html(
                '<a {}>{}</a>',
                flatatt({'href': reverse('profile_detail', profile_id=self.instance.pk)}),
                self.instance.user
            )

        model = ProjectMember
        fields = '__all__'
        widgets = {
            'profile': DisplayText(get_text_method=get_profile)
        }

WidgetInstancesMixin is used to make model self.instance available in DisplayText widget callbacks. It enables access to all fields of current model instance in get_text_method callback, in addition to value of the current field.

Note that get_text_method argument will be re-bound from form Meta class to instance of DisplayText widget.

DisplayText field widget supports selective skipping of table rows rendering via setting widget instance property skip_output to True:

# ... skipped imports ...
class ProjectMemberDisplayForm(WidgetInstancesMixin, BootstrapModelForm, metaclass=DisplayModelMetaclass):

    class Meta:

        def get_profile(self, value):
            if self.instance.is_active:
                return format_html(
                    '<a {}>{}</a>',
                    flatatt({'href': reverse('profile_detail', profile_id=self.instance.pk)}),
                    self.instance.user
                )
            else:
                # Do not display inactive user profile link in table form.
                self.skip_output = True
                return None

        model = ProjectMember
        fields = '__all__'
        widgets = {
            'profile': DisplayText(get_text_method=get_profile)
        }

Customizing string representation of scalar values is performed via scalar_display argument of DisplayText widget:

class ProjectMemberDisplayForm(WidgetInstancesMixin, BootstrapModelForm, metaclass=DisplayModelMetaclass):

    class Meta:
        widgets = {
            'state': DisplayText(
                scalar_display={True: 'Allow', False: 'Deny', None: 'Unknown', 1: 'One'}
            ),
        }

Optional scalar_display and get_text_method arguments of DisplayText widget can be used together.

Optional get_text_fn argument of DisplayText widget allows to use non-bound functions to generate text of the widget. It can be used with scalar_display argument, but not with get_text_method argument.

forms vm_renderers module

Since v2.2.0, AJAX rendering of ModelForm and FormWithInlineFormsets are implemented via separate vm_renderers module.

It includes FormViewmodel and InlineViewmodel classes, which are used to render forms submitted with AJAX actions.

It’s possible to override the default rendering of AJAX actions ModelForm and / or FormWithInlineFormsets by extending FormViewmodel / InlineViewmodel classes, then overriding AJAX view’s vm_form / vm_inline attributes:

from django_jinja_knockout.forms.vm_renderers import FormViewmodel

# ... skipped ...

class UserChangeFormViewmodel(FormViewmodel):

    def get_action_local_name(self):
        action_local_name = super().get_action_local_name()
        action_local_name = f'{action_local_name} user {self.instance}'
        return action_local_name

    def get_verbose_name(self):
        verbose_names = model_fields_verbose_names(self.instance)
        verbose_names['full_name'] = 'Full name'
        str_fields = self.get_object_desc(self.instance)
        str_fields['full_name'] = f'{str_fields.pop("first_name", "")} {str_fields.pop("last_name", "")}'.strip()
        if str_fields['full_name'] == '':
            del str_fields['full_name']
        return tpl.print_bs_badges(str_fields, show_keys=1, i18n=verbose_names)


class UserChangeView(ModelFormActionsView):

    form = UserPreferencesForm
    # Overriding vm_form is optional and is not required:
    vm_form = UserChangeFormViewmodel

and / or calling AJAX view’s ioc_vm_form() / ioc_vm_inline() methods, which may be used in custom AJAX form actions:

class ClubEquipmentGrid(EditableClubGrid):

    # ... skipped ...
    form = ClubForm
    form_with_inline_formsets = None

    def get_actions(self):
        actions = super().get_actions()
        actions['built_in']['save_equipment'] = {}
        actions['iconui']['add_equipment'] = {
            'localName': _('Add club equipment'),
            'css': 'iconui-wrench',
        }
        return actions

    # Creates AJAX ClubEquipmentForm bound to particular Club instance.
    def action_add_equipment(self):
        club = self.get_object_for_action()
        if club is None:
            return vm_list({
                'view': 'alert_error',
                'title': 'Error',
                'message': 'Unknown instance of Club'
            })
        equipment_form = ClubEquipmentForm(initial={'club': club.pk})
        # Generate equipment_form viewmodel
        vms = self.ioc_vm_form(
            form=equipment_form
        )(
            form_action='save_equipment'
        )
        return vms

In more complex scenarios, calling ioc_vm_form() / ioc_vm_inline() in AJAX actions handlers allows to specify the source action and / or to override the form action, like this example when one view handles different Django models:

class ClubGrid(KoGridView):
    form = ClubForm
    # ... skipped ...

    # Custom action to show bound MemberForm, not ClubForm.
    def action_edit_owner_form(self):
        club = self.get_object_for_action()
        obj = club.member_set.get(role='owner')
        form = MemberForm(instance=obj)
        # Generates different model instance AJAX form:
        return self.ioc_vm_form(
            form=form,
            source_action=self.current_action_name,
            instance=obj,
        )(
            form_action='save_owner_form',
            action_query={
                # Saved ModelForm object pk.
                'pk_val': obj.pk,
            }
        )

See djk_sample demo project for the complete example.