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 the TENANT_MODEL setting.

  • 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_type is omitted, it defaults to OptionType.MANDATORY.

  • Only OptionType.MANDATORY and OptionType.OPTIONAL are valid here. OptionType.CUSTOM is 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 - a TextField for longer descriptive text.

  • help_text - a CharField (max length 255) for short inline guidance.

  • sort_order - an indexed IntegerField (default 0) for human-curated ordering.

  • category - a CharField (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:

Model relationships diagram

Model relationships detail

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