Customization

This guide covers advanced customization: custom querysets and managers, custom form fields, base class configuration, database triggers, and performance tips.

Custom querysets and managers

Extend the package’s querysets and managers to add domain-specific query methods.

Custom queryset

from django_tenant_options.models import OptionQuerySet, OptionManager


class PriorityOptionQuerySet(OptionQuerySet):
    def critical_only(self):
        return self.active().filter(name__icontains="critical")

    def active_priority(self):
        return self.active().filter(priority_weight__gt=0)

Custom manager

class PriorityOptionManager(OptionManager):
    def get_queryset(self):
        return PriorityOptionQuerySet(self.model, using=self._db)

Using them in your model

class TaskPriorityOption(AbstractOption):
    objects = PriorityOptionManager()
    priority_weight = models.IntegerField(default=0)

    tenant_model = "yourapp.Tenant"
    selection_model = "yourapp.TaskPrioritySelection"
    # ...

Important

Custom managers must inherit from OptionManager (for Option models) or SelectionManager (for Selection models). Custom querysets must inherit from OptionQuerySet or SelectionQuerySet. System checks will flag incorrect inheritance.

Custom form fields

Customize how options appear in SelectionsForm by subclassing OptionsModelMultipleChoiceField:

from django_tenant_options.form_fields import OptionsModelMultipleChoiceField


class CustomOptionsField(OptionsModelMultipleChoiceField):
    def label_from_instance(self, obj):
        label = obj.name
        if obj.option_type == "dm":
            label += " [Required]"
        elif obj.option_type == "do":
            label += " [Available]"
        else:
            label += " [Custom]"
        return label

Configure globally in settings:

DJANGO_TENANT_OPTIONS = {
    "DEFAULT_MULTIPLE_CHOICE_FIELD": "yourapp.forms.CustomOptionsField",
}

Or set it per-form by overriding multiple_choice_field_class before the SelectionsForm initializes its fields.

Base class configuration

The package allows you to replace the default Django base classes used internally. This is useful when integrating with packages like django-auto-prefetch that provide optimized base classes.

Configuration through settings

import auto_prefetch

DJANGO_TENANT_OPTIONS = {
    "MODEL_CLASS": auto_prefetch.Model,            # Default: django.db.models.Model
    "MANAGER_CLASS": "auto_prefetch.Manager",      # Default: django.db.models.Manager
    "QUERYSET_CLASS": "auto_prefetch.QuerySet",    # Default: django.db.models.QuerySet
    "FOREIGNKEY_CLASS": auto_prefetch.ForeignKey,   # Default: django.db.models.ForeignKey
    "ONETOONEFIELD_CLASS": "auto_prefetch.OneToOneField",  # Default: django.db.models.OneToOneField
}

Values can be actual class references or dotted string paths.

Programmatic configuration

For more granular control:

from django_tenant_options.app_settings import model_config

model_config.model_class = "auto_prefetch.Model"
model_config.manager_class = auto_prefetch.Manager
model_config.queryset_class = auto_prefetch.QuerySet
model_config.foreignkey_class = auto_prefetch.ForeignKey
model_config.onetoonefield_class = "auto_prefetch.OneToOneField"

Meta class with custom base models

When using a custom base model class that has its own Meta, combine both in your model’s Meta:

class TaskPriorityOption(AbstractOption):
    class Meta(AbstractOption.Meta, auto_prefetch.Model.Meta):
        verbose_name = "Task Priority Option"

Database triggers

Database triggers provide an additional layer of referential integrity enforcement at the database level.

What they protect

Triggers ensure that a Selection’s tenant always matches the Option’s tenant (for custom options) or that the Option is a default option. This prevents mismatches that could occur from:

  • Direct SQL operations bypassing Django’s ORM

  • Race conditions in concurrent operations

  • Bugs in custom code that skip model validation

Supported databases

Database

Support

PostgreSQL

Full

MySQL

Full

SQLite

Full

Oracle

Full

Custom database backends

If you use a custom backend (e.g., PostGIS) built on a supported database, override the vendor detection:

DJANGO_TENANT_OPTIONS = {
    "DB_VENDOR_OVERRIDE": "postgresql",
}

Or pass it per-command:

python manage.py maketriggers --db-vendor-override postgresql

Regenerating triggers

If you change your model structure, regenerate triggers:

python manage.py maketriggers --force
python manage.py migrate

Performance tips

Cache frequently accessed options

Options don’t change often. Cache them to avoid repeated database queries:

from django.core.cache import cache


def get_tenant_options(tenant):
    cache_key = f"tenant_{tenant.id}_options"
    options = cache.get(cache_key)
    if options is None:
        options = list(TaskPriorityOption.objects.selected_options_for_tenant(tenant))
        cache.set(cache_key, options, timeout=3600)
    return options

Remember to invalidate the cache when selections change (e.g., in a post-save signal or after SelectionsForm.save()).

Consider auto_prefetch

If your application has many ForeignKey lookups, the django-auto-prefetch package can automatically optimize queries. Configure it using the base class settings described above.

Further reading