Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Relationships #158

Merged
merged 13 commits into from
Mar 23, 2017
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
1.6.3

- Added `ENABLE_SELF_LINKS` option, default False.
When enabled, links will include reference to the current resource.

- Made `serializer_class` optional on `DynamicRelationField`.
`DynamicRelationField` will attempt to infer `serializer_class` from the
`source` using `DynamicRouter.get_canonical_serializer`.

- Added `getter`/`setter` support to `DynamicRelationField`, allowing
for custom relationship getting and setting. This can be useful for simplifying
complex "through"-relations.
6 changes: 4 additions & 2 deletions dynamic_rest/fields/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
from dynamic_rest.fields.fields import * # noqa
from dynamic_rest.fields.generic import * # noqa
# flake8: noqa
from .base import DynamicField, DynamicMethodField, CountField
from .relation import DynamicRelationField
from .generic import DynamicGenericRelationField
119 changes: 119 additions & 0 deletions dynamic_rest/fields/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
from rest_framework.serializers import SerializerMethodField
from rest_framework import fields


class DynamicField(fields.Field):

"""
Generic field base to capture additional custom field attributes.
"""

def __init__(
self,
requires=None,
deferred=None,
field_type=None,
immutable=False,
**kwargs
):
"""
Arguments:
deferred: Whether or not this field is deferred.
Deferred fields are not included in the response,
unless explicitly requested.
field_type: Field data type, if not inferrable from model.
requires: List of fields that this field depends on.
Processed by the view layer during queryset build time.
"""
self.requires = requires
self.deferred = deferred
self.field_type = field_type
self.immutable = immutable
self.kwargs = kwargs
super(DynamicField, self).__init__(**kwargs)

def to_representation(self, value):
return value

def to_internal_value(self, data):
return data


class DynamicComputedField(DynamicField):
pass


class DynamicMethodField(SerializerMethodField, DynamicField):
pass


class CountField(DynamicComputedField):

"""
Computed field that counts the number of elements in another field.
"""

def __init__(self, serializer_source, *args, **kwargs):
"""
Arguments:
serializer_source: A serializer field.
unique: Whether or not to perform a count of distinct elements.
"""
self.field_type = int
# Use `serializer_source`, which indicates a field at the API level,
# instead of `source`, which indicates a field at the model level.
self.serializer_source = serializer_source
# Set `source` to an empty value rather than the field name to avoid
# an attempt to look up this field.
kwargs['source'] = ''
self.unique = kwargs.pop('unique', True)
return super(CountField, self).__init__(*args, **kwargs)

def get_attribute(self, obj):
source = self.serializer_source
if source not in self.parent.fields:
return None
value = self.parent.fields[source].get_attribute(obj)
data = self.parent.fields[source].to_representation(value)

# How to count None is undefined... let the consumer decide.
if data is None:
return None

# Check data type. Technically len() works on dicts, strings, but
# since this is a "count" field, we'll limit to list, set, tuple.
if not isinstance(data, (list, set, tuple)):
raise TypeError(
"'%s' is %s. Must be list, set or tuple to be countable." % (
source, type(data)
)
)

if self.unique:
# Try to create unique set. This may fail if `data` contains
# non-hashable elements (like dicts).
try:
data = set(data)
except TypeError:
pass

return len(data)


class WithRelationalFieldMixin(object):
"""Mostly code shared by DynamicRelationField and
DynamicGenericRelationField.
"""

def _get_request_fields_from_parent(self):
"""Get request fields from the parent serializer."""
if not self.parent:
return None

if not getattr(self.parent, 'request_fields'):
return None

if not isinstance(self.parent.request_fields, dict):
return None

return self.parent.request_fields.get(self.field_name)
17 changes: 0 additions & 17 deletions dynamic_rest/fields/common.py

This file was deleted.

6 changes: 3 additions & 3 deletions dynamic_rest/fields/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@

from rest_framework.exceptions import ValidationError

from dynamic_rest.fields.common import WithRelationalFieldMixin
from dynamic_rest.fields.fields import DynamicField
from dynamic_rest.routers import DynamicRouter
from .base import WithRelationalFieldMixin, DynamicField
from dynamic_rest.tagged import TaggedDict


Expand Down Expand Up @@ -66,6 +64,7 @@ def get_pk_object(self, type_key, id_value):
def to_representation(self, instance):
try:
# Find serializer for the instance
from dynamic_rest.routers import DynamicRouter
serializer_class = DynamicRouter.get_canonical_serializer(
resource_key=None,
instance=instance
Expand Down Expand Up @@ -115,6 +114,7 @@ def to_internal_value(self, data):
model_name = data.get('type', None)
model_id = data.get('id', None)
if model_name and model_id:
from dynamic_rest.routers import DynamicRouter
serializer_class = DynamicRouter.get_canonical_serializer(
resource_key=None,
resource_name=model_name
Expand Down
Loading