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-info with 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 attach aria-invalid or aria-describedby to individual errored inputs. Pick one of these approaches (they are alternatives, not complements):

  • Mix django_tenant_options.forms.AccessibleFormMixin into your form (it sets aria-invalid and points aria-describedby at id_{{ 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 add AccessibleFormMixin, or its aria-describedby will 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