├── .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 |
--------------------------------------------------------------------------------