Skip to content

Commit 9d1e862

Browse files
committed
Add settings to allow changing the Model/Manager/QuerySet/OneToOneField/ForeignKey
- Useful in case one is using django-auto-prefetch, for instance
1 parent 2e88bf1 commit 9d1e862

File tree

2 files changed

+118
-26
lines changed

2 files changed

+118
-26
lines changed

src/django_tenant_options/app_settings.py

Lines changed: 99 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,31 +10,121 @@
1010
}
1111
1212
"""
13-
import logging
1413

15-
from django.core.exceptions import ImproperlyConfigured
14+
import logging
1615

16+
from django_tenant_options.form_fields import ( # noqa: F401
17+
OptionsModelMultipleChoiceField,
18+
)
1719

18-
logger = logging.getLogger(__name__)
1920

2021
try:
22+
import_error = None
2123
from django.conf import settings
24+
from django.core.exceptions import ImproperlyConfigured
25+
from django.db import models
2226
except ImproperlyConfigured as e:
23-
logger.error("Settings could not be imported: %s", e)
27+
import_error = "Settings could not be imported: %s", e
2428
settings = None # pylint: disable=C0103
2529
except ImportError as e:
26-
logger.error("Django could not be imported. Settings cannot be loaded: %s", e)
30+
import_error = "Django could not be imported. Settings cannot be loaded: %s", e
2731
settings = None # pylint: disable=C0103
28-
from django.db import models
2932

30-
from django_tenant_options.form_fields import ( # noqa: F401
31-
OptionsModelMultipleChoiceField,
32-
)
33+
logger = logging.getLogger(__name__)
34+
35+
if import_error:
36+
logger.error(import_error)
37+
38+
39+
class ModelClassConfig:
40+
"""Configuration class for model base classes."""
41+
42+
def __init__(self):
43+
self._model_class = models.Model
44+
self._manager_class = models.Manager
45+
self._queryset_class = models.QuerySet
46+
self._foreignkey_class = models.ForeignKey
47+
self._onetoonefield_class = models.OneToOneField
48+
49+
@property
50+
def model_class(self):
51+
"""The base class to use for all django-tenant-options models."""
52+
return self._model_class
53+
54+
@model_class.setter
55+
def model_class(self, cls):
56+
"""Set the base class to use for all django-tenant-options models."""
57+
self._model_class = cls
3358

59+
@property
60+
def manager_class(self):
61+
"""The base class to use for all django-tenant-options model managers."""
62+
return self._manager_class
63+
64+
@manager_class.setter
65+
def manager_class(self, cls):
66+
"""Set the base class to use for all django-tenant-options model managers."""
67+
self._manager_class = cls
68+
69+
@property
70+
def queryset_class(self):
71+
"""The base class to use for all django-tenant-options model querysets."""
72+
return self._queryset_class
73+
74+
@queryset_class.setter
75+
def queryset_class(self, cls):
76+
"""Set the base class to use for all django-tenant-options model querysets."""
77+
self._queryset_class = cls
78+
79+
@property
80+
def foreignkey_class(self):
81+
"""The base class to use for all django-tenant-options foreign keys."""
82+
return self._foreignkey_class
83+
84+
@foreignkey_class.setter
85+
def foreignkey_class(self, cls):
86+
"""Set the base class to use for all django-tenant-options foreign keys."""
87+
self._foreignkey_class = cls
88+
89+
@property
90+
def onetoonefield_class(self):
91+
"""The base class to use for all django-tenant-options one-to-one fields."""
92+
return self._onetoonefield_class
93+
94+
@onetoonefield_class.setter
95+
def onetoonefield_class(self, cls):
96+
"""Set the base class to use for all django-tenant-options one-to-one fields."""
97+
self._onetoonefield_class = cls
98+
99+
100+
# Global config instance for django-tenant-options models
101+
model_config = ModelClassConfig()
34102

35103
_DJANGO_TENANT_OPTIONS = getattr(settings, "DJANGO_TENANT_OPTIONS", {})
36104
"""dict: The settings for the django-tenant-options app."""
37105

106+
# Base class settings
107+
MODEL_CLASS = _DJANGO_TENANT_OPTIONS.get("MODEL_CLASS", models.Model)
108+
"""The base Model class to use. Defaults to django.db.models.Model."""
109+
110+
MANAGER_CLASS = _DJANGO_TENANT_OPTIONS.get("MANAGER_CLASS", models.Manager)
111+
"""The base Manager class to use. Defaults to django.db.models.Manager."""
112+
113+
QUERYSET_CLASS = _DJANGO_TENANT_OPTIONS.get("QUERYSET_CLASS", models.QuerySet)
114+
"""The base QuerySet class to use. Defaults to django.db.models.QuerySet."""
115+
116+
FOREIGNKEY_CLASS = _DJANGO_TENANT_OPTIONS.get("FOREIGNKEY_CLASS", models.ForeignKey)
117+
"""The ForeignKey field class to use. Defaults to django.db.models.ForeignKey."""
118+
119+
ONETOONEFIELD_CLASS = _DJANGO_TENANT_OPTIONS.get("ONETOONEFIELD_CLASS", models.OneToOneField)
120+
"""The OneToOneField field class to use. Defaults to django.db.models.OneToOneField."""
121+
122+
model_config.model_class = MODEL_CLASS
123+
model_config.manager_class = MANAGER_CLASS
124+
model_config.queryset_class = QUERYSET_CLASS
125+
model_config.foreignkey_class = FOREIGNKEY_CLASS
126+
model_config.onetoonefield_class = ONETOONEFIELD_CLASS
127+
38128
TENANT_MODEL = _DJANGO_TENANT_OPTIONS.get("TENANT_MODEL", "django_tenant_options.Tenant")
39129
"""str: The model to use for the tenant."""
40130

src/django_tenant_options/models.py

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from django_tenant_options.app_settings import TENANT_MODEL_RELATED_NAME
2626
from django_tenant_options.app_settings import TENANT_MODEL_RELATED_QUERY_NAME
2727
from django_tenant_options.app_settings import TENANT_ON_DELETE
28+
from django_tenant_options.app_settings import model_config
2829
from django_tenant_options.choices import OptionType
2930
from django_tenant_options.exceptions import IncorrectSubclassError
3031
from django_tenant_options.exceptions import InvalidDefaultOptionError
@@ -103,7 +104,9 @@ def __new__(cls, name, bases, attrs, **kwargs):
103104
fields["blank"] = True
104105
fields["null"] = True
105106

106-
ConcreteModel.add_to_class("tenant", models.ForeignKey(ConcreteModel.tenant_model, **fields))
107+
ConcreteModel.add_to_class(
108+
"tenant", model_config.foreignkey_class(ConcreteModel.tenant_model, **fields)
109+
)
107110

108111
return model
109112

@@ -190,7 +193,7 @@ def __new__(cls, name, bases, attrs, **kwargs):
190193

191194
ConcreteSelectionModel.add_to_class(
192195
"option",
193-
models.ForeignKey(
196+
model_config.foreignkey_class(
194197
ConcreteSelectionModel.option_model,
195198
on_delete=ConcreteSelectionModel.option_on_delete,
196199
related_name=ConcreteSelectionModel.option_model_related_name,
@@ -203,7 +206,7 @@ def __new__(cls, name, bases, attrs, **kwargs):
203206
return model
204207

205208

206-
class OptionQuerySet(models.QuerySet):
209+
class OptionQuerySet(model_config.queryset_class):
207210
"""Custom QuerySet for Option models.
208211
209212
Subclass this QuerySet to provide additional functionality for your concrete Option model.
@@ -284,7 +287,7 @@ def delete(self, override=False):
284287
return self.update(deleted=timezone.now())
285288

286289

287-
class OptionManager(models.Manager):
290+
class OptionManager(model_config.manager_class):
288291
"""Manager for Option models.
289292
290293
Provides methods for creating default options and filtering out deleted options.
@@ -327,7 +330,7 @@ def _update_or_create_default_option(self, item_name: str, options_dict: dict =
327330
f"You specified {key} = {value} for {item_name=}."
328331
)
329332

330-
obj, created = self.model.objects.update_or_create(
333+
self.model.objects.update_or_create(
331334
name=item_name,
332335
option_type=option_type,
333336
defaults={"deleted": None}, # Undelete the option if it was previously deleted
@@ -366,7 +369,7 @@ def _update_default_options(self) -> dict:
366369
self.model.objects._update_or_create_default_option(name, options_dict)
367370
updated_options[name] = options_dict
368371
except Exception as e:
369-
logger.error(f"Error updating option {name}: {e}")
372+
logger.error("Error updating option %s: %s", name, e)
370373

371374
# Soft delete options no longer in defaults
372375
existing_options = self.model.objects.filter(
@@ -402,7 +405,7 @@ def get_condition_argument_name():
402405
}
403406

404407

405-
class AbstractOption(models.Model, metaclass=OptionModelBase):
408+
class AbstractOption(model_config.model_class, metaclass=OptionModelBase):
406409
"""Abstract model for defining all available Options.
407410
408411
Options which are provided by default through model configuration may be Mandatory or Optional.
@@ -438,7 +441,7 @@ class AbstractOption(models.Model, metaclass=OptionModelBase):
438441
)
439442

440443
objects = OptionManager.from_queryset(OptionQuerySet)()
441-
unscoped = models.Manager()
444+
unscoped = model_config.manager_class()
442445

443446
class Meta: # pylint: disable=R0903
444447
"""Meta options for AbstractOption.
@@ -546,7 +549,7 @@ def save(self, *args, **kwargs):
546549
super().save(*args, **kwargs)
547550

548551

549-
class SelectionQuerySet(models.QuerySet):
552+
class SelectionQuerySet(model_config.queryset_class):
550553
"""Custom QuerySet for Selection models.
551554
552555
Subclass this QuerySet to provide additional functionality for your concrete Selection model.
@@ -575,7 +578,7 @@ def delete(self, override=False):
575578
return self.update(deleted=timezone.now())
576579

577580

578-
class SelectionManager(models.Manager):
581+
class SelectionManager(model_config.manager_class):
579582
"""Custom Manager for Selection models.
580583
581584
Subclass this manager to provide additional functionality for your concrete Selection model.
@@ -621,7 +624,7 @@ def selected_options_for_tenant(self, tenant, include_deleted=False):
621624
return None
622625

623626

624-
class AbstractSelection(models.Model, metaclass=SelectionModelBase):
627+
class AbstractSelection(model_config.model_class, metaclass=SelectionModelBase):
625628
"""Identifies all selected Options for a given tenant, which it's users can then choose from.
626629
627630
A single tenant can select multiple Options. This model is a through model for the ManyToManyField
@@ -645,7 +648,7 @@ class AbstractSelection(models.Model, metaclass=SelectionModelBase):
645648
)
646649

647650
objects = SelectionManager.from_queryset(SelectionQuerySet)()
648-
unscoped = models.Manager()
651+
unscoped = model_config.manager_class()
649652

650653
class Meta: # pylint: disable=R0903
651654
"""Meta options for AbstractSelection.
@@ -668,8 +671,8 @@ def clean(self):
668671
"""Ensure that the selected option is available to the tenant."""
669672
if self.option.tenant and self.option.tenant != self.tenant: # pylint: disable=E1101
670673
raise ValueError(
671-
"The selected custom option '%s' belongs to tenant '%s', and is not available to tenant '%s'."
672-
% (self.option.name, self.option.tenant, self.tenant)
674+
f"The selected custom option '{self.option.name}' belongs to tenant '{self.option.tenant}', "
675+
f"and is not available to tenant '{self.tenant}'."
673676
)
674677

675678
super().clean()
@@ -689,9 +692,8 @@ def delete(self, using=None, keep_parents=False, override=False):
689692
"""
690693
if override:
691694
return super().delete(using=using, keep_parents=keep_parents)
692-
else:
693-
self.deleted = timezone.now()
694-
self.save()
695+
self.deleted = timezone.now()
696+
self.save()
695697

696698
@classmethod
697699
def get_concrete_subclasses(cls) -> list:

0 commit comments

Comments
 (0)