-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #84 from proteanhq/75-referenced-objects-take-4
75 Support for Foreign Key relationships - Reference Class
- Loading branch information
Showing
10 changed files
with
405 additions
and
14 deletions.
There are no files selected for viewing
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
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
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
from .base import Field | ||
from .mixins import FieldCacheMixin | ||
|
||
from protean.core import exceptions | ||
|
||
|
||
class ReferenceField(Field): | ||
"""Shadow Attribute Field to back References""" | ||
|
||
def __init__(self, reference, **kwargs): | ||
"""Accept reference field as a an attribute, otherwise is a straightforward field""" | ||
self.reference = reference | ||
super().__init__(**kwargs) | ||
|
||
def __set__(self, instance, value): | ||
"""Override `__set__` to update relation field""" | ||
value = self._load(value) | ||
|
||
if value: | ||
instance.__dict__[self.field_name] = value | ||
|
||
# Fetch target object and refresh the reference field value | ||
reference_obj = self.reference.to_cls.find_by( | ||
**{self.reference.linked_attribute: value}) | ||
if reference_obj: | ||
self.reference.value = reference_obj | ||
instance.__dict__[self.reference.field_name] = reference_obj | ||
else: | ||
# Object was not found in the database | ||
raise exceptions.ValueError( | ||
"Target Object not found", | ||
self.reference.field_name) | ||
else: | ||
self._reset_values(instance) | ||
|
||
def __delete__(self, instance): | ||
self._reset_values(instance) | ||
|
||
def _cast_to_type(self, value): | ||
"""Verify type of value assigned to the shadow field""" | ||
# FIXME Verify that the value being assigned is compatible with the remote field | ||
return value | ||
|
||
def _reset_values(self, instance): | ||
"""Reset all associated values and clean up dictionary items""" | ||
instance.__dict__.pop(self.field_name) | ||
instance.__dict__.pop(self.reference.field_name) | ||
self.reference.value = None | ||
self.value = None | ||
|
||
|
||
class Reference(FieldCacheMixin, Field): | ||
""" | ||
Provide a many-to-one relation by adding a column to the local entity | ||
to hold the remote value. | ||
By default ForeignKey will target the pk of the remote model but this | ||
behavior can be changed by using the ``via`` argument. | ||
""" | ||
|
||
def __init__(self, to_cls, via=None, **kwargs): | ||
super().__init__(**kwargs) | ||
self.to_cls = to_cls | ||
self.via = via | ||
|
||
# Choose the Linkage attribute between `via` and `id` | ||
self.linked_attribute = self.via or 'id' | ||
|
||
self.relation = ReferenceField(self) | ||
|
||
def get_attribute_name(self): | ||
"""Return attribute name suffixed with via if defined, or `_id`""" | ||
return '{}_{}'.format(self.field_name, self.linked_attribute) | ||
|
||
def get_shadow_field(self): | ||
"""Return shadow field | ||
Primarily used during Entity initialization to register shadow field""" | ||
return (self.attribute_name, self.relation) | ||
|
||
def __set__(self, instance, value): | ||
"""Override `__set__` to coordinate between relation field and its shadow attribute""" | ||
value = self._load(value) | ||
|
||
if value: | ||
# Check if the reference object has been saved. Otherwise, throw ValueError | ||
if value.id is None: # FIXME not a comprehensive check. Should refer to state | ||
raise exceptions.ValueError( | ||
"Target Object must be saved before being referenced", | ||
self.field_name) | ||
else: | ||
self.relation.value = value.id | ||
instance.__dict__[self.field_name] = value | ||
instance.__dict__[self.attribute_name] = getattr(value, self.linked_attribute) | ||
else: | ||
self._reset_values(instance) | ||
|
||
def __delete__(self, instance): | ||
self._reset_values(instance) | ||
|
||
def _reset_values(self, instance): | ||
"""Reset all associated values and clean up dictionary items""" | ||
self.value = None | ||
self.relation.value = None | ||
instance.__dict__.pop(self.field_name, None) | ||
instance.__dict__.pop(self.attribute_name, None) | ||
|
||
def _cast_to_type(self, value): | ||
if not isinstance(value, self.to_cls): | ||
self.fail('invalid', value=value) | ||
return value | ||
|
||
def get_cache_name(self): | ||
return 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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
NOT_PROVIDED = object() | ||
|
||
|
||
class FieldCacheMixin: | ||
"""Provide an API for working with the model's fields value cache.""" | ||
|
||
def get_cache_name(self): | ||
raise NotImplementedError | ||
|
||
def get_cached_value(self, instance, default=NOT_PROVIDED): | ||
cache_name = self.get_cache_name() | ||
try: | ||
return instance._state.fields_cache[cache_name] | ||
except KeyError: | ||
if default is NOT_PROVIDED: | ||
raise | ||
return default | ||
|
||
def is_cached(self, instance): | ||
return self.get_cache_name() in instance._state.fields_cache | ||
|
||
def set_cached_value(self, instance, value): | ||
instance._state.fields_cache[self.get_cache_name()] = value | ||
|
||
def delete_cached_value(self, instance): | ||
del instance._state.fields_cache[self.get_cache_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
Oops, something went wrong.