Skip to content

Commit 2e88bf1

Browse files
committed
Add checks to ensure correct Manager types
1 parent 5359505 commit 2e88bf1

File tree

2 files changed

+120
-0
lines changed

2 files changed

+120
-0
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Checks for the django_tenant_options app."""
2+
3+
import inspect
4+
5+
from django.core.checks import Error
6+
from django.core.checks import Warning
7+
8+
9+
def check_manager_compliance(model, manager, required_manager, required_queryset, error_ids):
10+
"""Helper function to check if a manager complies with requirements."""
11+
results = []
12+
manager_mro = inspect.getmro(manager.__class__)
13+
queryset_mro = inspect.getmro(manager._queryset_class)
14+
15+
if required_manager not in manager_mro:
16+
results.append(
17+
Warning(
18+
f"Model manager '{manager.__class__.__name__}' does not inherit from '{required_manager.__name__}', "
19+
"so filtering may not work as expected.",
20+
obj=model,
21+
id=f"django_tenant_options.I{error_ids[0]}",
22+
)
23+
)
24+
elif required_queryset not in queryset_mro:
25+
results.append(
26+
Error(
27+
f"Manager {manager.__class__.__name__} must use a queryset that inherits from "
28+
f"{required_queryset.__name__}",
29+
obj=model,
30+
id=f"django_tenant_options.E{error_ids[1]}",
31+
)
32+
)
33+
return results

src/django_tenant_options/models.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
import logging
44

55
from django.apps import apps
6+
from django.core.checks import Error
67
from django.core.exceptions import ValidationError
78
from django.db import models
9+
from django.db.models import Manager
810
from django.db.models import Q
911
from django.db.models.base import ModelBase
1012
from django.db.models.constraints import CheckConstraint
@@ -27,10 +29,33 @@
2729
from django_tenant_options.exceptions import IncorrectSubclassError
2830
from django_tenant_options.exceptions import InvalidDefaultOptionError
2931

32+
from .checks import check_manager_compliance
33+
3034

3135
logger = logging.getLogger("django_tenant_options")
3236

3337

38+
def get_all_managers(model):
39+
"""Get all managers from a model using multiple approaches."""
40+
managers = set()
41+
42+
# Check default manager
43+
if hasattr(model._meta, "default_manager"):
44+
managers.add(model._meta.default_manager)
45+
46+
# Check _managers list
47+
if hasattr(model._meta, "_managers"):
48+
managers.update(model._meta._managers)
49+
50+
# Check direct manager attributes
51+
for attr_name in dir(model):
52+
attr = getattr(model, attr_name)
53+
if isinstance(attr, Manager) and attr.__class__ != Manager:
54+
managers.add(attr)
55+
56+
return managers
57+
58+
3459
def validate_model_is_concrete(model):
3560
"""Raises an error if the provided model class is abstract."""
3661
if model._meta.abstract: # pylint: disable=W0212
@@ -471,6 +496,37 @@ def get_concrete_subclasses(cls) -> list:
471496
result.append(model)
472497
return result
473498

499+
@classmethod
500+
def check(cls, **kwargs):
501+
"""Check that the model has at least one manager that inherits from OptionManager and uses OptionQuerySet."""
502+
errors = super().check(**kwargs)
503+
504+
if cls._meta.abstract:
505+
return errors
506+
507+
managers = get_all_managers(cls)
508+
has_valid_manager = False
509+
510+
for manager in managers:
511+
results = check_manager_compliance(cls, manager, OptionManager, OptionQuerySet, ("001", "002"))
512+
errors.extend(results)
513+
514+
# Check if this manager is fully compliant
515+
if not any(isinstance(r, Error) for r in results):
516+
has_valid_manager = True
517+
518+
if not has_valid_manager:
519+
errors.append(
520+
Error(
521+
f"Model {cls.__name__} must have at least one manager that inherits from OptionManager "
522+
"and uses OptionQuerySet",
523+
obj=cls,
524+
id="django_tenant_options.E003",
525+
)
526+
)
527+
528+
return errors
529+
474530
def clean(self):
475531
"""Ensure no tenant can have the same name as a MANDATORY or OPTIONAL option."""
476532
if self.option_type == OptionType.CUSTOM:
@@ -648,3 +704,34 @@ def get_concrete_subclasses(cls) -> list:
648704
if issubclass(model, cls) and model is not cls and not model._meta.abstract: # pylint: disable=W0212
649705
result.append(model)
650706
return result
707+
708+
@classmethod
709+
def check(cls, **kwargs):
710+
"""Check that model has at least one manager that inherits from SelectionManager and uses SelectionQuerySet."""
711+
errors = super().check(**kwargs)
712+
713+
if cls._meta.abstract:
714+
return errors
715+
716+
managers = get_all_managers(cls)
717+
has_valid_manager = False
718+
719+
for manager in managers:
720+
results = check_manager_compliance(cls, manager, SelectionManager, SelectionQuerySet, ("004", "005"))
721+
errors.extend(results)
722+
723+
# Check if this manager is fully compliant
724+
if not any(isinstance(r, Error) for r in results):
725+
has_valid_manager = True
726+
727+
if not has_valid_manager:
728+
errors.append(
729+
Error(
730+
f"Model {cls.__name__} must have at least one manager that inherits from SelectionManager "
731+
"and uses SelectionQuerySet",
732+
obj=cls,
733+
id="django_tenant_options.E006",
734+
)
735+
)
736+
737+
return errors

0 commit comments

Comments
 (0)