├── .coveragerc ├── .gitignore ├── .isort.cfg ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── codecov.yml ├── docs └── index.md ├── mkdocs.yml ├── requirements ├── base.txt ├── codestyle.txt ├── packaging.txt └── tests.txt ├── rest_framework_files ├── __init__.py ├── generics.py ├── mixins.py ├── routers.py └── viewsets.py ├── runtests.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── assets │ ├── abc.csv │ ├── abc.json │ ├── abc.xlsx │ ├── abc.xml │ └── abc.yaml ├── conftest.py ├── test_app │ ├── __init__.py │ ├── models.py │ ├── serializers.py │ ├── urls.py │ └── views.py ├── test_generics │ ├── __init__.py │ ├── test_export.py │ └── test_import.py └── test_viewsets │ ├── __init__.py │ ├── test_export.py │ └── test_import.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = rest_framework_files 4 | 5 | [report] 6 | show_missing = True 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | # built site 92 | /site/ 93 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | skip=.tox 3 | atomic=true 4 | multi_line_output=5 5 | known_standard_library=types 6 | known_third_party=pytest,six,django,rest_framework,rest_framework_xml,rest_framework_csv,rest_framework_yaml 7 | known_first_party=rest_framework_files 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "2.7" 5 | - "3.5" 6 | - "3.6" 7 | 8 | sudo: false 9 | 10 | env: 11 | - TOX_ENV=django111-drf34 12 | - TOX_ENV=django111-drf35 13 | - TOX_ENV=django111-drf36 14 | - TOX_ENV=django111-drf37 15 | - TOX_ENV=django111-drf38 16 | - TOX_ENV=django20-drf37 17 | - TOX_ENV=django20-drf38 18 | 19 | matrix: 20 | fast_finish: true 21 | include: 22 | - python: "2.7" 23 | env: TOX_ENV="lint" 24 | exclude: 25 | - python: "2.7" 26 | env: TOX_ENV=django20-drf37 27 | - python: "2.7" 28 | env: TOX_ENV=django20-drf38 29 | 30 | install: 31 | - pip install tox-travis 32 | 33 | script: 34 | - tox -e $TOX_ENV 35 | 36 | after_success: 37 | - pip install codecov 38 | - codecov -e TOX_ENV 39 | 40 | notifications: 41 | email: false 42 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 1.1.0 [16-05-2018] 4 | 5 | - Python 3.6 support 6 | - DRF 3.8 support 7 | - Django 2.0 support 8 | 9 | ## 1.0.1 [20-05-2017] 10 | 11 | - Initial implementation 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Evans Murithi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | recursive-include rest_framework_files/static *.js *.css *.png *.eot *.svg *.ttf *.woff 4 | recursive-include rest_framework_files/templates *.html 5 | recursive-exclude * __pycache__ 6 | recursive-exclude * *.py[co] 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # REST framework files 2 | 3 | [![build-status-image]][travis] 4 | [![coverage-status-image]][codecov] 5 | 6 | **File download and upload support for Django REST framework.** 7 | 8 | --- 9 | 10 | # Overview 11 | 12 | REST framework files allows you to download a file in the format used to render the response and 13 | also allows creation of model instances by uploading a file containing the model fields. 14 | 15 | # Requirements 16 | 17 | * Python (2.7, 3.5, 3.6) 18 | * Django REST framework (3.4, 3.5, 3.6, 3.7, 3.8) 19 | 20 | # Installation 21 | 22 | Install using `pip`: 23 | 24 | pip install djangorestframework-files 25 | 26 | # Example 27 | 28 | *models.py* 29 | ```python 30 | from django.db import models 31 | 32 | class ABC(models.Model): 33 | name = models.CharField(max_length=255) 34 | ``` 35 | 36 | *serializers.py* 37 | ```python 38 | from rest_framework import serializers 39 | 40 | from .models import ABC 41 | 42 | class ABCSerializer(serializers.ModelSerializer): 43 | class Meta: 44 | model = ABC 45 | fields = '__all__' 46 | ``` 47 | 48 | *views.py* 49 | ```python 50 | from rest_framework.parsers import JSONParser, MultiPartParser 51 | from rest_framework_files.viewsets import ImportExportModelViewSet 52 | 53 | from .models import ABC 54 | from .serializers import ABCSerializer 55 | 56 | class ABCViewSet(ImportExportModelViewSet): 57 | queryset = ABC.objects.all() 58 | serializer_class = ABCSerializer 59 | # if filename is not provided, the view name will be used as the filename 60 | filename = 'ABC' 61 | # renderer classes used to render your content. will determine the file type of the download 62 | renderer_classes = (JSONParser, ) 63 | parser_classes = (MultiPartParser, ) 64 | # parser classes used to parse the content of the uploaded file 65 | file_content_parser_classes = (JSONParser, ) 66 | ``` 67 | 68 | Some third party packages that offer media type support: 69 | 70 | * [Parsers][parsers] 71 | * [Renderers][renderers] 72 | 73 | *urls.py* 74 | ```python 75 | from rest_framework_files import routers 76 | 77 | from .views import ABCViewSet 78 | 79 | router = routers.ImportExportRouter() 80 | router.register(r'abc', ABCViewSet) 81 | 82 | urlpatterns = router.urls 83 | ``` 84 | 85 | ## Downloading 86 | 87 | To download a `json` file you can go to the url `/abc/?format=json`. The `format` query parameter 88 | specifies the media type you want your response represented in. To download an `xml` file, your 89 | url would be `/abc/?format=xml`. For this to work, make sure you have the respective `renderers` 90 | to render your response. 91 | 92 | ## Uploading 93 | 94 | To create model instances from a file, upload a file to the url `/abc/`. Make sure the content 95 | of the file can be parsed by the parsers specified in the `file_content_parser_classes` or else 96 | it will return a `HTTP_415_UNSUPPORTED_MEDIA_TYPE` error. 97 | 98 | For sample file examples you can upload, check the [assets folder][assets] 99 | 100 | For more examples on how to use the viewsets or generic views, check the [test application][test-app] 101 | 102 | [build-status-image]: https://travis-ci.org/evansmurithi/django-rest-framework-files.svg?branch=master 103 | [travis]: https://travis-ci.org/evansmurithi/django-rest-framework-files 104 | [coverage-status-image]: https://codecov.io/gh/evansmurithi/django-rest-framework-files/branch/master/graph/badge.svg 105 | [codecov]: https://codecov.io/gh/evansmurithi/django-rest-framework-files 106 | 107 | [parsers]: http://www.django-rest-framework.org/api-guide/parsers/#third-party-packages 108 | [renderers]: http://www.django-rest-framework.org/api-guide/renderers/#third-party-packages 109 | [assets]: https://github.com/evansmurithi/django-rest-framework-files/tree/master/tests/assets 110 | [test-app]: https://github.com/evansmurithi/django-rest-framework-files/tree/master/tests/test_app 111 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: false 4 | patch: false 5 | changes: false 6 | 7 | comment: off 8 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # REST framework files 2 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: REST framework files 2 | site_description: File download and upload support for Django REST framework 3 | 4 | repo_url: https://github.com/evansmurithi/django-rest-framework-files 5 | 6 | markdown_extensions: 7 | - toc: 8 | anchorlink: True 9 | 10 | pages: 11 | - Home: 'index.md' 12 | -------------------------------------------------------------------------------- /requirements/base.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | 3 | -r codestyle.txt 4 | -r packaging.txt 5 | -r tests.txt 6 | -------------------------------------------------------------------------------- /requirements/codestyle.txt: -------------------------------------------------------------------------------- 1 | flake8==2.4.0 2 | pep8==1.5.7 3 | isort==4.2.5 4 | -------------------------------------------------------------------------------- /requirements/packaging.txt: -------------------------------------------------------------------------------- 1 | pypandoc==1.3.3 2 | wheel==0.29.0 3 | twine==1.8.1 4 | -------------------------------------------------------------------------------- /requirements/tests.txt: -------------------------------------------------------------------------------- 1 | pytest==3.0.5 2 | pytest-cov==2.4.0 3 | pytest-django==3.1.2 4 | tox==2.6.0 5 | -------------------------------------------------------------------------------- /rest_framework_files/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '1.1.0' 2 | __author__ = 'Evans Murithi' 3 | __license__ = 'MIT License' 4 | __copyright__ = 'Copyright 2017 Evans Murithi' 5 | -------------------------------------------------------------------------------- /rest_framework_files/generics.py: -------------------------------------------------------------------------------- 1 | """ 2 | Generic views that provide commonly needed behaviour. 3 | """ 4 | from __future__ import unicode_literals 5 | 6 | from rest_framework.generics import GenericAPIView 7 | 8 | from . import mixins 9 | 10 | 11 | class ImportCreateAPIView(mixins.ImportMixin, GenericAPIView): 12 | """ 13 | Concrete view for uploading a file. 14 | """ 15 | 16 | def post(self, request, *args, **kwargs): 17 | return self.upload(request, *args, **kwargs) 18 | 19 | 20 | class ExportListAPIView(mixins.ExportMixin, GenericAPIView): 21 | """ 22 | Concrete view for downloading a file. 23 | """ 24 | 25 | def get(self, request, *args, **kwargs): 26 | return self.download(request, *args, **kwargs) 27 | 28 | 29 | class ExportListImportCreateAPIView(mixins.ExportMixin, 30 | mixins.ImportMixin, 31 | GenericAPIView): 32 | """ 33 | Concrete view for downloading and uploading a file. 34 | """ 35 | 36 | def get(self, request, *args, **kwargs): 37 | return self.download(request, *args, **kwargs) 38 | 39 | def post(self, request, *args, **kwargs): 40 | return self.upload(request, *args, **kwargs) 41 | -------------------------------------------------------------------------------- /rest_framework_files/mixins.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import six 4 | from django.utils.datastructures import MultiValueDictKeyError 5 | from rest_framework.exceptions import ParseError, ValidationError 6 | from rest_framework.mixins import CreateModelMixin 7 | from rest_framework.response import Response 8 | from rest_framework.status import HTTP_200_OK, HTTP_201_CREATED 9 | 10 | 11 | class ImportMixin(CreateModelMixin): 12 | """ 13 | Upload a file and parse its content. 14 | """ 15 | 16 | def upload(self, request, *args, **kwargs): 17 | try: 18 | uploaded_file = request.data['file'] 19 | except MultiValueDictKeyError: 20 | raise MultiValueDictKeyError("Upload a file with the key 'file'") 21 | 22 | content = b'' 23 | for chunk in uploaded_file.chunks(): 24 | content += chunk 25 | 26 | # try parsers defined in the `file_content_parser_classes` 27 | for parser_cls in self.file_content_parser_classes: 28 | try: 29 | data = parser_cls().parse(six.BytesIO(content)) 30 | break 31 | except (ParseError, ValidationError): 32 | # try all parsers provided 33 | continue 34 | else: 35 | raise ParseError( 36 | 'Could not parse content of the file to any of the parsers ' 37 | 'provided.' 38 | ) 39 | 40 | # create model instances from contents of the file 41 | serializer = self.get_serializer(data=data, many=True) 42 | serializer.is_valid(raise_exception=True) 43 | self.perform_create(serializer) 44 | headers = self.get_success_headers(serializer.data) 45 | return Response( 46 | data=serializer.data, status=HTTP_201_CREATED, headers=headers 47 | ) 48 | 49 | 50 | class ExportMixin(object): 51 | """ 52 | Download a rendered serialized response. 53 | """ 54 | 55 | def download(self, request, *args, **kwargs): 56 | qs = self.filter_queryset(self.get_queryset()) 57 | queryset = self.paginate_queryset(qs) or qs 58 | serializer = self.get_serializer(queryset, many=True) 59 | 60 | filename = getattr(self, 'filename', self.get_view_name()) 61 | extension = self.get_content_negotiator().select_renderer( 62 | request, self.renderer_classes 63 | )[0].format 64 | 65 | return Response( 66 | data=serializer.data, status=HTTP_200_OK, 67 | headers={ 68 | 'content-disposition': ( 69 | 'attachment; filename="{}.{}"'.format(filename, extension) 70 | ) 71 | } 72 | ) 73 | -------------------------------------------------------------------------------- /rest_framework_files/routers.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import copy 4 | 5 | from rest_framework.routers import DefaultRouter, SimpleRouter 6 | 7 | 8 | class ImportExportRouter(DefaultRouter): 9 | """ 10 | Map http methods to actions defined on the import-export mixins. 11 | """ 12 | 13 | routes = copy.deepcopy(SimpleRouter.routes) 14 | routes[0].mapping.update({ 15 | 'get': 'download', 16 | 'post': 'upload', 17 | }) 18 | -------------------------------------------------------------------------------- /rest_framework_files/viewsets.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet 4 | 5 | from . import mixins 6 | 7 | 8 | class ReadOnlyImportExportModelViewSet(mixins.ExportMixin, 9 | ReadOnlyModelViewSet): 10 | pass 11 | 12 | 13 | class ImportExportModelViewSet(mixins.ImportMixin, 14 | mixins.ExportMixin, 15 | ModelViewSet): 16 | pass 17 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | from __future__ import print_function 3 | 4 | import os 5 | import subprocess 6 | import sys 7 | 8 | import pytest 9 | 10 | PYTEST_ARGS = { 11 | 'default': ['tests', '--tb=short', '-s', '-rw'], 12 | 'fast': ['tests', '--tb=short', '-q', '-s', '-rw'], 13 | } 14 | 15 | FLAKE8_ARGS = ['rest_framework_files', 'tests', '--ignore=E501'] 16 | 17 | ISORT_ARGS = ['--recursive', '--check-only', '-o' 'uritemplate', '-p', 'tests', 'rest_framework_files', 'tests'] 18 | 19 | sys.path.append(os.path.dirname(__file__)) 20 | 21 | 22 | def exit_on_failure(ret, message=None): 23 | if ret: 24 | sys.exit(ret) 25 | 26 | 27 | def flake8_main(args): 28 | print('Running flake8 code linting') 29 | ret = subprocess.call(['flake8'] + args) 30 | print('flake8 failed' if ret else 'flake8 passed') 31 | return ret 32 | 33 | 34 | def isort_main(args): 35 | print('Running isort code checking') 36 | ret = subprocess.call(['isort'] + args) 37 | 38 | if ret: 39 | print('isort failed: Some modules have incorrectly ordered imports. Fix by running `isort --recursive .`') 40 | else: 41 | print('isort passed') 42 | 43 | return ret 44 | 45 | 46 | def split_class_and_function(string): 47 | class_string, function_string = string.split('.', 1) 48 | return "%s and %s" % (class_string, function_string) 49 | 50 | 51 | def is_function(string): 52 | # `True` if it looks like a test function is included in the string. 53 | return string.startswith('test_') or '.test_' in string 54 | 55 | 56 | def is_class(string): 57 | # `True` if first character is uppercase - assume it's a class name. 58 | return string[0] == string[0].upper() 59 | 60 | 61 | if __name__ == "__main__": 62 | try: 63 | sys.argv.remove('--nolint') 64 | except ValueError: 65 | run_flake8 = True 66 | run_isort = True 67 | else: 68 | run_flake8 = False 69 | run_isort = False 70 | 71 | try: 72 | sys.argv.remove('--lintonly') 73 | except ValueError: 74 | run_tests = True 75 | else: 76 | run_tests = False 77 | 78 | try: 79 | sys.argv.remove('--fast') 80 | except ValueError: 81 | style = 'default' 82 | else: 83 | style = 'fast' 84 | run_flake8 = False 85 | run_isort = False 86 | 87 | if len(sys.argv) > 1: 88 | pytest_args = sys.argv[1:] 89 | first_arg = pytest_args[0] 90 | 91 | try: 92 | pytest_args.remove('--coverage') 93 | except ValueError: 94 | pass 95 | else: 96 | pytest_args = [ 97 | '--cov-report', 98 | 'xml', 99 | '--cov', 100 | 'rest_framework_files'] + pytest_args 101 | 102 | if first_arg.startswith('-'): 103 | # `runtests.py [flags]` 104 | pytest_args = ['tests'] + pytest_args 105 | elif is_class(first_arg) and is_function(first_arg): 106 | # `runtests.py TestCase.test_function [flags]` 107 | expression = split_class_and_function(first_arg) 108 | pytest_args = ['tests', '-k', expression] + pytest_args[1:] 109 | elif is_class(first_arg) or is_function(first_arg): 110 | # `runtests.py TestCase [flags]` 111 | # `runtests.py test_function [flags]` 112 | pytest_args = ['tests', '-k', pytest_args[0]] + pytest_args[1:] 113 | else: 114 | pytest_args = PYTEST_ARGS[style] 115 | 116 | if run_tests: 117 | exit_on_failure(pytest.main(pytest_args)) 118 | 119 | if run_flake8: 120 | exit_on_failure(flake8_main(FLAKE8_ARGS)) 121 | 122 | if run_isort: 123 | exit_on_failure(isort_main(ISORT_ARGS)) 124 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import re 6 | 7 | from setuptools import setup 8 | 9 | try: 10 | from pypandoc import convert_file 11 | 12 | def read_md(f): 13 | return convert_file(f, 'rst') 14 | 15 | except ImportError: 16 | print( 17 | "warning: pypandoc module not found, could not convert Markdown to RST" 18 | ) 19 | 20 | def read_md(f): 21 | return open(f, 'r').read() 22 | 23 | 24 | def get_version(package): 25 | """ 26 | Return package version as listed in `__version__` in `__init__.py`. 27 | """ 28 | init_py = open(os.path.join(package, '__init__.py')).read() 29 | return re.search("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1) 30 | 31 | 32 | def get_packages(package): 33 | """ 34 | Return root package and all sub-packages. 35 | """ 36 | return [ 37 | dirpath for dirpath, dirnames, filenames in os.walk(package) 38 | if os.path.exists(os.path.join(dirpath, '__init__.py')) 39 | ] 40 | 41 | 42 | package = 'rest_framework_files' 43 | version = get_version(package) 44 | 45 | 46 | setup( 47 | name='djangorestframework-files', 48 | version=version, 49 | url='https://github.com/evansmurithi/django-rest-framework-files', 50 | license='MIT License', 51 | description='File download and upload support for Django REST framework', 52 | long_description=read_md('README.md'), 53 | author='Evans Murithi', 54 | author_email='murithievans80@gmail.com', 55 | packages=get_packages(package), 56 | install_requires=[], 57 | classifiers=[ 58 | 'Development Status :: 4 - Beta', 59 | 'Environment :: Web Environment', 60 | 'Framework :: Django', 61 | 'Framework :: Django :: 1.11', 62 | 'Framework :: Django :: 2.0', 63 | 'Intended Audience :: Developers', 64 | 'License :: OSI Approved :: MIT License', 65 | 'Operating System :: OS Independent', 66 | 'Programming Language :: Python', 67 | 'Programming Language :: Python :: 2', 68 | 'Programming Language :: Python :: 2.7', 69 | 'Programming Language :: Python :: 3', 70 | 'Programming Language :: Python :: 3.5', 71 | 'Programming Language :: Python :: 3.6', 72 | 'Topic :: Internet :: WWW/HTTP', 73 | ] 74 | ) 75 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evansmurithi/django-rest-framework-files/7de44be6c6eca4700524c184d7f83dbb162d2ba4/tests/__init__.py -------------------------------------------------------------------------------- /tests/assets/abc.csv: -------------------------------------------------------------------------------- 1 | "name" 2 | "giraffe" 3 | "leopard" 4 | "rhino" 5 | -------------------------------------------------------------------------------- /tests/assets/abc.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"name": "lion"}, 3 | {"name": "zebra"} 4 | ] 5 | -------------------------------------------------------------------------------- /tests/assets/abc.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evansmurithi/django-rest-framework-files/7de44be6c6eca4700524c184d7f83dbb162d2ba4/tests/assets/abc.xlsx -------------------------------------------------------------------------------- /tests/assets/abc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | cheetah 5 | 6 | 7 | gazelle 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/assets/abc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - 3 | name: bufallo 4 | - 5 | name: elephant 6 | - 7 | name: hyena 8 | - 9 | name: crocodile 10 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def pytest_configure(): 5 | from django.conf import settings 6 | 7 | MIDDLEWARE = ( 8 | 'django.middleware.common.CommonMiddleware', 9 | 'django.contrib.sessions.middleware.SessionMiddleware', 10 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 11 | 'django.contrib.messages.middleware.MessageMiddleware', 12 | ) 13 | 14 | settings.configure( 15 | BASE_DIR=os.path.dirname(os.path.abspath(__file__)), 16 | DEBUG_PROPAGATE_EXCEPTIONS=True, 17 | DATABASES={ 18 | 'default': { 19 | 'ENGINE': 'django.db.backends.sqlite3', 20 | 'NAME': ':memory:' 21 | } 22 | }, 23 | SITE_ID=1, 24 | SECRET_KEY='not very secret in tests', 25 | USE_I18N=True, 26 | USE_L10N=True, 27 | STATIC_URL='/static/', 28 | ROOT_URLCONF='tests.test_app.urls', 29 | TEMPLATES=[ 30 | { 31 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 32 | 'APP_DIRS': True, 33 | }, 34 | ], 35 | MIDDLEWARE=MIDDLEWARE, 36 | MIDDLEWARE_CLASSES=MIDDLEWARE, 37 | INSTALLED_APPS=( 38 | 'django.contrib.auth', 39 | 'django.contrib.contenttypes', 40 | 'django.contrib.sessions', 41 | 'django.contrib.sites', 42 | 'django.contrib.staticfiles', 43 | 'rest_framework', 44 | 'rest_framework.authtoken', 45 | 'tests.test_app', 46 | ), 47 | PASSWORD_HASHERS=( 48 | 'django.contrib.auth.hashers.MD5PasswordHasher', 49 | ), 50 | ) 51 | 52 | try: 53 | import django 54 | django.setup() 55 | except AttributeError: 56 | pass 57 | -------------------------------------------------------------------------------- /tests/test_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evansmurithi/django-rest-framework-files/7de44be6c6eca4700524c184d7f83dbb162d2ba4/tests/test_app/__init__.py -------------------------------------------------------------------------------- /tests/test_app/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.db import models 4 | 5 | 6 | class ABC(models.Model): 7 | 8 | name = models.CharField(max_length=100) 9 | -------------------------------------------------------------------------------- /tests/test_app/serializers.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from rest_framework import serializers 4 | 5 | from .models import ABC 6 | 7 | 8 | class ABCSerializer(serializers.ModelSerializer): 9 | 10 | class Meta(object): 11 | model = ABC 12 | fields = '__all__' 13 | -------------------------------------------------------------------------------- /tests/test_app/urls.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.conf.urls import url 4 | 5 | from rest_framework_files import routers 6 | 7 | from . import views 8 | 9 | router = routers.ImportExportRouter() 10 | router.register(r'abc', views.ABCViewSet) 11 | router.register(r'def', views.DEFViewSet) 12 | 13 | urlpatterns = router.urls 14 | 15 | urlpatterns += ( 16 | url( 17 | r'^abc_import/$', views.ABCImportView.as_view(), 18 | name='abc_import' 19 | ), 20 | url( 21 | r'^abc_export/$', views.ABCExportView.as_view(), 22 | name='abc_export' 23 | ), 24 | url( 25 | r'^abc_import_export/$', views.ABCImportExportView.as_view(), 26 | name='abc_import_export' 27 | ), 28 | ) 29 | -------------------------------------------------------------------------------- /tests/test_app/views.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from rest_framework.parsers import JSONParser, MultiPartParser 4 | from rest_framework.renderers import JSONRenderer 5 | from rest_framework_csv.parsers import CSVParser 6 | from rest_framework_csv.renderers import CSVRenderer 7 | from rest_framework_xml.parsers import XMLParser 8 | from rest_framework_xml.renderers import XMLRenderer 9 | from rest_framework_yaml.parsers import YAMLParser 10 | from rest_framework_yaml.renderers import YAMLRenderer 11 | 12 | from rest_framework_files.generics import ( 13 | ExportListAPIView, ExportListImportCreateAPIView, ImportCreateAPIView 14 | ) 15 | from rest_framework_files.viewsets import ImportExportModelViewSet 16 | 17 | from .models import ABC 18 | from .serializers import ABCSerializer 19 | 20 | 21 | class ABCViewSet(ImportExportModelViewSet): 22 | """ 23 | Test use of model viewset. 24 | """ 25 | 26 | queryset = ABC.objects.all() 27 | serializer_class = ABCSerializer 28 | parser_classes = (MultiPartParser, ) 29 | renderer_classes = ( 30 | JSONRenderer, XMLRenderer, CSVRenderer, YAMLRenderer, 31 | ) 32 | file_content_parser_classes = ( 33 | JSONParser, XMLParser, YAMLParser, CSVParser, 34 | ) 35 | 36 | 37 | class DEFViewSet(ImportExportModelViewSet): 38 | """ 39 | Test use of ``filename`` attribute during download. 40 | """ 41 | 42 | queryset = ABC.objects.all() 43 | serializer_class = ABCSerializer 44 | renderer_classes = ( 45 | JSONRenderer, XMLRenderer, CSVRenderer, YAMLRenderer, 46 | ) 47 | filename = "My file" 48 | 49 | 50 | class ABCImportView(ImportCreateAPIView): 51 | """ 52 | Test use of import generic view. 53 | """ 54 | 55 | queryset = ABC.objects.all() 56 | serializer_class = ABCSerializer 57 | parser_classes = (MultiPartParser, ) 58 | file_content_parser_classes = (CSVParser, ) 59 | filename = "ABC" 60 | 61 | 62 | class ABCExportView(ExportListAPIView): 63 | """ 64 | Test use of export generic view. 65 | """ 66 | 67 | queryset = ABC.objects.all() 68 | serializer_class = ABCSerializer 69 | renderer_classes = (JSONRenderer, ) 70 | filename = "ABC" 71 | 72 | 73 | class ABCImportExportView(ExportListImportCreateAPIView): 74 | """ 75 | Test use of generic views. 76 | """ 77 | 78 | queryset = ABC.objects.all() 79 | serializer_class = ABCSerializer 80 | parser_classes = (MultiPartParser, ) 81 | renderer_classes = (YAMLRenderer, ) 82 | file_content_parser_classes = (XMLParser, ) 83 | filename = "ABC" 84 | -------------------------------------------------------------------------------- /tests/test_generics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evansmurithi/django-rest-framework-files/7de44be6c6eca4700524c184d7f83dbb162d2ba4/tests/test_generics/__init__.py -------------------------------------------------------------------------------- /tests/test_generics/test_export.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from rest_framework.test import APITestCase 4 | 5 | from tests.test_app.models import ABC 6 | 7 | 8 | class TestExportGenericViews(APITestCase): 9 | 10 | def setUp(self): 11 | """ 12 | Create records in ABC model. 13 | """ 14 | names = ['me', 'you', 'him', 'her'] 15 | for name in names: 16 | ABC.objects.create(name=name) 17 | 18 | def test_download_using_export_generic_view(self): 19 | """ 20 | Download a file using generic view. 21 | """ 22 | response = self.client.get('/abc_export/?format=json') 23 | content = ( 24 | b'[{"id":1,"name":"me"},{"id":2,"name":"you"},' 25 | b'{"id":3,"name":"him"},{"id":4,"name":"her"}]' 26 | ) 27 | self.assertEqual(response.status_code, 200) 28 | self.assertEqual(response.content, content) 29 | self.assertEqual( 30 | response._headers.get('content-type'), 31 | ('Content-Type', 'application/json') 32 | ) 33 | self.assertEqual( 34 | response._headers.get('content-disposition'), 35 | ('content-disposition', 'attachment; filename="ABC.json"') 36 | ) 37 | 38 | def test_download_using_import_export_generic_view(self): 39 | """ 40 | Download a file using generic view. 41 | """ 42 | response = self.client.get('/abc_import_export/?format=yaml') 43 | content = ( 44 | b'- id: 1\n name: me\n- id: 2\n name: you\n' 45 | b'- id: 3\n name: him\n- id: 4\n name: her\n' 46 | ) 47 | self.assertEqual(response.status_code, 200) 48 | self.assertEqual(response.content, content) 49 | self.assertEqual( 50 | response._headers.get('content-type'), 51 | ('Content-Type', 'application/yaml; charset=utf-8') 52 | ) 53 | self.assertEqual( 54 | response._headers.get('content-disposition'), 55 | ('content-disposition', 'attachment; filename="ABC.yaml"') 56 | ) 57 | -------------------------------------------------------------------------------- /tests/test_generics/test_import.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import os 4 | 5 | from django.conf import settings 6 | from rest_framework.test import APITestCase 7 | 8 | from tests.test_app.models import ABC 9 | 10 | 11 | class TestImportGenericViews(APITestCase): 12 | 13 | assets_dir = os.path.join(settings.BASE_DIR, 'assets/') 14 | 15 | def upload_file(self, filename, url='/abc/'): 16 | with open(self.assets_dir + filename, 'rb') as f: 17 | response = self.client.post(url, {'file': f}) 18 | 19 | return response 20 | 21 | def test_upload_using_import_generic_view(self): 22 | """ 23 | Upload a file and create model instances using generic view. 24 | """ 25 | response = self.upload_file('abc.csv', '/abc_import/') 26 | self.assertEqual(response.status_code, 201) 27 | self.assertEqual(ABC.objects.count(), 3) 28 | self.assertTrue(ABC.objects.filter(name='giraffe').exists()) 29 | 30 | def test_upload_using_import_export_generic_view(self): 31 | """ 32 | Upload a file and create model instances using generic view. 33 | """ 34 | response = self.upload_file('abc.xml', '/abc_import_export/') 35 | self.assertEqual(response.status_code, 201) 36 | self.assertEqual(ABC.objects.count(), 2) 37 | self.assertTrue(ABC.objects.filter(name='cheetah').exists()) 38 | -------------------------------------------------------------------------------- /tests/test_viewsets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evansmurithi/django-rest-framework-files/7de44be6c6eca4700524c184d7f83dbb162d2ba4/tests/test_viewsets/__init__.py -------------------------------------------------------------------------------- /tests/test_viewsets/test_export.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from rest_framework.test import APITestCase 4 | 5 | from tests.test_app.models import ABC 6 | 7 | 8 | class TestExportViewsets(APITestCase): 9 | 10 | def setUp(self): 11 | """ 12 | Create records in ABC model. 13 | """ 14 | names = ['me', 'you', 'him', 'her'] 15 | for name in names: 16 | ABC.objects.create(name=name) 17 | 18 | def test_download_file_name(self): 19 | """ 20 | Filename in viewset is the name of the file to be downloaded. 21 | """ 22 | response = self.client.get('/def/?format=json') 23 | self.assertEqual(response.status_code, 200) 24 | self.assertEqual( 25 | response._headers.get('content-disposition'), 26 | ('content-disposition', 'attachment; filename="My file.json"') 27 | ) 28 | 29 | def test_download_json(self): 30 | """ 31 | Response rendered in json, should output a json file when requested. 32 | """ 33 | response = self.client.get('/abc/?format=json') 34 | content = ( 35 | b'[{"id":1,"name":"me"},{"id":2,"name":"you"},' 36 | b'{"id":3,"name":"him"},{"id":4,"name":"her"}]' 37 | ) 38 | self.assertEqual(response.status_code, 200) 39 | self.assertEqual(response.content, content) 40 | self.assertEqual( 41 | response._headers.get('content-type'), 42 | ('Content-Type', 'application/json') 43 | ) 44 | self.assertEqual( 45 | response._headers.get('content-disposition'), 46 | ('content-disposition', 'attachment; filename="Abc List.json"') 47 | ) 48 | 49 | def test_download_xml(self): 50 | """ 51 | Response rendered in xml, should output a xml file when requested. 52 | """ 53 | response = self.client.get('/abc/?format=xml') 54 | content = ( 55 | b'\n' 56 | b'1me' 57 | b'2you' 58 | b'3him' 59 | b'4her' 60 | ) 61 | self.assertEqual(response.status_code, 200) 62 | self.assertEqual(response.content, content) 63 | self.assertEqual( 64 | response._headers.get('content-type'), 65 | ('Content-Type', 'application/xml; charset=utf-8') 66 | ) 67 | self.assertEqual( 68 | response._headers.get('content-disposition'), 69 | ('content-disposition', 'attachment; filename="Abc List.xml"') 70 | ) 71 | 72 | def test_download_csv(self): 73 | """ 74 | Response rendered in csv, should output a csv file when requested. 75 | """ 76 | response = self.client.get('/abc/?format=csv') 77 | content = b'id,name\r\n1,me\r\n2,you\r\n3,him\r\n4,her\r\n' 78 | self.assertEqual(response.status_code, 200) 79 | self.assertEqual(response.content, content) 80 | self.assertEqual( 81 | response._headers.get('content-type'), 82 | ('Content-Type', 'text/csv; charset=utf-8') 83 | ) 84 | self.assertEqual( 85 | response._headers.get('content-disposition'), 86 | ('content-disposition', 'attachment; filename="Abc List.csv"') 87 | ) 88 | 89 | def test_download_yaml(self): 90 | """ 91 | Response rendered in yaml, should output a yaml file when requested. 92 | """ 93 | response = self.client.get('/abc/?format=yaml') 94 | content = ( 95 | b'- id: 1\n name: me\n- id: 2\n name: you\n' 96 | b'- id: 3\n name: him\n- id: 4\n name: her\n' 97 | ) 98 | self.assertEqual(response.status_code, 200) 99 | self.assertEqual(response.content, content) 100 | self.assertEqual( 101 | response._headers.get('content-type'), 102 | ('Content-Type', 'application/yaml; charset=utf-8') 103 | ) 104 | self.assertEqual( 105 | response._headers.get('content-disposition'), 106 | ('content-disposition', 'attachment; filename="Abc List.yaml"') 107 | ) 108 | -------------------------------------------------------------------------------- /tests/test_viewsets/test_import.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import os 4 | 5 | from django.conf import settings 6 | from django.utils.datastructures import MultiValueDictKeyError 7 | from rest_framework.test import APITestCase 8 | 9 | from tests.test_app.models import ABC 10 | 11 | 12 | class TestImportViewsets(APITestCase): 13 | 14 | assets_dir = os.path.join(settings.BASE_DIR, 'assets/') 15 | 16 | def upload_file(self, filename): 17 | with open(self.assets_dir + filename, 'rb') as f: 18 | response = self.client.post('/abc/', {'file': f}) 19 | 20 | return response 21 | 22 | def test_upload_with_wrong_key(self): 23 | """ 24 | Should throw error while uploading with wrong key 25 | """ 26 | with open(self.assets_dir + 'abc.json', 'rb') as f, \ 27 | self.assertRaises(MultiValueDictKeyError) as err: 28 | self.client.post('/abc/', {'wrong_key': f}) 29 | self.assertIn( 30 | "Upload a file with the key 'file'", err.exception.args 31 | ) 32 | 33 | def test_upload_json_file(self): 34 | """ 35 | Should create model instances from the json file uploaded. 36 | """ 37 | response = self.upload_file('abc.json') 38 | self.assertEqual(response.status_code, 201) 39 | self.assertEqual(ABC.objects.count(), 2) 40 | self.assertTrue(ABC.objects.filter(name='lion').exists()) 41 | 42 | def test_upload_xml_file(self): 43 | """ 44 | Should create model instances from the xml file uploaded. 45 | """ 46 | response = self.upload_file('abc.xml') 47 | self.assertEqual(response.status_code, 201) 48 | self.assertEqual(ABC.objects.count(), 2) 49 | self.assertTrue(ABC.objects.filter(name='gazelle').exists()) 50 | 51 | def test_upload_csv_file(self): 52 | """ 53 | Should create model instances from the csv file uploaded. 54 | """ 55 | response = self.upload_file('abc.csv') 56 | self.assertEqual(response.status_code, 201) 57 | self.assertEqual(ABC.objects.count(), 3) 58 | self.assertTrue(ABC.objects.filter(name='leopard').exists()) 59 | 60 | def test_upload_yaml_file(self): 61 | """ 62 | Should create model instances from the yaml file uploaded. 63 | """ 64 | response = self.upload_file('abc.yaml') 65 | self.assertEqual(response.status_code, 201) 66 | self.assertEqual(ABC.objects.count(), 4) 67 | self.assertTrue(ABC.objects.filter(name='hyena').exists()) 68 | 69 | def test_upload_unsupported_file_type(self): 70 | """ 71 | Should give an error when uploading an unsupported file type. 72 | """ 73 | response = self.upload_file('abc.xlsx') 74 | self.assertEqual(response.status_code, 400) 75 | self.assertEqual( 76 | response.data['detail'], 77 | ( 78 | 'Could not parse content of the file to any of the parsers ' 79 | 'provided.' 80 | ) 81 | ) 82 | self.assertEqual(ABC.objects.count(), 0) 83 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{27,35,36}-django{111}-drf{34,35,36,37,38}, 4 | py{35,36}-django{20}-drf{37,38}, 5 | lint 6 | 7 | [flake8] 8 | exclude = .tox,docs 9 | 10 | [testenv] 11 | commands = ./runtests.py --fast {posargs} --coverage -rw 12 | setenv = 13 | PYTHONDONTWRITEBYTECODE=1 14 | PYTHONWARNINGS=once 15 | deps = 16 | django111: Django>=1.11,<2.0 17 | django20: Django>=2.0,<2.1 18 | drf34: djangorestframework>=3.4.0,<3.5.0 19 | drf35: djangorestframework>=3.5.0,<3.6.0 20 | drf36: djangorestframework>=3.6.0,<3.7.0 21 | drf37: djangorestframework>=3.7.0,<3.8.0 22 | drf38: djangorestframework>=3.8.0,<3.9.0 23 | djangorestframework-xml==1.3.0 24 | djangorestframework-csv==2.1.0 25 | djangorestframework-yaml==1.0.3 26 | -rrequirements/tests.txt 27 | 28 | [testenv:lint] 29 | basepython = python2.7 30 | commands = ./runtests.py --lintonly 31 | deps = 32 | -rrequirements/codestyle.txt 33 | -rrequirements/tests.txt 34 | --------------------------------------------------------------------------------