Views and Templates¶
Patterns for passing tenants to forms, rendering options in templates, and registering models in the Django admin.
Passing tenant to forms¶
Every form that uses a django-tenant-options mixin requires a tenant argument. How you determine the current tenant depends on your application architecture.
Function-based views¶
from django.shortcuts import get_object_or_404, redirect, render
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})
def task_update(request, task_id):
task = get_object_or_404(Task, id=task_id)
if request.method == "POST":
form = TaskForm(request.POST, instance=task, tenant=request.user.tenant)
if form.is_valid():
form.save()
return redirect("task_list")
else:
form = TaskForm(instance=task, tenant=request.user.tenant)
return render(request, "tasks/form.html", {"form": form})
Class-based views¶
Override get_form_kwargs to inject the tenant:
from django.views.generic import CreateView, UpdateView
from django.contrib.auth.mixins import LoginRequiredMixin
class TaskCreateView(LoginRequiredMixin, CreateView):
model = Task
form_class = TaskForm
template_name = "tasks/form.html"
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["tenant"] = self.request.user.tenant
return kwargs
class TaskUpdateView(LoginRequiredMixin, UpdateView):
model = Task
form_class = TaskForm
template_name = "tasks/form.html"
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["tenant"] = self.request.user.tenant
return kwargs
Determining the current tenant¶
Your approach depends on your tenant architecture:
# Direct ForeignKey on User
tenant = request.user.tenant
# Through a profile model
tenant = request.user.profile.organization
# From middleware that sets it on the request
tenant = request.tenant
# From the URL (e.g., subdomain-based)
tenant = Tenant.objects.get(subdomain=request.subdomain)
The package doesn’t impose a particular tenant resolution strategy. As long as you pass a valid tenant model instance to the form, it works.
Template patterns¶
Displaying option types¶
Options have an option_type field with values "dm" (Mandatory), "do" (Optional), or "cu" (Custom). Use get_option_type_display for human-readable labels:
<ul>
{% for priority in priorities %}
<li>
{{ priority.name }}
<span class="badge">{{ priority.get_option_type_display }}</span>
</li>
{% endfor %}
</ul>
Color-coding by type¶
Apply CSS classes based on option type for visual distinction:
{% for priority in priorities %}
<li>
{{ priority.name }}
{% if priority.option_type == "dm" %}
<span class="badge badge-primary">Mandatory</span>
{% elif priority.option_type == "do" %}
<span class="badge badge-secondary">Optional</span>
{% else %}
<span class="badge badge-info">Custom</span>
{% endif %}
</li>
{% endfor %}
Accessibility note: The visible text (“Mandatory”, “Optional”, “Custom”) is what conveys meaning here - color is supplementary, satisfying WCAG 1.4.1. Do not drop the text and rely on color alone. Also verify badge contrast (WCAG 1.4.3, 4.5:1 minimum): Bootstrap’s
badge-infowith white text fails contrast in common themes - prefer a class whose color/background pair you have measured (e.g.badge-dark).
Listing options for a tenant¶
In your view, use the queryset methods to get the right set of options:
def priority_list(request):
# All options available to this tenant (mandatory + optional + custom)
options = TaskPriorityOption.objects.options_for_tenant(request.user.tenant)
# Only selected options (what users will see in forms)
selections = TaskPrioritySelection.objects.selected_options_for_tenant(
tenant=request.user.tenant
)
return render(request, "priorities/list.html", {
"options": options,
"selections": selections,
})
Simple form template¶
<form method="post" novalidate>
{% csrf_token %}
{# Error summary - announced on submit because of role="alert". #}
{% if form.errors %}
<div role="alert">
<h2>Please correct the following:</h2>
<ul>
{% for field in form %}
{% for error in field.errors %}
<li><a href="#id_{{ field.html_name }}">{{ field.label }}: {{ error }}</a></li>
{% endfor %}
{% endfor %}
{% for error in form.non_field_errors %}<li>{{ error }}</li>{% endfor %}
</ul>
</div>
{% endif %}
{{ form.as_p }}
<button type="submit">Save</button>
</form>
Accessibility note:
{{ form.as_p }}does not attacharia-invalidoraria-describedbyto individual errored inputs. Pick one of these approaches (they are alternatives, not complements):
Mix
django_tenant_options.forms.AccessibleFormMixininto your form (it setsaria-invalidand pointsaria-describedbyatid_{{ field.html_name }}_errors) and hand-render an error container with that exact id, e.g.<ul id="id_{{ field.html_name }}_errors" role="alert">. The mixin assumes you render the error markup yourself, so do not combine it with a renderer that emits its own error ids.Or use
django-crispy-forms, whose template packs wire error associations using their own element ids. In that case do not also addAccessibleFormMixin, or itsaria-describedbywill point at a container crispy does not render.
Admin integration¶
The package provides base admin classes for registering Option and Selection models in the Django admin.
Admin classes¶
from django.contrib import admin
from django_tenant_options.admin import BaseOptionsAdmin, BaseSelectionsAdmin
from .models import TaskPriorityOption, TaskPrioritySelection
@admin.register(TaskPriorityOption)
class TaskPriorityOptionAdmin(BaseOptionsAdmin):
list_display = ["name", "option_type", "tenant", "deleted"]
list_filter = ["option_type", "deleted"]
search_fields = ["name"]
@admin.register(TaskPrioritySelection)
class TaskPrioritySelectionAdmin(BaseSelectionsAdmin):
list_display = ["option", "tenant", "deleted"]
list_filter = ["deleted"]
Admin mixins¶
If you need to combine with other admin base classes, use the mixins instead:
from django_tenant_options.admin import BaseOptionsAdminMixin, SelectionsAdminMixin
class TaskPriorityOptionAdmin(BaseOptionsAdminMixin, SomeOtherAdminBase):
# ...
pass
Tenant-aware admin forms¶
If you use a UserFacingFormMixin form in the admin, override get_form to pass the tenant:
@admin.register(Task)
class TaskAdmin(admin.ModelAdmin):
form = TaskForm
def get_form(self, request, obj=None, **kwargs):
Form = super().get_form(request, obj, **kwargs)
class TenantForm(Form):
def __init__(self, *args, **inner_kwargs):
inner_kwargs["tenant"] = request.user.tenant
super().__init__(*args, **inner_kwargs)
return TenantForm
URL patterns¶
A recommended URL structure for option management:
from django.urls import path
urlpatterns = [
# Tasks
path("tasks/", views.task_list, name="task_list"),
path("tasks/create/", views.task_create, name="task_create"),
path("tasks/<int:task_id>/edit/", views.task_update, name="task_update"),
# Priority options (tenant admin)
path("priorities/", views.priority_list, name="priority_list"),
path("priorities/create/", views.priority_create, name="priority_create"),
path("priorities/<int:pk>/edit/", views.priority_update, name="priority_update"),
path("priorities/selections/", views.priority_selections, name="priority_selections"),
]
Further reading¶
Forms Guide – Detailed coverage of each form mixin
Models Guide – Manager and queryset methods for views
Tutorial – Building a complete application end to end