-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Sebastian Bredehöft
committed
Jun 29, 2015
0 parents
commit 31acd20
Showing
21 changed files
with
594 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
# Byte-compiled / optimized / DLL files | ||
__pycache__/ | ||
*.py[cod] | ||
|
||
# C extensions | ||
*.so | ||
|
||
# Distribution / packaging | ||
.Python | ||
env/ | ||
build/ | ||
develop-eggs/ | ||
dist/ | ||
downloads/ | ||
eggs/ | ||
.eggs/ | ||
lib/ | ||
lib64/ | ||
parts/ | ||
sdist/ | ||
var/ | ||
*.egg-info/ | ||
.installed.cfg | ||
*.egg | ||
|
||
# PyInstaller | ||
# Usually these files are written by a python script from a template | ||
# before PyInstaller builds the exe, so as to inject date/other infos into it. | ||
*.manifest | ||
*.spec | ||
|
||
# Installer logs | ||
pip-log.txt | ||
pip-delete-this-directory.txt | ||
|
||
# Unit test / coverage reports | ||
htmlcov/ | ||
.tox/ | ||
.coverage | ||
.coverage.* | ||
.cache | ||
nosetests.xml | ||
coverage.xml | ||
*,cover | ||
|
||
# Translations | ||
*.mo | ||
*.pot | ||
|
||
# Django stuff: | ||
*.log | ||
|
||
# Sphinx documentation | ||
docs/_build/ | ||
|
||
# PyBuilder | ||
target/ | ||
|
||
**/.idea/* |
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,51 @@ | ||
README.md | ||
|
||
## Main License ## | ||
|
||
### The MIT License (MIT) ### | ||
|
||
Copyright (c) 2015 Sebastian Bredehöft | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. | ||
|
||
## Additional License information ## | ||
|
||
Parts from *drf_nested_routing.routers.py* and *drf_nested_routing.views.py* are copied from or similar to contents from drf-extensions with the following license: | ||
|
||
### The MIT License (MIT) ### | ||
|
||
Copyright (c) 2013 Gennady Chibisov. | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
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,59 @@ | ||
drf-nested-routing | ||
================= | ||
Extension for Django REST Framework 3 which allows for usage nested resources. | ||
|
||
## Setup ## | ||
|
||
pip install drf-nested-routing | ||
|
||
## Requirement ## | ||
|
||
* Python 2.7+ | ||
* Django 1.6+ | ||
* Django REST Framework 3 | ||
|
||
## Example ## | ||
|
||
models.py: | ||
|
||
class TestResource(models.Model): | ||
name = models.CharField(max_length=255) | ||
active = models.BooleanField(default=True) | ||
|
||
class NestedResource(models.Model): | ||
resource = models.ForeignKey(TestResource) | ||
name = models.CharField(max_length=255) | ||
serializers.py: | ||
|
||
class TestResourceSerializer(HyperlinkedModelSerializer): | ||
class Meta: | ||
model = TestResource | ||
|
||
class NestedResourceSerializer(NestedRoutingSerializerMixin, HyperlinkedModelSerializer): | ||
class Meta: | ||
model = NestedResource | ||
views.py: | ||
|
||
class TestResourceViewSet(ModelViewSet): | ||
serializer_class = TestResourceSerializer | ||
queryset = TestResource.objects.all() | ||
|
||
class NestedResourceViewSet(NestedViewSetMixin, ModelViewSet): | ||
serializer_class = NestedResourceSerializer | ||
queryset = NestedResource.objects.all() | ||
|
||
urls.py | ||
|
||
class NestedSimpleRouter(NestedRouterMixin, SimpleRouter): | ||
pass | ||
|
||
router = NestedSimpleRouter() | ||
resourceRoute = router.register(r'test-resources', TestResourceViewSet) | ||
resourceRoute.register(r'nested', NestedResourceViewSet, ['resource']) | ||
|
||
urlpatterns = patterns( | ||
'', | ||
url(r'', include(router.urls)), | ||
) |
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,15 @@ | ||
PARENT_LOOKUP_NAME_PREFIX="parent_lookup_" | ||
|
||
__parent_query_lookups_by_view = dict() | ||
|
||
|
||
def add_parent_query_lookups(viewName, parent_query_lookups): | ||
__parent_query_lookups_by_view[viewName] = parent_query_lookups | ||
|
||
|
||
def get_parent_query_lookups_by_view(view_name): | ||
return __parent_query_lookups_by_view.get(view_name, []) | ||
|
||
|
||
def get_parent_query_lookups_by_class(clazz): | ||
return get_parent_query_lookups_by_view(clazz.__name__.lower()) |
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,62 @@ | ||
from django.db.models import Model | ||
from rest_framework.relations import HyperlinkedRelatedField, HyperlinkedIdentityField, reverse, PKOnlyObject | ||
from drf_nested_routing import get_parent_query_lookups_by_view | ||
import drf_nested_routing | ||
|
||
|
||
class NestedHyperlinkedRelatedField(HyperlinkedRelatedField): | ||
def get_url(self, obj, view_name, request, format): | ||
""" | ||
Given an object, return the URL that hyperlinks to the object. | ||
May raise a `NoReverseMatch` if the `view_name` and `lookup_field` | ||
attributes are not configured to correctly match the URL conf. | ||
""" | ||
if hasattr(obj, 'pk') and obj.pk is None: | ||
return None | ||
|
||
if isinstance(obj, PKOnlyObject): | ||
obj = self.queryset.get(pk=obj.pk) | ||
|
||
parent_lookups = get_parent_query_lookups_by_view(view_name.split("-")[0]) | ||
lookup_field = getattr(obj, self.lookup_field) | ||
kwargs = {self.lookup_field: lookup_field} | ||
if parent_lookups: | ||
for lookup in parent_lookups: | ||
parent_lookup = obj | ||
lookup_path = lookup.split('__') | ||
if len(lookup_path) > 1: | ||
for part in lookup_path[:-1]: | ||
parent_lookup = getattr(parent_lookup, part) | ||
parent_lookup_id = getattr(parent_lookup, lookup_path[-1] + '_id', None) | ||
kwargs[drf_nested_routing.PARENT_LOOKUP_NAME_PREFIX + lookup] = parent_lookup_id | ||
return reverse(view_name, kwargs=kwargs, request=request, format=format) | ||
|
||
|
||
class NestedHyperlinkedIdentityField(HyperlinkedIdentityField): | ||
def get_url(self, obj, view_name, request, format): | ||
""" | ||
Given an object, return the URL that hyperlinks to the object. | ||
May raise a `NoReverseMatch` if the `view_name` and `lookup_field` | ||
attributes are not configured to correctly match the URL conf. | ||
""" | ||
if hasattr(obj, 'pk') and obj.pk is None: | ||
return None | ||
|
||
parent_lookups = get_parent_query_lookups_by_view(view_name.split("-")[0]) | ||
lookup_field = getattr(obj, self.lookup_field, None) | ||
kwargs = {self.lookup_field: lookup_field} | ||
if parent_lookups: | ||
for lookup in parent_lookups: | ||
lookup_path = lookup.split('__') | ||
parent_lookup = obj | ||
for part in lookup_path: | ||
parent_lookup = getattr(parent_lookup, part) | ||
parent_lookup_id = parent_lookup.id if isinstance(parent_lookup, Model) else parent_lookup | ||
kwargs[drf_nested_routing.PARENT_LOOKUP_NAME_PREFIX + lookup] = parent_lookup_id | ||
|
||
if lookup_field is None: # Handle unsaved object case | ||
return None | ||
|
||
return reverse(view_name, kwargs=kwargs, request=request, format=format) |
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,68 @@ | ||
from django.core.urlresolvers import NoReverseMatch | ||
from rest_framework import views | ||
from rest_framework.reverse import reverse | ||
from rest_framework.response import Response | ||
|
||
from drf_nested_routing import add_parent_query_lookups | ||
import drf_nested_routing | ||
|
||
|
||
class NestedRegistryItem(object): | ||
def __init__(self, router, parent_prefix, parent_item=None): | ||
self.router = router | ||
self.parent_prefix = parent_prefix | ||
self.parent_item = parent_item | ||
|
||
def register(self, prefix, viewset, parent_query_lookups=None): | ||
base_name = viewset.queryset.model.__name__.lower() | ||
if parent_query_lookups: | ||
add_parent_query_lookups(base_name, parent_query_lookups) | ||
self.router._register(prefix=self.get_prefix(current_prefix=prefix, parent_query_lookups=parent_query_lookups), | ||
viewset=viewset, base_name=base_name) | ||
return NestedRegistryItem(router=self.router, parent_prefix=prefix, parent_item=self) | ||
|
||
def get_prefix(self, current_prefix, parent_query_lookups): | ||
return '{0}/{1}'.format(self.get_parent_prefix(parent_query_lookups), current_prefix) | ||
|
||
def get_parent_prefix(self, parent_query_lookups): | ||
prefix = '/' | ||
current_item = self | ||
i = len(parent_query_lookups) - 1 | ||
while current_item: | ||
prefix = '{}/(?P<{}>[^/.]+)/{}'.format(current_item.parent_prefix, | ||
drf_nested_routing.PARENT_LOOKUP_NAME_PREFIX + parent_query_lookups[i], prefix) | ||
i -= 1 | ||
current_item = current_item.parent_item | ||
return prefix.strip('/') | ||
|
||
|
||
class NestedRouterMixin(object): | ||
def _register(self, *args, **kwargs): | ||
return super(NestedRouterMixin, self).register(*args, **kwargs) | ||
|
||
def register(self, *args, **kwargs): | ||
self._register(*args, **kwargs) | ||
return NestedRegistryItem(router=self, parent_prefix=self.registry[-1][0]) | ||
|
||
def get_api_root_view(self): | ||
""" | ||
Return a view to use as the API root. | ||
""" | ||
api_root_dict = {} | ||
list_name = self.routes[0].name | ||
for prefix, viewset, basename in self.registry: | ||
api_root_dict[prefix] = list_name.format(basename=basename) | ||
|
||
class APIRoot(views.APIView): | ||
_ignore_model_permissions = True | ||
|
||
def get(self, request, format=None): | ||
ret = {} | ||
for key, url_name in api_root_dict.items(): | ||
try: | ||
ret[key] = reverse(url_name, request=request, format=format) | ||
except NoReverseMatch: | ||
pass | ||
return Response(ret) | ||
|
||
return APIRoot.as_view() |
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,7 @@ | ||
from drf_nested_routing.fields import NestedHyperlinkedIdentityField | ||
from drf_nested_routing.fields import NestedHyperlinkedRelatedField | ||
|
||
|
||
class NestedRoutingSerializerMixin: | ||
serializer_related_field = NestedHyperlinkedRelatedField | ||
serializer_url_field = NestedHyperlinkedIdentityField |
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,67 @@ | ||
from rest_framework.mixins import UpdateModelMixin, CreateModelMixin | ||
|
||
import drf_nested_routing | ||
|
||
|
||
class NestedViewSetMixin(object): | ||
def get_queryset(self): | ||
queryset = super(NestedViewSetMixin, self).get_queryset() | ||
self.__add_related_fetches_to_querySet(queryset) | ||
return self.__filter_query_set_by_parent_lookups(queryset) | ||
|
||
def __add_related_fetches_to_querySet(self, queryset): | ||
parent_lookups = drf_nested_routing.get_parent_query_lookups_by_class(queryset.model.__class__) | ||
related = getattr(self, 'select_related', []) | ||
lookups = parent_lookups + related | ||
if lookups: | ||
queryset = queryset.select_related(*lookups) | ||
prefetches = getattr(self, 'prefetch_related', []) | ||
for prefetch in prefetches: | ||
queryset = queryset.prefetch_related(prefetch) | ||
|
||
def __filter_query_set_by_parent_lookups(self, queryset): | ||
parents_query_dict = self.__get_parents_query_filter() | ||
if parents_query_dict: | ||
queryset = queryset.filter(**parents_query_dict) | ||
return queryset | ||
|
||
def __get_parents_query_filter(self): | ||
result = {} | ||
for kwarg_name in self.kwargs: | ||
if kwarg_name.startswith(drf_nested_routing.PARENT_LOOKUP_NAME_PREFIX): | ||
query_lookup = kwarg_name.replace(drf_nested_routing.PARENT_LOOKUP_NAME_PREFIX, '', 1) | ||
query_value = self.kwargs.get(kwarg_name) | ||
result[query_lookup] = query_value | ||
# return parent query filter if not wildcard * | ||
return {k: v for k, v in result.items() if v != '*'} | ||
|
||
|
||
class AddParentToRequestDataMixin: | ||
def _add_parent_to_request_data(self, request, parent_key, parent_id): | ||
raise NotImplementedError() | ||
|
||
def _add_parent_to_request_data_through_lookup(self, request, **kwargs): | ||
lookup_prefix = drf_nested_routing.PARENT_LOOKUP_NAME_PREFIX | ||
for kwarg in kwargs: | ||
if not kwarg.startswith(lookup_prefix): | ||
continue | ||
|
||
parent_lookup_key = kwarg | ||
parent_lookup_key_without_prefix = parent_lookup_key[len(lookup_prefix):] | ||
if '__' in parent_lookup_key_without_prefix: | ||
continue # only the direct parent is added to data | ||
|
||
self._add_parent_to_request_data(request, parent_lookup_key_without_prefix, kwargs[parent_lookup_key]) | ||
|
||
|
||
class CreateNestedModelMixin(AddParentToRequestDataMixin, CreateModelMixin): | ||
def create(self, request, *args, **kwargs): | ||
self._add_parent_to_request_data_through_lookup(request, **kwargs) | ||
return super(CreateNestedModelMixin, self).create(request, *args, **kwargs) | ||
|
||
|
||
class UpdateNestedModelMixin(AddParentToRequestDataMixin, UpdateModelMixin): | ||
def update(self, request, *args, **kwargs): | ||
if not kwargs.get('partial', False): | ||
self._add_parent_to_request_data_through_lookup(request, **kwargs) | ||
return super(UpdateNestedModelMixin, self).update(request, *args, **kwargs) |
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,2 @@ | ||
Django==1.8.2 | ||
djangorestframework==3.1.3 |
Oops, something went wrong.