Skip to content

Commit

Permalink
ADDED: initial version
Browse files Browse the repository at this point in the history
  • Loading branch information
Sebastian Bredehöft committed Jun 30, 2015
0 parents commit f1a5028
Show file tree
Hide file tree
Showing 27 changed files with 1,739 additions and 0 deletions.
59 changes: 59 additions & 0 deletions .gitignore
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/*
22 changes: 22 additions & 0 deletions LICENSE.md
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.

33 changes: 33 additions & 0 deletions README.md
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
1 change: 1 addition & 0 deletions drf_tools/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

50 changes: 50 additions & 0 deletions drf_tools/exceptions.py
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}
9 changes: 9 additions & 0 deletions drf_tools/fields.py
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
89 changes: 89 additions & 0 deletions drf_tools/filters.py
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)
69 changes: 69 additions & 0 deletions drf_tools/renderers.py
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()
57 changes: 57 additions & 0 deletions drf_tools/routers.py
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()
Loading

0 comments on commit f1a5028

Please sign in to comment.