-
Notifications
You must be signed in to change notification settings - Fork 2
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 30, 2015
0 parents
commit f1a5028
Showing
27 changed files
with
1,739 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,22 @@ | ||
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. | ||
|
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,33 @@ | ||
drf-tools | ||
================= | ||
Multiple extensions and test utilities for Django REST Framework 3. | ||
|
||
## Setup ## | ||
|
||
pip install drf-tools | ||
|
||
## Requirement ## | ||
|
||
* Python 2.7+ | ||
* Django 1.6+ | ||
* Django REST Framework 3 | ||
* drf-nested-fields 0.9+ | ||
* drf-hal-json 0.9+ | ||
* drf-enum-field 0.9+ | ||
* drf-nested-routing 0.9+ | ||
* django-filter 0.10+ | ||
* openpyxl 2.0+ | ||
* chardet 2.3+ | ||
|
||
## Features ## | ||
|
||
* Combination of the following libs: | ||
* https://github.com/seebass/drf-nested-fields | ||
* https://github.com/seebass/drf-hal-json | ||
* https://github.com/seebass/drf-enum-field | ||
* https://github.com/seebass/drf-nested-routing | ||
* Additional renderers | ||
* CsvRenderer | ||
* ZipFileRenderer | ||
* XlsxRenderer | ||
* Test utitilities |
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 @@ | ||
|
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,50 @@ | ||
import logging | ||
|
||
from django.core.exceptions import ValidationError, ObjectDoesNotExist, PermissionDenied | ||
from django.http import Http404 | ||
from rest_framework.exceptions import APIException | ||
from rest_framework import status | ||
from rest_framework.response import Response | ||
|
||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
def exception_handler(exc): | ||
headers = {} | ||
if isinstance(exc, APIException): | ||
if getattr(exc, 'auth_header', None): | ||
headers['WWW-Authenticate'] = exc.auth_header | ||
if getattr(exc, 'wait', None): | ||
headers['X-Throttle-Wait-Seconds'] = '%d' % exc.wait | ||
headers['Retry-After'] = '%d' % exc.wait | ||
status_code = exc.status_code | ||
elif isinstance(exc, (ValueError, ValidationError)): | ||
status_code = status.HTTP_400_BAD_REQUEST | ||
elif isinstance(exc, PermissionDenied): | ||
status_code = status.HTTP_403_FORBIDDEN | ||
elif isinstance(exc, (ObjectDoesNotExist, Http404)): | ||
status_code = status.HTTP_404_NOT_FOUND | ||
else: | ||
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR | ||
|
||
if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR or logger.isEnabledFor(logging.DEBUG): | ||
logger.exception(str(exc)) | ||
|
||
return Response(__create_error_response_by_exception(exc), status=status_code, headers=headers) | ||
|
||
|
||
def __create_error_response_by_exception(exc): | ||
if hasattr(exc, 'messages'): | ||
messages = exc.messages | ||
else: | ||
messages = [str(exc)] | ||
return __create_error_response(exc.__class__.__name__, messages) | ||
|
||
|
||
def __create_error_response(error_type, messages, code=0): | ||
error = dict() | ||
error['type'] = error_type | ||
error['messages'] = messages | ||
error['code'] = code | ||
return {'error': error} |
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,9 @@ | ||
from rest_framework.fields import CharField | ||
|
||
|
||
class FilenameField(CharField): | ||
def to_representation(self, value): | ||
value = super(FilenameField, self).to_representation(value) | ||
if value: | ||
value = value.split("/")[-1] | ||
return value |
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,89 @@ | ||
from django.core.validators import EMPTY_VALUES | ||
from django_filters import FilterSet, BooleanFilter | ||
from django_filters.filters import Filter | ||
from django import forms | ||
from django.utils import six | ||
|
||
|
||
class ListFilterSet(FilterSet): | ||
""" | ||
The filterset handles a list of values as filter, that are connected using the OR-Operator | ||
""" | ||
|
||
@property | ||
def qs(self): | ||
if not hasattr(self, '_qs'): | ||
valid = self.is_bound and self.form.is_valid() | ||
|
||
if self.strict and self.is_bound and not valid: | ||
self._qs = self.queryset.none() | ||
return self._qs | ||
|
||
# start with all the results and filter from there | ||
qs = self.queryset.all() | ||
for name, filter_ in six.iteritems(self.filters): | ||
# CUSTOM:START | ||
value_list = None | ||
|
||
if valid and self.data: | ||
value_list = self.data.getlist(name) | ||
|
||
if value_list: # valid & clean data | ||
filtered_qs = None | ||
for value in value_list: | ||
if isinstance(filter_, BooleanFilter): | ||
value = self._str_to_boolean(value) | ||
|
||
if not filtered_qs: | ||
filtered_qs = filter_.filter(qs, value) | ||
else: | ||
filtered_qs |= filter_.filter(qs, value) | ||
qs = filtered_qs | ||
# CUSTOM:END | ||
|
||
if self._meta.order_by: | ||
order_field = self.form.fields[self.order_by_field] | ||
data = self.form[self.order_by_field].data | ||
ordered_value = None | ||
try: | ||
ordered_value = order_field.clean(data) | ||
except forms.ValidationError: | ||
pass | ||
|
||
if ordered_value in EMPTY_VALUES and self.strict: | ||
ordered_value = self.form.fields[self.order_by_field].choices[0][0] | ||
|
||
if ordered_value: | ||
qs = qs.order_by(*self.get_order_by(ordered_value)) | ||
|
||
self._qs = qs | ||
|
||
return self._qs | ||
|
||
@staticmethod | ||
def _str_to_boolean(value): | ||
if value.lower() == "true": | ||
return True | ||
if value.lower() == "false": | ||
return False | ||
return value | ||
|
||
|
||
class EnumFilter(Filter): | ||
def __init__(self, enum_type, *args, **kwargs): | ||
super(EnumFilter, self).__init__(*args, **kwargs) | ||
self.enum_type = enum_type | ||
|
||
field_class = forms.CharField | ||
|
||
def filter(self, qs, value): | ||
if value in ([], (), {}, None, ''): | ||
return qs | ||
enum_value = None | ||
for choice in self.enum_type: | ||
if choice.name == value or choice.value == value: | ||
enum_value = choice | ||
break | ||
if enum_value is None: | ||
raise ValueError("'{value}' is not a valid value for '{enum}'".format(value=value, enum=self.enum_type.__name__)) | ||
return super(EnumFilter, self).filter(qs, enum_value) |
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,69 @@ | ||
from rest_framework.renderers import BaseRenderer as OriginalBaseRenderer | ||
|
||
from drf_tools.serializers import ZipSerializer, CsvSerializer | ||
from drf_tools.serializers import XlsxSerializer | ||
|
||
|
||
class BaseFileRenderer(OriginalBaseRenderer): | ||
KWARGS_KEY_FILENAME = "filename" | ||
|
||
def _add_filename_to_response(self, renderer_context): | ||
filename = self.__get_filename(renderer_context) | ||
if filename: | ||
renderer_context['response']['Content-Disposition'] = 'attachment; filename="{}"'.format(filename) | ||
|
||
def __get_filename(self, renderer_context): | ||
filename = renderer_context['kwargs'].get(self.KWARGS_KEY_FILENAME) | ||
if filename and self.format and not filename.endswith('.' + self.format): | ||
filename += "." + self.format | ||
return filename | ||
|
||
|
||
class CsvRenderer(BaseFileRenderer): | ||
media_type = "text/csv" | ||
format = "txt" | ||
|
||
def render(self, data, accepted_media_type=None, renderer_context=None): | ||
self._add_filename_to_response(renderer_context) | ||
return CsvSerializer.serialize(data) | ||
|
||
|
||
class XlsxRenderer(BaseFileRenderer): | ||
media_type = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' | ||
format = 'xlsx' | ||
charset = None | ||
render_style = 'binary' | ||
|
||
def render(self, data, accepted_media_type=None, renderer_context=None): | ||
self._add_filename_to_response(renderer_context) | ||
if not isinstance(data, list): | ||
return data | ||
|
||
return XlsxSerializer.serialize(data) | ||
|
||
|
||
class ZipFileRenderer(BaseFileRenderer): | ||
""" | ||
A zip file is created containing the given dict with filename->bytes | ||
""" | ||
media_type = 'application/x-zip-compressed' | ||
format = 'zip' | ||
charset = None | ||
render_style = 'binary' | ||
|
||
def render(self, data, accepted_media_type=None, renderer_context=None): | ||
self._add_filename_to_response(renderer_context) | ||
return ZipSerializer.serialize(data) | ||
|
||
|
||
class AnyFileFromSystemRenderer(BaseFileRenderer): | ||
""" | ||
Given the full file path, the file is opened, read and returned | ||
""" | ||
media_type = '*/*' | ||
render_style = 'binary' | ||
|
||
def render(self, data, accepted_media_type=None, renderer_context=None): | ||
self._add_filename_to_response(renderer_context) | ||
with open(data, "rb") as file: | ||
return file.read() |
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,57 @@ | ||
from collections import OrderedDict | ||
|
||
from django.core.urlresolvers import NoReverseMatch | ||
from rest_framework.permissions import AllowAny | ||
from rest_framework.response import Response | ||
from rest_framework.reverse import reverse | ||
from rest_framework.routers import DefaultRouter | ||
from rest_framework.views import APIView | ||
|
||
from drf_nested_routing.routers import NestedRouterMixin | ||
|
||
|
||
class NestedRouterWithExtendedRootView(NestedRouterMixin, DefaultRouter): | ||
""" | ||
Router that handles nested routes and additionally adds given api_view_urls to the ApiRootView (the api entrypoint) | ||
""" | ||
def __init__(self, api_view_urls): | ||
self.__api_view_urls = api_view_urls | ||
super(NestedRouterWithExtendedRootView, self).__init__() | ||
|
||
def get_api_root_view(self): | ||
api_root_routes = {} | ||
list_name = self.routes[0].name | ||
for prefix, viewset, basename in self.registry: | ||
api_root_routes[prefix] = list_name.format(basename=basename) | ||
|
||
api_view_urls = self.__api_view_urls | ||
|
||
class ApiRootView(APIView): | ||
|
||
permission_classes = (AllowAny,) | ||
|
||
def get(self, request, *args, **kwargs): | ||
links = OrderedDict() | ||
links['viewsets'] = OrderedDict() | ||
for key, url_name in api_root_routes.items(): | ||
try: | ||
links['viewsets'][key] = reverse(url_name, request=request, format=kwargs.get('format', None)) | ||
except NoReverseMatch: | ||
continue | ||
|
||
links['views'] = OrderedDict() | ||
for api_view_url in api_view_urls: | ||
url_name = api_view_url.name | ||
try: | ||
if '<pk>' in api_view_url._regex: | ||
links['views'][url_name] = reverse(url_name, request=request, | ||
format=kwargs.get('format', None), args=(0,)) | ||
else: | ||
links['views'][url_name] = reverse(url_name, request=request, | ||
format=kwargs.get('format', None)) | ||
except NoReverseMatch as e: | ||
continue | ||
|
||
return Response({"_links": links}) | ||
|
||
return ApiRootView().as_view() |
Oops, something went wrong.