Skip to content
Snippets Groups Projects

Allow adding a foreign key to Exensible Model

Merged Nik | Klampfradler requested to merge feature/extensible-model-fk into master
All threads resolved!
+ 62
2
@@ -21,7 +21,7 @@ from django.views.generic.edit import ModelFormMixin
import reversion
from easyaudit.models import CRUDEvent
from guardian.admin import GuardedModelAdmin
from jsonstore.fields import JSONField, JSONFieldMixin
from jsonstore.fields import IntegerField, JSONField, JSONFieldMixin
from material.base import Layout, LayoutNode
from rules.contrib.admin import ObjectPermissionsModelAdmin
@@ -103,7 +103,7 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase):
objects_all_sites = models.Manager()
extra_permissions = []
def get_absolute_url(self) -> str:
"""Get the URL o a view representing this model instance."""
pass
@@ -209,6 +209,66 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase):
cls._safe_add(field, name)
@classmethod
def foreign_key(
cls,
field_name: str,
to: models.Model,
to_field: str = "pk",
to_field_type: JSONFieldMixin = IntegerField,
related_name: Optional[str] = None,
) -> None:
"""Add a virtual ForeignKey.
This works by storing the primary key (or any field passed in the to_field argument)
and adding a property that queries the desired model.
If the foreign model also is an ExtensibleModel, a reverse mapping is also added under
the related_name passed as argument, or this model's default related name.
"""
id_field_name = f"{field_name}_id"
if related_name is None:
related_name = cls.Meta.default_related_name
# Add field to hold key to foreign model
id_field = to_field_type()
cls.field(**{id_field_name: id_field})
@property
def _virtual_fk(self) -> Optional[models.Model]:
id_field_val = getattr(self, id_field_name)
if id_field_val:
try:
return to.objects.get(**{to_field: id_field_val})
except to.DoesNotExist:
# We found a stale foreign key
setattr(self, id_field_name, None)
self.save()
return None
else:
return None
@_virtual_fk.setter
def _virtual_fk(self, value: Optional[models.Model] = None) -> None:
if value is None:
id_field_val = None
else:
id_field_val = getattr(value, to_field)
setattr(self, id_field_name, id_field_val)
# Add property to wrap get/set on foreign model instance
cls._safe_add(_virtual_fk, field_name)
# Add related property on foreign model instance if it provides such an interface
if hasattr(to, "_safe_add"):
def _virtual_related(self) -> models.QuerySet:
id_field_val = getattr(self, to_field)
return cls.objects.filter(**{id_field_name: id_field_val})
to.property_(_virtual_related, related_name)
@classmethod
def syncable_fields(cls) -> List[models.Field]:
"""Collect all fields that can be synced on a model."""
Loading