Skip to content

Instantly share code, notes, and snippets.

@rgutierrez-cotech
Last active January 27, 2021 19:38
Show Gist options
  • Save rgutierrez-cotech/7655b7574b51e48dee91e33695758348 to your computer and use it in GitHub Desktop.
Save rgutierrez-cotech/7655b7574b51e48dee91e33695758348 to your computer and use it in GitHub Desktop.
Contextual model full version
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