-
-
Save rgutierrez-cotech/7655b7574b51e48dee91e33695758348 to your computer and use it in GitHub Desktop.
Contextual model full version
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
class ContextualModel(BaseModel): | |
""" | |
A model mixin for handling the creation of a Context | |
instance when a model has a `context` field. | |
This mixin allows us to add a property with a "context_" | |
prefix to our model instance before saving. Then, the mixin | |
will parse the property name and create a corresponding | |
Context object. This object is then accessible through a | |
lazy `context` property to avoid multiple database reads, | |
since a Context will only need to be created once. | |
""" | |
_context_type = models.CharField(max_length=255, db_column='context_type') | |
_context_ref = models.ForeignKey(Context, | |
on_delete=models.CASCADE, | |
related_name='+', | |
db_column='context_ref') | |
class Meta: | |
abstract = True | |
def _context_getter(self): | |
return getattr(self._context_ref, self._context_type) | |
def __getattribute__(self, name): | |
""" | |
Magic to intercept the | |
django/db/models/fields/related_descriptors magic, just | |
for the `context` property only. | |
Hard-codes the `lazy_property` code in `utils`; I | |
couldn't think of a better way | |
""" | |
if name == 'context': | |
if not hasattr(self, '_lazy_context'): | |
setattr(self, '_lazy_context', self._context_getter()) | |
return getattr(self, '_lazy_context') | |
else: | |
return BaseModel.__getattribute__(self, name) | |
def _get_context_field_name(self): | |
""" | |
Get the "fake" context field. Only works during object | |
creation; will return None otherwise. | |
""" | |
return get_context_field_name(self) | |
def save(self, *args, **kwargs): | |
""" | |
When saving, check for an instance property with the | |
prefix `context_`. This is our "fake" context property | |
that we'll use to create the Context object, add the | |
instance we want to use in our Context object, and | |
wire it up to this instance. | |
""" | |
if not self.pk: | |
context_field_name = self._get_context_field_name() | |
context_type = context_field_name.split('_')[1] | |
context_obj = getattr(self, context_field_name) | |
if not context_obj: | |
raise ValidationError('A model instance is required in order to create a Context through-object.') | |
delattr(self, context_field_name) | |
ctx = Context.get_or_create(context_type, context_obj) | |
if not ctx: | |
raise DatabaseError('Cannot proceed! Context through-object was not saved.') | |
self._context_type = context_type | |
self._context_ref = ctx | |
super().save(*args, **kwargs) | |
@classmethod | |
def get_by_context(cls, context_obj, **filter_args): | |
""" | |
Shortcut method that allows us to pass an object as the | |
'context' and go through the Context object to return a | |
list of objects connected to the Context object. | |
If no Context through-object is found, just return an | |
empty QuerySet instead of erroring out, because we want | |
this to behave like filter() | |
""" | |
context_type = context_obj.object_type | |
args = {context_type: context_obj.id} | |
try: | |
ctx = Context.objects.get(**args) | |
except Context.DoesNotExist: | |
logging.warning('Context through-object does not exist with parameters <{}, {}>'\ | |
.format(context_type, context_obj.id)) | |
return cls.objects.none() | |
return cls.objects.filter(_context_ref=ctx, **filter_args) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment