Created
February 25, 2024 18:31
-
-
Save nrbnlulu/623628a65f3b98a3b415d51999a41480 to your computer and use it in GitHub Desktop.
Django Generic Relation "Table-per-relation"
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from __future__ import annotations | |
import contextlib | |
import dataclasses | |
import enum | |
from importlib import import_module | |
from typing import TYPE_CHECKING, Generic, TypeVar, get_type_hints | |
from django.db import models | |
from django.utils.translation import gettext_lazy as _ | |
Container = TypeVar("Container", bound=models.Model) | |
GenericModel = TypeVar("GenericModel", bound=models.Model) | |
if TYPE_CHECKING: | |
class _GeneratedModelBase(models.Model, Generic[Container]): | |
objects: models.Manager[Container] = models.Manager() # type: ignore | |
related: Container | |
__related_model__: type[Container] | |
@dataclasses.dataclass(slots=True, kw_only=True) | |
class ModelGenerator(Generic[Container, GenericModel]): | |
generic_model: type[GenericModel] | None = None | |
models_to_inject: dict[str, type[Container]] = dataclasses.field(default_factory=dict) | |
generated_models: dict[str, type[_GeneratedModelBase[Container]]] = dataclasses.field( | |
default_factory=dict, | |
) | |
def add(self, t: type[Container]) -> type[Container]: | |
self.models_to_inject[t.__name__] = t | |
return t | |
def get_for_model(self, model: type[Container]) -> type[_GeneratedModelBase[Container]]: | |
for generated in self.generated_models.values(): | |
if generated.__related_model__ is model: # type: ignore | |
return generated | |
msg = f"generic model: {self.generic_model} was not found for model: {model}" | |
raise KeyError(msg) | |
def assign_generic(self, t: type[GenericModel]) -> type[_GeneratedModelBase[Container]]: | |
assert t._meta.abstract | |
self.generic_model = t | |
return t # type: ignore | |
def generate(self) -> None: | |
"""Call this at the end of models.py to generate models.""" | |
assert self.generic_model | |
for model_to_inject in self.models_to_inject.values(): | |
cls_name = model_to_inject.__name__ + self.generic_model.__name__ | |
related_name: str | None = None | |
for name, annot in get_type_hints(model_to_inject).items(): | |
if annot is self.generic_model: | |
related_name = name | |
assert ( | |
related_name | |
), "the generic model was not type hinted at the injected model or in one of it bases" | |
class Meta: | |
verbose_name = _(cls_name + f" {related_name}") | |
verbose_name_plural = _(cls_name + f" {related_name}s") | |
new_model = type( | |
cls_name, | |
(self.generic_model,), | |
{ | |
"related": models.ForeignKey( | |
model_to_inject, | |
on_delete=models.CASCADE, | |
related_name=related_name, | |
), | |
"__related_model__": model_to_inject, | |
"Meta": Meta, | |
"__module__": model_to_inject.__module__, | |
}, | |
) | |
self.generated_models[cls_name] = new_model # type: ignore | |
module = import_module(self.generic_model.__module__) | |
module.__dict__.update(self.generated_models) | |
class GenericTableGeneratorEnumMeta(Generic[Container], enum.EnumMeta): | |
def __new__(metacls, klass_name, bases, ns, **kwargs): # noqa: ANN003, ANN001, ANN204 | |
if klass_name == "GenericTableGeneratorEnum": | |
return super().__new__(metacls, klass_name, bases, ns, **kwargs) | |
enumerations = {member: value for member, value in ns.items() if not member.startswith("_")} | |
model_map = {} | |
ns["__model_map__"] = model_map | |
for name, model in enumerations.items(): | |
assert issubclass(model, models.Model) | |
assert name == model.__name__ | |
model_map[name] = model | |
enumerations[name] = name | |
with contextlib.suppress(TypeError): | |
# django imported this module twice? | |
ns.update(enumerations) | |
return super().__new__(metacls, klass_name, bases, ns, **kwargs) | |
class GenericTableGeneratorEnum( | |
Generic[Container, GenericModel], | |
enum.Enum, | |
metaclass=GenericTableGeneratorEnumMeta, | |
): | |
__model_map__: dict[str, type[_GeneratedModelBase[Container]]] | |
def get_klass(self) -> type[_GeneratedModelBase[Container]]: | |
return self.__model_map__[self.name] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from __future__ import annotations | |
from typing import TYPE_CHECKING, TypeVar | |
import attrs | |
from attr import define | |
import factory.django | |
if TYPE_CHECKING: | |
from django.db import models | |
from djtools.model_generator import ModelGenerator, _GeneratedModelBase | |
T = TypeVar("T", bound=factory.django.DjangoModelFactory) | |
@define | |
class ModelGeneratorTester: | |
generator: ModelGenerator | |
factories_by_related_model: dict[models.Model, factory.django.DjangoModelFactory] | |
factories_by_generated_model: dict[type[_GeneratedModelBase], factory.django.DjangoModelFactory] | |
factories_by_related_factory: dict[ | |
factory.django.DjangoModelFactory, | |
factory.django.DjangoModelFactory, | |
] = attrs.Factory(dict) | |
"""hash of related factory with its generated factory.""" | |
@classmethod | |
def from_model_generator( | |
cls, | |
generator: ModelGenerator, | |
generic_factory: type[factory.django.DjangoModelFactory], | |
) -> ModelGeneratorTester: | |
factory = None | |
generated_map: dict[type[_GeneratedModelBase], factory.django.DjangoModelFactory] = {} | |
related_map: dict[models.Model, factory.django.DjangoModelFactory] = {} | |
for generated in generator.generated_models.values(): | |
class Meta: | |
model = generated._meta.model | |
factory = type(generated.__name__ + "Factory", (generic_factory,), {"Meta": Meta}) | |
generated_map[generated._meta.model] = factory | |
related_map[generated.__related_model__] = factory | |
return cls( | |
factories_by_generated_model=generated_map, | |
factories_by_related_model=related_map, | |
generator=generator, | |
) | |
def add(self, related_factory: type[T]) -> type[T]: | |
match: factory.django.DjangoModelFactory | None = None | |
for generated_model, generated_factory in self.factories_by_generated_model.items(): | |
if generated_model.__related_model__ is related_factory._meta.model: | |
match = generated_factory | |
assert match, f"could not find corresponding model for {related_factory}" | |
self.factories_by_related_factory[related_factory] = match | |
return related_factory |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This won't work for you out of the box and the typing are not 100% correct though this proves a concept that you don't need
Django's GFK framework in order to create generic relations.