Forms¶
django-tenant-options provides form mixins and fields that handle tenant-aware option filtering. This guide covers each one in detail.
Every form mixin in this package requires a tenant argument passed from your view. Forgetting this raises NoTenantProvidedFromViewError.
UserFacingFormMixin¶
Use this mixin for forms that end users interact with – task creation forms, order forms, any form where a user selects from available options.
from django import forms
from django_tenant_options.forms import UserFacingFormMixin
class TaskForm(UserFacingFormMixin, forms.ModelForm):
class Meta:
model = Task
fields = ["title", "priority", "status"]
What it does automatically¶
When initialized with a tenant, the mixin:
Finds ForeignKey fields pointing to any
AbstractOptionsubclassFilters their querysets to show only options the tenant has selected (via
selected_options_for_tenant())Hides the tenant field if present (sets it to
HiddenInput)Removes the
associated_tenantsfield if presentHandles deleted selections – if an existing record references a deleted option, the behavior depends on the
DISABLE_FIELD_FOR_DELETED_SELECTIONsetting
Deleted selection behavior¶
When a tenant deselects an option but existing records still reference it:
DISABLE_FIELD_FOR_DELETED_SELECTION = False(default): The user must select a new option when editing the record. The deleted option won’t appear as a choice.DISABLE_FIELD_FOR_DELETED_SELECTION = True: The deleted option appears in the dropdown but is disabled (read-only). The user sees what was previously selected but can’t change it.
In both cases, deleted options never appear in forms for new records.
View integration¶
Pass tenant from your view:
# Function-based view
def task_create(request):
if request.method == "POST":
form = TaskForm(request.POST, tenant=request.user.tenant)
if form.is_valid():
form.save()
return redirect("task_list")
else:
form = TaskForm(tenant=request.user.tenant)
return render(request, "tasks/form.html", {"form": form})
# Class-based view
class TaskCreateView(LoginRequiredMixin, CreateView):
model = Task
form_class = TaskForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["tenant"] = self.request.user.tenant
return kwargs
OptionCreateFormMixin¶
Use this mixin for forms that let tenant admins create new custom options.
from django import forms
from django_tenant_options.forms import OptionCreateFormMixin
class TaskPriorityCreateForm(OptionCreateFormMixin, forms.ModelForm):
class Meta:
model = TaskPriorityOption
fields = ["name", "option_type", "tenant", "deleted"]
What it does automatically¶
Validates the tenant and hides the tenant field (
HiddenInput)Sets
option_typetoOptionType.CUSTOMand hides it – tenant-created options are always customHides the
deletedfield and initializes it toNoneRemoves
associated_tenantsif presentEnforces
option_type = CUSTOMinclean()– even if someone manipulates the hidden field
Specifying fields¶
You can use fields = "__all__" or list specific fields. Either way, the mixin will hide fields that tenants shouldn’t see (tenant, option_type, deleted):
# Explicit fields -- recommended for clarity
class TaskPriorityCreateForm(OptionCreateFormMixin, forms.ModelForm):
class Meta:
model = TaskPriorityOption
fields = ["name", "option_type", "tenant", "deleted"]
# Implicit -- the mixin hides what it needs to
class TaskPriorityCreateForm(OptionCreateFormMixin, forms.ModelForm):
class Meta:
model = TaskPriorityOption
fields = "__all__"
OptionUpdateFormMixin¶
Extends OptionCreateFormMixin with a delete checkbox for soft-deleting options.
from django import forms
from django_tenant_options.forms import OptionUpdateFormMixin
class TaskPriorityUpdateForm(OptionUpdateFormMixin, forms.ModelForm):
class Meta:
model = TaskPriorityOption
fields = "__all__"
What it adds¶
On top of everything OptionCreateFormMixin does:
Adds a
deleteBooleanField (not required, defaults to False)In
clean(), ifdeleteis checked, setscleaned_data["deleted"]to the current timestamp
The resulting form lets tenant admins rename a custom option or soft-delete it in a single form.
View integration¶
def priority_update(request, option_id):
option = get_object_or_404(TaskPriorityOption, id=option_id)
if request.method == "POST":
form = TaskPriorityUpdateForm(
request.POST,
instance=option,
tenant=request.user.tenant,
)
if form.is_valid():
form.save()
return redirect("priority_list")
else:
form = TaskPriorityUpdateForm(
instance=option,
tenant=request.user.tenant,
)
return render(request, "priorities/form.html", {"form": form})
SelectionsForm¶
Use this form to let tenant admins manage which options are enabled for their tenant.
from django_tenant_options.forms import SelectionsForm
class TaskPrioritySelectionForm(SelectionsForm):
class Meta:
model = TaskPrioritySelection
What it does¶
Creates a
selectionsfield – aModelMultipleChoiceFieldshowing all options available to the tenantPre-selects currently enabled options as initial values
In
clean(), automatically includes all mandatory options (even if the tenant tries to deselect them)Identifies removed selections – options that were previously selected but aren’t in the new submission
In
save(), uses an atomic transaction to:Soft-delete removed selections (sets
deletedtimestamp)Create or restore selections for newly chosen options
Customizing the widget¶
You can adjust the selection widget in your form’s __init__:
class TaskPrioritySelectionForm(SelectionsForm):
class Meta:
model = TaskPrioritySelection
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["selections"].widget.attrs["size"] = "10"
View integration¶
def priority_selections(request):
if request.method == "POST":
form = TaskPrioritySelectionForm(
request.POST,
tenant=request.user.tenant,
)
if form.is_valid():
form.save()
return redirect("priority_list")
else:
form = TaskPrioritySelectionForm(tenant=request.user.tenant)
return render(request, "priorities/selections.html", {"form": form})
TenantFormBaseMixin¶
This is the base mixin that OptionCreateFormMixin and SelectionsForm build on. You generally don’t use it directly, but it’s useful to understand what it provides:
Pops
tenantfrom kwargs and validates it’s notNoneHides the
tenantfield if presentSets
option_typetoCUSTOMif the field existsRemoves
associated_tenantsif presentOverrides
clean()to ensuretenantis always correct in cleaned data
OptionsModelMultipleChoiceField¶
A custom ModelMultipleChoiceField used by SelectionsForm that displays option type labels alongside option names.
The default label_from_instance shows: "Option Name (Mandatory)", "Option Name (Optional)", or "Option Name (Custom)".
Custom display¶
To customize how options appear in the selection widget, subclass the field:
from django_tenant_options.form_fields import OptionsModelMultipleChoiceField
class CustomOptionsField(OptionsModelMultipleChoiceField):
def label_from_instance(self, obj):
return f"{obj.name} - {obj.get_option_type_display()}"
Then configure it globally:
DJANGO_TENANT_OPTIONS = {
"DEFAULT_MULTIPLE_CHOICE_FIELD": "yourapp.forms.CustomOptionsField",
}
Grouped option fields¶
GroupedOptionsModelMultipleChoiceField is a drop-in replacement for OptionsModelMultipleChoiceField that renders selectable options inside HTML <optgroup> groups. This keeps long option catalogs navigable. It subclasses OptionsModelMultipleChoiceField, so option labels still show their type suffix (for example High (mandatory)) inside each group.
By default it groups by option_type, ordering the groups Mandatory -> Optional -> Custom. You can group by any attribute on your option model by passing group_by; objects whose attribute is missing or empty fall into an Uncategorized group, and the remaining groups are sorted alphabetically.
Use it project-wide¶
Point the package setting at the grouped field’s dotted path:
DJANGO_TENANT_OPTIONS = {
"DEFAULT_MULTIPLE_CHOICE_FIELD": "django_tenant_options.form_fields.GroupedOptionsModelMultipleChoiceField",
}
Every SelectionsForm then renders grouped selections automatically.
Use it on a single form¶
Set the multiple_choice_field_class class attribute on a SelectionsForm subclass. Forms that do not set it are unaffected:
from django_tenant_options.forms import SelectionsForm
from django_tenant_options.form_fields import GroupedOptionsModelMultipleChoiceField
class GroupedSelectionsForm(SelectionsForm):
multiple_choice_field_class = GroupedOptionsModelMultipleChoiceField
class Meta:
model = TaskPrioritySelection
Grouping by a custom attribute¶
If your option model exposes an attribute such as category (for example via a metadata mixin), subclass the field with group_by defaulted, then reference it from your form:
from django_tenant_options.form_fields import GroupedOptionsModelMultipleChoiceField
class CategoryGroupedField(GroupedOptionsModelMultipleChoiceField):
def __init__(self, *args, **kwargs):
kwargs.setdefault("group_by", "category")
super().__init__(*args, **kwargs)
class CategorySelectionsForm(SelectionsForm):
multiple_choice_field_class = CategoryGroupedField
class Meta:
model = TaskPrioritySelection
If the option model has no category attribute, every option simply appears under Uncategorized - the field degrades gracefully rather than raising.
Common mistakes¶
Forgetting to pass tenant¶
Every form that uses these mixins requires tenant= in its constructor call:
# This will raise NoTenantProvidedFromViewError
form = TaskForm(request.POST)
# Correct
form = TaskForm(request.POST, tenant=request.user.tenant)
Using the wrong mixin¶
End-user forms (selecting an option for a record):
UserFacingFormMixinCreating custom options (tenant admin):
OptionCreateFormMixinUpdating/deleting custom options (tenant admin):
OptionUpdateFormMixinManaging which options are enabled (tenant admin):
SelectionsForm
Combining with crispy-forms¶
The mixins work with django-crispy-forms. Initialize the crispy helper after calling super().__init__():
class TaskPriorityCreateForm(OptionCreateFormMixin, ModelForm):
class Meta:
model = TaskPriorityOption
fields = "__all__"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.layout = Layout(
Fieldset("Create Priority", *self.fields.keys()),
Submit("submit", "Save"),
)
Further reading¶
Views and Templates – Wiring forms into views and templates
Models Guide – The models behind the forms
Configuration Reference – Form-related settings