Forms are part of almost every create and edit screen in Horilla. In a typical Django project, this can mean writing the same form configuration again and again — setting up widgets, handling ForeignKey fields, checking permissions, and taking care of fields that should not be exposed to users.

Horilla CRM’s HorillaModelForm is designed to avoid that repetition.

It extends Django’s ModelForm through HorillaFormMixin and provides the common behavior needed across Horilla forms. Instead of configuring the same things in every form, you can start with HorillaModelForm and let the framework handle the standard work.

What Does HorillaModelForm Handle?

A simple HorillaModelForm already takes care of several things that would otherwise have to be implemented separately.

It automatically:

  • Excludes common audit fields such as company, is_active, created_at, updated_at, created_by, updated_by, and additional_info
  • Applies Horilla’s Tailwind-based widgets and placeholders
  • Uses paginated Select2 widgets for ForeignKey and ManyToMany fields
  • Re-validates submitted ForeignKey and ManyToMany values against a fresh queryset
  • Applies field-level permissions such as hidden, read-only, and mandatory fields
  • Handles differences between normal create and duplicate forms
  • Detects phone, date, datetime, time, file, and image fields and applies the appropriate widgets
  • Supports fieldsets for grouped form layouts
  • Supports condition fields used by rule-builder style forms

The class is located at:

horilla/contrib/generics/forms/single_step.py

It is also exported from horilla.contrib.generics.forms, alongside HorillaMultiStepForm.

Starting With a Simple Form

For many forms, very little code is needed.

For example:

from horilla.contrib.generics.forms import HorillaModelForm

class ScoringRuleForm(HorillaModelForm):
    class Meta:
        model = ScoringRule
        fields = "__all__"

There is no need to manually remove the audit fields or configure the basic widgets. HorillaModelForm handles those details automatically.

ForeignKey and ManyToMany fields are also picked up and configured with the appropriate Select2-based widgets.

This is one of the main advantages of using the base form — the form stays focused on the model instead of repeating framework-level configuration.

Keeping an Audit Field on the Form

Horilla normally removes its common audit fields from forms. In some situations, however, a particular field may need to remain available.

For example, a form might need to allow a privileged user to select the company manually.

This can be done with keep_on_form:

class ScoringRuleForm(HorillaModelForm):
    class Meta:
        model = ScoringRule
        fields = "__all__"
        keep_on_form = ("company",)

Here, company is normally excluded, but keep_on_form tells the form to keep it.

This is useful when a form has a special requirement without changing the default behavior for every other form.

Organizing Fields With Fieldsets

A long form can become difficult to read when every field is displayed in one continuous list.

fieldsets can be used to group related fields together:

class OpportunitySingleForm(HorillaModelForm):
    fieldsets = (
        ("Basic Info", {"fields": ["name", "account", "stage"]}),
        (
            "Financials",
            {
                "fields": ["amount", "currency", "close_date"],
                "description": "Deal value and expected close",
            },
        ),
    )


    class Meta:
        model = Opportunity
        fields = "__all__"

The form can then be rendered by the template as separate sections.

For example, the basic opportunity information can be shown together, while financial information appears under its own section.

This keeps the form definition readable while also giving the UI a structured layout.

Using Condition Fields in Rule Builder Forms

Some forms in Horilla need more than the normal model fields.

A good example is a scoring or rule-builder form where users can add conditions such as:

If field X is equal to Y

The form can provide a condition_model and use the condition-field support built into HorillaModelForm.

class ScoringCriterionForm(HorillaModelForm):
    def __init__(self, *args, **kwargs):
        kwargs["condition_model"] = ScoringCondition
        super().__init__(*args, **kwargs)


    def clean(self):
        cleaned_data = super().clean()


        condition_rows = self._extract_condition_rows()


        # Validate or attach condition_rows as needed


        return cleaned_data


    class Meta:
        model = ScoringCriterion
        fields = "__all__"

The base form already calls condition_fields_module.clean_condition_fields() from its clean() method.

That means the common condition-row parsing is handled by the framework. A custom form only needs to add any validation that is specific to its own business logic.

Combining HorillaModelForm With Ownership Filtering

In CRM forms, users should not always be able to select every record in the system.

For example, an account form may need to show only records that the current user is allowed to access.

Horilla provides OwnerQuerysetMixin for this kind of filtering:

class AccountSingleForm(OwnerQuerysetMixin, HorillaModelForm):
    class Meta:
        model = Account
        fields = "__all__"

The order of the base classes is important.

OwnerQuerysetMixin needs to run before HorillaModelForm builds its field querysets. This allows the ownership-based filtering to be applied to the choices shown by the form.

Why Does clean() Re-Validate ForeignKey and ManyToMany Fields?

One of the important security details in HorillaModelForm happens during validation.

It is not enough to check which records a user could select when the form was first displayed.

Imagine a user opens a form and sees a list of accounts they are allowed to access. Later, their permissions change. The form that is already open still contains the old account ID.

A malicious request could also modify the submitted Select2 value manually.

For this reason, HorillaModelForm.clean() does not simply trust the submitted primary keys.

For ForeignKey and ManyToMany fields, it creates a fresh queryset using _get_fresh_queryset() and checks whether the submitted values are still valid.

If the record is no longer accessible, validation fails instead of allowing an unauthorized object to be saved.

This extra validation is particularly important for forms where the available records depend on permissions, ownership, or other access rules.

Custom Fields Without Changing HorillaModelForm

Another useful part of the design is that HorillaModelForm does not need to know how custom fields work.

The custom-field functionality is added through Horilla’s extension system.

The process works roughly like this:

  1. The model is registered for the custom_fields_models feature using register_model_for_feature.
  2. custom_fields/extensions.py finds HorillaModelForm subclasses whose Meta.model matches the registered model.
  3. A FormExtension is attached to the form.
  4. After the form’s __init__ finishes, setup_form_extension_fields calls apply_single_form_custom_fields.
  5. The custom cf_* fields are added to form.fields, and their stored values are used to populate form.initial.
  6. When the form is saved, the extension wraps the save process and uses persist_custom_fields_for_form to store the submitted custom-field values.

There is only a small point where HorillaModelForm needs to be aware of this system:

getattr(field, “is_custom_field”, False)

This allows the form’s normal widget and hidden-field handling to skip synthetic custom fields instead of modifying them.

So a form does not need special custom-field code just because the model supports custom fields. Once the model is registered with the feature system, the extension takes care of the rest.

Why Use HorillaModelForm?

The main benefit of HorillaModelForm is not just that it saves a few lines of code.

It gives Horilla forms a common behavior.

Instead of each developer deciding how to style a field, configure Select2, exclude audit fields, validate related objects, or handle permissions, those responsibilities are handled by the base form and the extension system.

At the same time, the form can still be customized when needed using features such as keep_on_form, fieldsets, condition_model, and OwnerQuerysetMixin.

HorillaModelForm provides a common foundation for create and edit forms throughout Horilla CRM.

For a basic model form, a Meta.model and fields = “__all__” are often enough to get the standard widgets, audit-field handling, and related-field validation automatically.

When a form needs something more specific, features such as keep_on_form, fieldsets, condition fields, and ownership filtering can be added without replacing the base form.

The bigger idea is that these features are built through composition and extensions rather than being repeated in every individual form. That keeps Horilla’s forms smaller, consistent, and easier to maintain as the application grows.

Share this article