Models¶
This guide covers how to define and work with Option and Selection models in django-tenant-options.
Defining an Option model¶
Create a concrete Option model by inheriting from AbstractOption:
from django_tenant_options.models import AbstractOption
from django_tenant_options.choices import OptionType
class TaskPriorityOption(AbstractOption):
tenant_model = "yourapp.Tenant"
selection_model = "yourapp.TaskPrioritySelection"
default_options = {
"Critical": {"option_type": OptionType.OPTIONAL},
"High": {"option_type": OptionType.MANDATORY},
"Medium": {"option_type": OptionType.OPTIONAL},
"Low": {}, # Defaults to OptionType.MANDATORY
}
class Meta(AbstractOption.Meta):
verbose_name = "Task Priority Option"
verbose_name_plural = "Task Priority Options"
Required attributes¶
tenant_model– String path to your tenant model (e.g.,"myapp.Tenant"). Can also be set globally via theTENANT_MODELsetting.selection_model– String path to the paired Selection model (e.g.,"myapp.TaskPrioritySelection").
The default_options dictionary¶
Each key is the option name. The value is a dictionary of configuration:
default_options = {
"Option Name": {
"option_type": OptionType.MANDATORY, # or OptionType.OPTIONAL
},
"Another Option": {}, # Empty dict defaults to OptionType.MANDATORY
}
If
option_typeis omitted, it defaults toOptionType.MANDATORY.Only
OptionType.MANDATORYandOptionType.OPTIONALare valid here.OptionType.CUSTOMis for tenant-created options only.
Meta class inheritance¶
Your Meta class must inherit from AbstractOption.Meta:
class Meta(AbstractOption.Meta):
verbose_name = "Task Priority Option"
This ensures database constraints (unique name constraint, tenant check constraint) are properly created. If you’re using a custom base model like auto_prefetch, combine both Meta classes:
class Meta(AbstractOption.Meta, auto_prefetch.Model.Meta):
verbose_name = "Task Priority Option"
Warning
If your Meta class doesn’t inherit from AbstractOption.Meta, you’ll lose database constraints that protect data integrity. Django system checks (W007, W008) will warn you about this.
Adding metadata to options (optional)¶
Options are minimal by default (a name and an option type). To make options
self-documenting, orderable, and categorizable, mix in the optional
OptionMetadataMixin. It adds four fields:
description- aTextFieldfor longer descriptive text.help_text- aCharField(max length 255) for short inline guidance.sort_order- an indexedIntegerField(default0) for human-curated ordering.category- aCharField(max length 100) for grouping options.
The mixin is a plain abstract Django model, so it composes with AbstractOption
regardless of your configured MODEL_CLASS. List the mixin first in the base
class list, and set ordering = ("sort_order", "name") in your Meta so options
are returned in a stable, curated order:
from django_tenant_options.mixins import OptionMetadataMixin
from django_tenant_options.models import AbstractOption
class MyOption(OptionMetadataMixin, AbstractOption):
tenant_model = "myapp.Tenant"
selection_model = "myapp.MySelection"
class Meta(AbstractOption.Meta):
ordering = ("sort_order", "name")
Filter by category with the standard ORM:
MyOption.objects.filter(category="colors")
Because the mixin only adds fields, it works alongside soft delete and every
existing OptionQuerySet method (active(), deleted(), custom_options(),
and so on). After adding the mixin to a model, create and apply a migration with
python manage.py makemigrations and python manage.py migrate.
Defining a Selection model¶
Create a concrete Selection model by inheriting from AbstractSelection:
from django_tenant_options.models import AbstractSelection
class TaskPrioritySelection(AbstractSelection):
tenant_model = "yourapp.Tenant"
option_model = "yourapp.TaskPriorityOption"
class Meta(AbstractSelection.Meta):
verbose_name = "Task Priority Selection"
verbose_name_plural = "Task Priority Selections"
Required attributes¶
tenant_model– String path to your tenant model. Can also be set globally.option_model– String path to the paired Option model.
Meta class inheritance¶
Same rule as Options – inherit from AbstractSelection.Meta:
class Meta(AbstractSelection.Meta):
verbose_name = "Task Priority Selection"
Missing this inheritance triggers system check warnings W009, W010, and W011.
Using options in your business models¶
Add ForeignKey fields from your business models to the Option model:
from django.db import models
class Task(models.Model):
title = models.CharField(max_length=100)
priority = models.ForeignKey(
"yourapp.TaskPriorityOption",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="tasks",
)
status = models.ForeignKey(
"yourapp.TaskStatusOption",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="tasks",
)
Tip
Using on_delete=models.SET_NULL with null=True is recommended. Since options use soft deletes, this prevents cascading deletions of your business data if an option is hard-deleted.
Model relationships¶
The following diagram shows how the models relate to each other:

Managers and querysets¶
The package automatically adds an objects manager and an unscoped manager to every Option and Selection model.
OptionManager methods¶
Create options programmatically:
# Create a custom option for a specific tenant
option = TaskPriorityOption.objects.create_for_tenant(
tenant=my_tenant,
name="Urgent",
)
# Create a mandatory default option
option = TaskPriorityOption.objects.create_mandatory(name="High")
# Create an optional default option
option = TaskPriorityOption.objects.create_optional(name="Medium")
OptionQuerySet methods¶
Query options with tenant-aware filtering:
# All options available to a tenant (mandatory + optional + tenant's custom)
TaskPriorityOption.objects.options_for_tenant(tenant)
# Only options the tenant has selected
TaskPriorityOption.objects.selected_options_for_tenant(tenant)
# Only active (non-deleted) options
TaskPriorityOption.objects.active()
# Only deleted options
TaskPriorityOption.objects.deleted()
# Only custom options (created by tenants)
TaskPriorityOption.objects.custom_options()
SelectionManager methods¶
The Selection model’s manager provides the same tenant-aware methods:
# Options available to a tenant (through the selection model)
TaskPrioritySelection.objects.options_for_tenant(tenant)
# Currently selected options for a tenant
TaskPrioritySelection.objects.selected_options_for_tenant(tenant)
The unscoped manager¶
Every model also has an unscoped manager that includes soft-deleted records:
# Get all options, including deleted ones
TaskPriorityOption.unscoped.all()
Use this for administrative views or data cleanup tasks. The default objects manager excludes soft-deleted records.
Soft delete operations¶
# Soft delete (sets the `deleted` timestamp)
option.delete()
# Hard delete (actually removes from database)
option.delete(override=True)
# Restore a soft-deleted option
option.undelete()
# Bulk restore
TaskPriorityOption.unscoped.filter(deleted__isnull=False).undelete()
Custom managers and querysets¶
If you need custom query methods, subclass the package’s managers and querysets:
from django_tenant_options.models import OptionQuerySet, OptionManager
class CustomOptionQuerySet(OptionQuerySet):
def high_priority(self):
return self.active().filter(name__in=["Critical", "High"])
class CustomOptionManager(OptionManager):
def get_queryset(self):
return CustomOptionQuerySet(self.model, using=self._db)
class TaskPriorityOption(AbstractOption):
objects = CustomOptionManager()
# ... rest of model definition
Warning
Custom managers must inherit from OptionManager (or SelectionManager for Selection models). Custom querysets must inherit from OptionQuerySet (or SelectionQuerySet). If they don’t, the package’s filtering methods won’t be available, and Django system checks will flag the issue.
Further reading¶
Concepts – Why the two-model pattern exists
Forms Guide – Building forms that work with your models
Configuration Reference – Customizing model behavior through settings