├── .editorconfig ├── .flake8 ├── .github └── workflows │ └── test.yml ├── .gitignore ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE.md ├── LICENSE.txt ├── MANIFEST.in ├── Makefile ├── Procfile ├── README.md ├── README.rst ├── benchmarks.html ├── benchmarks ├── __init__.py ├── drest.py ├── drf.py ├── models.py ├── settings.py ├── test_bench.py └── urls.py ├── docs ├── Makefile ├── _modules │ ├── dynamic_rest │ │ ├── bases.html │ │ ├── conf.html │ │ ├── datastructures.html │ │ ├── fields.html │ │ ├── filters.html │ │ ├── links.html │ │ ├── meta.html │ │ ├── metadata.html │ │ ├── pagination.html │ │ ├── patches.html │ │ ├── processors.html │ │ ├── renderers.html │ │ ├── routers.html │ │ ├── serializers.html │ │ ├── tagged.html │ │ └── viewsets.html │ └── index.html ├── _sources │ ├── dynamic_rest.txt │ ├── index.txt │ └── tutorial.txt ├── _static │ ├── ajax-loader.gif │ ├── alabaster.css │ ├── basic.css │ ├── comment-bright.png │ ├── comment-close.png │ ├── comment.png │ ├── css │ │ ├── badge_only.css │ │ └── theme.css │ ├── doctools.js │ ├── down-pressed.png │ ├── down.png │ ├── file.png │ ├── fonts │ │ ├── Inconsolata-Bold.ttf │ │ ├── Inconsolata-Regular.ttf │ │ ├── Lato-Bold.ttf │ │ ├── Lato-Regular.ttf │ │ ├── RobotoSlab-Bold.ttf │ │ ├── RobotoSlab-Regular.ttf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ └── fontawesome-webfont.woff │ ├── jquery-1.11.1.js │ ├── jquery.js │ ├── js │ │ ├── modernizr.min.js │ │ └── theme.js │ ├── minus.png │ ├── plus.png │ ├── pygments.css │ ├── searchtools.js │ ├── underscore-1.3.1.js │ ├── underscore.js │ ├── up-pressed.png │ ├── up.png │ └── websupport.js ├── conf.py ├── dynamic_rest.html ├── dynamic_rest.rst ├── genindex.html ├── index.html ├── index.rst ├── make.bat ├── objects.inv ├── py-modindex.html ├── search.html ├── searchindex.js ├── tutorial.html └── tutorial.rst ├── dynamic_rest ├── __init__.py ├── apps.py ├── bases.py ├── blueprints │ ├── api │ │ ├── __init__.py │ │ ├── context.py │ │ └── templates │ │ │ └── {{app}} │ │ │ ├── __init__.py.j2 │ │ │ └── api │ │ │ ├── __init__.py.j2 │ │ │ ├── urls.py.j2 │ │ │ └── {{version}} │ │ │ ├── __init__.py.j2 │ │ │ ├── serializers │ │ │ ├── __init__.py.j2 │ │ │ ├── base.py.j2 │ │ │ └── {{name}}.py.j2 │ │ │ ├── urls.py.j2 │ │ │ └── views │ │ │ ├── __init__.py.j2 │ │ │ ├── base.py.j2 │ │ │ └── {{name}}.py.j2 │ └── init │ │ ├── __init__.py │ │ ├── context.py │ │ └── templates │ │ └── {{app}} │ │ └── settings.py.j2 ├── conf.py ├── constants.py ├── datastructures.py ├── fields │ ├── __init__.py │ ├── common.py │ ├── fields.py │ └── generic.py ├── filters.py ├── links.py ├── meta.py ├── metadata.py ├── pagination.py ├── paginator.py ├── patches.py ├── prefetch.py ├── processors.py ├── related.py ├── renderers.py ├── routers.py ├── serializers.py ├── static │ └── dynamic_rest │ │ └── css │ │ └── default.css ├── tagged.py ├── templates │ └── dynamic_rest │ │ └── api.html ├── utils.py └── viewsets.py ├── images ├── benchmark-cubic.png ├── benchmark-linear.png ├── benchmark-quadratic.png └── directory.png ├── install_requires.txt ├── manage.py ├── pytest.ini ├── requirements.benchmark.txt ├── requirements.txt ├── runtests.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── integration │ ├── __init__.py │ └── test_blueprints.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── initialize_fixture.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20160310_1052.py │ ├── 0003_auto_20160401_1656.py │ ├── 0004_user_is_dead.py │ ├── 0005_auto_20170712_0759.py │ ├── 0006_auto_20210921_1026.py │ └── __init__.py ├── models.py ├── serializers.py ├── settings.py ├── setup.py ├── test_api.py ├── test_fields.py ├── test_generic.py ├── test_meta.py ├── test_prefetch.py ├── test_prefetch2.py ├── test_router.py ├── test_serializers.py ├── test_utils.py ├── test_viewsets.py ├── urls.py └── viewsets.py └── tox.ini /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 4 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | 22 | [Makefile] 23 | indent_style = tab 24 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length=90 3 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | python-version: [3.7, 3.8, 3.9, "3.10", "3.11"] 16 | dj-version: ["3.2.*", "4.0.*", "4.1.*", "4.2.*"] 17 | drf-version: ["3.13.*", "3.14.*", "3.15.*"] 18 | exclude: 19 | - python-version: 3.7 20 | dj-version: '4.0.*' 21 | - python-version: 3.7 22 | dj-version: '4.1.*' 23 | - python-version: 3.7 24 | dj-version: '4.2.*' 25 | - dj-version: '4.2.*' 26 | drf-version: '3.13.*' 27 | 28 | steps: 29 | - uses: actions/checkout@v2 30 | - name: Set up Python ${{ matrix.python-version }} 31 | uses: actions/setup-python@v2 32 | with: 33 | python-version: ${{ matrix.python-version }} 34 | 35 | - name: Install Dependencies 36 | run: | 37 | python -m pip install --upgrade pip 38 | pip install -r requirements.txt 39 | pip install 'django==${{ matrix.dj-version }}' 'djangorestframework==${{ matrix.drf-version }}' 40 | 41 | - name: Run Tests 42 | run: | 43 | ./runtests.py --fast --coverage -rw 44 | 45 | lint: 46 | runs-on: ubuntu-latest 47 | 48 | steps: 49 | - uses: actions/checkout@v2 50 | 51 | - name: Set up Python39 52 | uses: actions/setup-python@v2 53 | with: 54 | python-version: 3.9 55 | 56 | - name: Install Dependencies 57 | run: | 58 | python -m pip install --upgrade pip 59 | pip install flake8==5.0.4 60 | 61 | - name: Lint Code 62 | run: | 63 | flake8 dynamic_rest tests 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | .build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | *.eggs/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .cache 44 | .pytest_cache 45 | nosetests.xml 46 | coverage.xml 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | # test database 62 | db.sqlite3 63 | 64 | # pyenv 65 | .python-version 66 | 67 | # pypi 68 | .pypirc 69 | 70 | # PyCharm 71 | .idea/ 72 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | We encourage bug reports, suggestions for improvements, and direct contributions through Github Issues/Pull Requests. 2 | 3 | When making contributions, try to follow these guidelines: 4 | 5 | # Development 6 | 7 | ## Style 8 | 9 | Use `make lint` to check your code for style violations. 10 | 11 | We use the `flake8` linter to enforce PEP8 code style. 12 | For additional details, see our [Python style guide](https://github.com/AltSchool/Python). 13 | 14 | ## Documentation 15 | 16 | Use `make docs` to generate the automated documentation for the project. 17 | 18 | We recommend documenting all public modules, classes, and methods, but generating the documentation is not required. 19 | 20 | ## Tests 21 | 22 | Use `make test` to lint and run all unit tests (runs in a few seconds). 23 | Use `make tox` to run all unit tests against all supported combinations of Python, Django, and Django REST Framework (can take several minutes). 24 | 25 | We recommend linting regularly, testing with every commit, and running tests against all combinations before submitting a pull request. 26 | 27 | ## Benchmarks 28 | 29 | Use `make benchmark` to benchmark your changes against the latest version of Django REST Framework (can take several minutes). 30 | 31 | We recommend running this before submitting a pull request. Doing so will create a [benchmarks.html](benchmarks.html) file in the repository root directory. 32 | 33 | # Submission 34 | 35 | Please submit your pull request with a clear title and description. 36 | Any visual changes (e.g. to the Browsable API) should include screenshots in the description. 37 | Any related issues in Dynamic REST, Django REST Framework, or Django should include a URL reference to the issue. 38 | 39 | # Publishing 40 | 41 | (PyPi and repository write access required) 42 | 43 | Before releasing: 44 | 45 | - Check/update the version in `dynamic_rest/constants.py` 46 | - Commit changes and tag the commit with the version, prefixed by "v" 47 | - Run `make pypi_upload_test` to upload a new version to PyPiTest. Check the contents at https://pypitest.python.org/pypi/dynamic-rest 48 | - Run `make pypi_upload` to upload a new version to PyPi. Check the contents at https://pypi.python.org/pypi/dynamic-rest 49 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:xenial 2 | ENV DEBIAN_FRONTEND noninteractive 3 | ENV TERM=xterm 4 | 5 | 6 | RUN apt-get -y --force-yes update 7 | RUN apt-get -y --force-yes install locales 8 | 9 | 10 | # Set the locale 11 | RUN locale-gen en_US.UTF-8 12 | ENV LANG en_US.UTF-8 13 | ENV LANGUAGE en_US:en 14 | ENV LC_ALL en_US.UTF-8 15 | 16 | # Upgrade packages 17 | RUN apt-get -y --force-yes upgrade 18 | RUN apt-get -y --force-yes install software-properties-common curl git wget unzip nano build-essential autoconf libxml2-dev libssl-dev libbz2-dev libcurl3-dev libjpeg-dev libpng-dev libfreetype6-dev libgmp3-dev libc-client-dev libldap2-dev libmcrypt-dev libmhash-dev freetds-dev libz-dev ncurses-dev libpcre3-dev libsqlite-dev libaspell-dev libreadline6-dev librecode-dev libsnmp-dev libtidy-dev libxslt-dev 19 | RUN apt-get -y --force-yes install ruby-dev debhelper python3-dev devscripts libxml2-dev 20 | 21 | RUN apt-get -y --force-yes install python3-pip python3-setuptools libpython3-dev 22 | RUN apt-get -y --force-yes install python-pip python-setuptools libpython-dev 23 | RUN apt-get install locales 24 | RUN add-apt-repository "deb http://repo.aptly.info/ squeeze main" -y 25 | RUN apt-key adv --keyserver keys.gnupg.net --recv-keys E083A3782A194991 26 | RUN apt-get update 27 | RUN apt-get -yq --force-yes install dh-virtualenv goaccess aptly 28 | 29 | RUN apt-get install postgresql libpq-dev postgresql-client postgresql-client-common -y 30 | RUN apt-get autoclean 31 | 32 | ADD . /home/ 33 | 34 | WORKDIR /home/ 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright AltSchool, PBC (c) 2016 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright AltSchool, PBC (c) 2016 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include dynamic_rest/templates * 2 | recursive-include dynamic_rest/static * 3 | recursive-include dynamic_rest/blueprints * 4 | include *.j2 5 | include *.txt 6 | include *.md 7 | include *.ini 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | APP_NAME := 'dynamic_rest' 2 | INSTALL_DIR ?= ./build 3 | PORT ?= 9002 4 | 5 | define header 6 | @tput setaf 6 7 | @echo "* $1" 8 | @tput sgr0 9 | endef 10 | 11 | .PHONY: docs 12 | 13 | pypi_upload_test: install 14 | $(call header,"Uploading new version to PyPi - test") 15 | @. $(INSTALL_DIR)/bin/activate; python setup.py sdist 16 | @$(INSTALL_DIR)/bin/twine upload --repository testpypi dist/* 17 | 18 | pypi_upload: install 19 | $(call header,"Uploading new version to PyPi") 20 | @. $(INSTALL_DIR)/bin/activate; python setup.py sdist 21 | @$(INSTALL_DIR)/bin/twine upload --repository dynamic-rest dist/* 22 | 23 | docs: install 24 | $(call header,"Building docs") 25 | @DJANGO_SETTINGS_MODULE='tests.settings' $(INSTALL_DIR)/bin/sphinx-build -b html ./docs ./_docs 26 | @cp -r ./_docs/* ./docs 27 | @rm -rf ./_docs 28 | 29 | # Build/install the app 30 | # Runs on every command 31 | install: $(INSTALL_DIR) 32 | $(call header,"Installing") 33 | @$(INSTALL_DIR)/bin/python setup.py -q develop 34 | 35 | # Install/update dependencies 36 | # Runs whenever the requirements.txt file changes 37 | $(INSTALL_DIR): $(INSTALL_DIR)/bin/activate 38 | $(INSTALL_DIR)/bin/activate: requirements.txt install_requires.txt 39 | $(call header,"Updating dependencies") 40 | @test -d $(INSTALL_DIR) || virtualenv $(INSTALL_DIR) 41 | @$(INSTALL_DIR)/bin/pip install -q --upgrade pip setuptools flake8==2.4.0 42 | @$(INSTALL_DIR)/bin/pip install -U -r requirements.txt 43 | @touch $(INSTALL_DIR)/bin/activate 44 | 45 | fixtures: install 46 | $(call header,"Initializing fixture data") 47 | $(INSTALL_DIR)/bin/python manage.py migrate --settings=tests.settings 48 | $(INSTALL_DIR)/bin/python manage.py initialize_fixture --settings=tests.settings 49 | 50 | # Removes build files in working directory 51 | clean_working_directory: 52 | $(call header,"Cleaning working directory") 53 | @rm -rf ./.tox ./dist ./$(APP_NAME).egg-info; 54 | @find . -name '*.pyc' -type f -exec rm -rf {} \; 55 | 56 | # Full clean 57 | clean: clean_working_directory 58 | $(call header,"Cleaning all build files") 59 | @rm -rf $(INSTALL_DIR) 60 | 61 | # Run tests 62 | test: install lint 63 | $(call header,"Running unit tests") 64 | @$(INSTALL_DIR)/bin/py.test --cov=$(APP_NAME) tests/$(TEST) 65 | 66 | # Run tests 67 | integration: install lint 68 | $(call header,"Running integration tests") 69 | @ENABLE_INTEGRATION_TESTS=True $(INSTALL_DIR)/bin/py.test tests/integration/$(TEST) 70 | 71 | test_just: install lint 72 | $(call header,"Running unit tests") 73 | @$(INSTALL_DIR)/bin/py.test --cov=$(APP_NAME) -k $(TEST) --ignore=build --ignore=benchmarks --nomigrations 74 | 75 | # Run all tests (tox) 76 | tox: install 77 | $(call header,"Running multi-version tests") 78 | @$(INSTALL_DIR)/bin/tox $(CMD) 79 | 80 | # Benchmarks 81 | benchmarks: benchmark 82 | benchmark: install 83 | $(call header,"Running benchmarks") 84 | @$(INSTALL_DIR)/bin/python runtests.py --benchmarks --fast 85 | 86 | # Create test app migrations 87 | migrations: install 88 | $(call header,"Creating test app migrations") 89 | $(INSTALL_DIR)/bin/python manage.py makemigrations --settings=tests.settings 90 | 91 | # Start the Django shell 92 | shell: install 93 | $(call header,"Starting shell") 94 | $(INSTALL_DIR)/bin/python manage.py shell --settings=tests.settings 95 | 96 | # Run a Django command 97 | run: install 98 | $(call header,"Running command: $(CMD)") 99 | $(INSTALL_DIR)/bin/python manage.py $(CMD) --settings=tests.settings 100 | 101 | # Start the development server 102 | serve: server 103 | server: start 104 | start: install 105 | $(call header,"Starting development server") 106 | $(INSTALL_DIR)/bin/python manage.py migrate --settings=tests.settings 107 | $(INSTALL_DIR)/bin/python manage.py runserver $(PORT) --settings=tests.settings 108 | 109 | # Lint the project 110 | lint: clean_working_directory 111 | $(call header,"Linting code") 112 | @find . -type f -name '*.py' -not -path '$(INSTALL_DIR)/*' -not -path './docs/*' -not -path './env/*' -not -path '$(INSTALL_DIR)/*' | xargs $(INSTALL_DIR)/bin/flake8 113 | 114 | # Auto-format the project 115 | format: clean_working_directory 116 | $(call header,"Auto-formatting code") 117 | @find $(APP_NAME) -type f -name '*.py' | xargs $(INSTALL_DIR)/bin/flake8 | sed -E 's/^([^:]*\.py).*/\1/g' | uniq | xargs autopep8 --experimental -a --in-place 118 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: manage.py migrate --settings=tests.settings && manage.py runserver 0.0.0.0:$PORT --settings=tests.settings 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Dynamic REST 2 | =================== 3 | 4 | **Dynamic API extensions for Django REST Framework** 5 | 6 | See http://dynamic-rest.readthedocs.org for full documentation. 7 | 8 | Overview 9 | ======== 10 | 11 | Dynamic REST (or DREST) extends the popular `Django REST 12 | Framework `__ (or DRF) with API 13 | features that empower simple RESTful APIs with the flexibility of a 14 | graph query language. 15 | 16 | DREST classes can be used as a drop-in replacement for DRF classes, 17 | which offer the following features on top of the standard DRF kit: 18 | 19 | - Linked relationships 20 | - Sideloaded relationships 21 | - Embedded relationships 22 | - Field inclusions 23 | - Field exclusions 24 | - Field-based filtering 25 | - Field-based sorting 26 | - Directory panel for your Browsable API 27 | - Optimizations 28 | 29 | DREST was initially written to complement `Ember 30 | Data `__, but it can be used to provide 31 | fast and flexible CRUD operations to any consumer that supports JSON 32 | over HTTP. 33 | 34 | Maintainers 35 | ----------- 36 | 37 | - `Anthony Leontiev `__ 38 | - `Savinay Nangalia `__ 39 | - `Christina D'Astolfo `__ 40 | 41 | Requirements 42 | ============ 43 | 44 | - Python (3.6, 3.7, 3.8) 45 | - Django (2.2, 3.1, 3.2) 46 | - Django REST Framework (3.11, 3.12, 3.13) 47 | -------------------------------------------------------------------------------- /benchmarks.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 43 |
44 |
45 | 46 | 82 |
83 |
84 | 85 | 121 |
122 |
123 | -------------------------------------------------------------------------------- /benchmarks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/benchmarks/__init__.py -------------------------------------------------------------------------------- /benchmarks/drest.py: -------------------------------------------------------------------------------- 1 | from dynamic_rest import fields as fields 2 | from dynamic_rest import routers as routers 3 | from dynamic_rest import serializers as serializers 4 | from dynamic_rest import viewsets as viewsets 5 | 6 | from .models import Group, Permission, User 7 | 8 | 9 | # DREST 10 | 11 | # DREST serializers 12 | 13 | 14 | class UserSerializer(serializers.DynamicModelSerializer): 15 | 16 | class Meta: 17 | model = User 18 | name = 'user' 19 | fields = ('id', 'name', 'groups') 20 | 21 | groups = fields.DynamicRelationField( 22 | 'GroupSerializer', 23 | embed=True, 24 | many=True, 25 | deferred=True 26 | ) 27 | 28 | 29 | class GroupSerializer(serializers.DynamicModelSerializer): 30 | 31 | class Meta: 32 | model = Group 33 | name = 'group' 34 | fields = ('id', 'name', 'permissions') 35 | 36 | permissions = fields.DynamicRelationField( 37 | 'PermissionSerializer', 38 | embed=True, 39 | many=True, 40 | deferred=True 41 | ) 42 | 43 | 44 | class PermissionSerializer(serializers.DynamicModelSerializer): 45 | 46 | class Meta: 47 | model = Permission 48 | name = 'permission' 49 | fields = ('id', 'name') 50 | 51 | # DREST views 52 | 53 | 54 | class UserViewSet(viewsets.DynamicModelViewSet): 55 | queryset = User.objects.all() 56 | serializer_class = UserSerializer 57 | 58 | 59 | # DREST router 60 | 61 | router = routers.DynamicRouter() 62 | router.register(r'drest/users', UserViewSet) 63 | -------------------------------------------------------------------------------- /benchmarks/drf.py: -------------------------------------------------------------------------------- 1 | from rest_framework import routers, serializers, viewsets 2 | 3 | from .models import Group, Permission, User 4 | 5 | 6 | # DRF 7 | 8 | # DRF Serializers 9 | 10 | 11 | class UserSerializer(serializers.ModelSerializer): 12 | 13 | class Meta: 14 | model = User 15 | fields = ('id', 'name') 16 | 17 | 18 | class GroupSerializer(serializers.ModelSerializer): 19 | 20 | class Meta: 21 | model = Group 22 | fields = ('id', 'name') 23 | 24 | 25 | class PermissionSerializer(serializers.ModelSerializer): 26 | 27 | class Meta: 28 | model = Permission 29 | fields = ('id', 'name') 30 | 31 | 32 | class UserWithGroupsSerializer(serializers.ModelSerializer): 33 | 34 | class Meta: 35 | model = User 36 | fields = ('id', 'name', 'groups') 37 | groups = GroupSerializer(many=True) 38 | 39 | 40 | class GroupWithPermissionsSerializer(serializers.ModelSerializer): 41 | 42 | class Meta: 43 | model = Group 44 | fields = ('id', 'name', 'permissions') 45 | 46 | permissions = PermissionSerializer(many=True) 47 | 48 | 49 | class UserWithAllSerializer(serializers.ModelSerializer): 50 | 51 | class Meta: 52 | model = User 53 | fields = ('id', 'name', 'groups') 54 | 55 | groups = GroupWithPermissionsSerializer(many=True) 56 | 57 | # DRF viewsets 58 | 59 | 60 | class UserViewSet(viewsets.ModelViewSet): 61 | queryset = User.objects.all() 62 | serializer_class = UserSerializer 63 | 64 | 65 | class UserWithGroupsViewSet(viewsets.ModelViewSet): 66 | queryset = User.objects.all() 67 | serializer_class = UserWithGroupsSerializer 68 | 69 | 70 | class UserWithAllViewSet(viewsets.ModelViewSet): 71 | queryset = User.objects.all() 72 | serializer_class = UserWithAllSerializer 73 | 74 | 75 | # DRF routing 76 | 77 | router = routers.DefaultRouter() 78 | router.register(r'drf/users', UserWithGroupsViewSet) 79 | router.register(r'drf/users_with_groups', UserWithGroupsViewSet) 80 | router.register(r'drf/users_with_all', UserWithAllViewSet) 81 | -------------------------------------------------------------------------------- /benchmarks/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class User(models.Model): 5 | name = models.TextField() 6 | groups = models.ManyToManyField('Group', related_name='users') 7 | created = models.DateTimeField(auto_now_add=True) 8 | updated = models.DateTimeField(auto_now=True) 9 | 10 | 11 | class Group(models.Model): 12 | name = models.TextField() 13 | max_size = models.PositiveIntegerField() 14 | permissions = models.ManyToManyField('Permission', related_name='groups') 15 | created = models.DateTimeField(auto_now_add=True) 16 | updated = models.DateTimeField(auto_now=True) 17 | 18 | 19 | class Permission(models.Model): 20 | name = models.TextField() 21 | created = models.DateTimeField(auto_now_add=True) 22 | updated = models.DateTimeField(auto_now=True) 23 | -------------------------------------------------------------------------------- /benchmarks/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | BASE_DIR = os.path.dirname(__file__) 4 | 5 | SECRET_KEY = 'test' 6 | INSTALL_DIR = '/usr/local/altschool/dynamic-rest/' 7 | STATIC_URL = '/static/' 8 | STATIC_ROOT = INSTALL_DIR + 'www/static' 9 | 10 | DEBUG = True 11 | USE_TZ = False 12 | 13 | DATABASES = {} 14 | if os.environ.get('DATABASE_URL'): 15 | # remote database 16 | import dj_database_url 17 | DATABASES['default'] = dj_database_url.config() 18 | else: 19 | # local sqlite database file 20 | DATABASES['default'] = { 21 | 'ENGINE': 'django.db.backends.sqlite3', 22 | 'NAME': os.path.abspath('db.sqlite3'), 23 | 'USER': '', 24 | 'PASSWORD': '', 25 | 'HOST': '', 26 | 'PORT': '' 27 | } 28 | 29 | MIDDLEWARE_CLASSES = () 30 | 31 | INSTALLED_APPS = ( 32 | 'django.contrib.staticfiles', 33 | 'django.contrib.contenttypes', 34 | 'django.contrib.auth', 35 | 'django.contrib.sites', 36 | 'dynamic_rest', 37 | 'rest_framework', 38 | 'benchmarks', 39 | ) 40 | 41 | ROOT_URLCONF = 'benchmarks.urls' 42 | 43 | DYNAMIC_REST = { 44 | 'ENABLE_LINKS': False 45 | } 46 | -------------------------------------------------------------------------------- /benchmarks/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | 3 | from .drest import router as drest_router 4 | from .drf import router as drf_router 5 | 6 | urlpatterns = [ 7 | path('', include(drf_router.urls)), 8 | path('', include(drest_router.urls)), 9 | ] 10 | -------------------------------------------------------------------------------- /docs/_modules/dynamic_rest/bases.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | dynamic_rest.bases — Dynamic REST 1.3.15 documentation 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 | 45 | 46 | 94 | 95 |
96 | 97 | 98 | 102 | 103 | 104 | 105 |
106 |
107 | 108 | 109 | 110 | 111 | 112 | 113 |
114 |
    115 |
  • Docs »
  • 116 | 117 |
  • Module code »
  • 118 | 119 |
  • dynamic_rest.bases
  • 120 |
  • 121 | 122 | 123 | 124 |
  • 125 |
126 |
127 |
128 |
129 |
130 | 131 |

Source code for dynamic_rest.bases

132 | """This module contains base classes for DREST."""
133 | 
134 | 
135 | 
[docs]class DynamicSerializerBase(object):
136 | 137 | """Base class for all DREST serializers.""" 138 | pass 139 |
140 | 141 |
142 |
143 |
144 | 145 | 146 |
147 | 148 |
149 |

150 | © Copyright 2016, ant@altschool.com, ryo@altschool.com. 151 | 152 |

153 |
154 | Built with Sphinx using a theme provided by Read the Docs. 155 | 156 |
157 | 158 |
159 |
160 | 161 |
162 | 163 |
164 | 165 | 166 | 167 | 168 | 169 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 196 | 197 | 198 | 199 | -------------------------------------------------------------------------------- /docs/_modules/dynamic_rest/renderers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | dynamic_rest.renderers — Dynamic REST 1.3.15 documentation 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 | 45 | 46 | 94 | 95 |
96 | 97 | 98 | 102 | 103 | 104 | 105 |
106 |
107 | 108 | 109 | 110 | 111 | 112 | 113 |
114 |
    115 |
  • Docs »
  • 116 | 117 |
  • Module code »
  • 118 | 119 |
  • dynamic_rest.renderers
  • 120 |
  • 121 | 122 | 123 | 124 |
  • 125 |
126 |
127 |
128 |
129 |
130 | 131 |

Source code for dynamic_rest.renderers

132 | """This module contains custom renderer classes."""
133 | from rest_framework.renderers import BrowsableAPIRenderer
134 | 
135 | 
136 | 
[docs]class DynamicBrowsableAPIRenderer(BrowsableAPIRenderer): 137 | """Renderer class that adds directory support to the Browsable API.""" 138 |
[docs] def get_context(self, data, media_type, context): 139 | from dynamic_rest.routers import get_directory 140 | 141 | context = super(DynamicBrowsableAPIRenderer, self).get_context( 142 | data, 143 | media_type, 144 | context 145 | ) 146 | request = context['request'] 147 | context['directory'] = get_directory(request) 148 | return context
149 |
150 | 151 |
152 |
153 |
154 | 155 | 156 |
157 | 158 |
159 |

160 | © Copyright 2016, ant@altschool.com, ryo@altschool.com. 161 | 162 |

163 |
164 | Built with Sphinx using a theme provided by Read the Docs. 165 | 166 |
167 | 168 |
169 |
170 | 171 |
172 | 173 |
174 | 175 | 176 | 177 | 178 | 179 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 206 | 207 | 208 | 209 | -------------------------------------------------------------------------------- /docs/_modules/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Overview: module code — Dynamic REST 1.3.15 documentation 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 | 44 | 45 | 93 | 94 |
95 | 96 | 97 | 101 | 102 | 103 | 104 |
105 |
106 | 107 | 108 | 109 | 110 | 111 | 112 |
113 |
    114 |
  • Docs »
  • 115 | 116 |
  • Overview: module code
  • 117 |
  • 118 | 119 | 120 | 121 |
  • 122 |
123 |
124 |
125 | 148 |
149 | 150 | 151 |
152 | 153 |
154 |

155 | © Copyright 2016, ant@altschool.com, ryo@altschool.com. 156 | 157 |

158 |
159 | Built with Sphinx using a theme provided by Read the Docs. 160 | 161 |
162 | 163 |
164 |
165 | 166 |
167 | 168 |
169 | 170 | 171 | 172 | 173 | 174 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 201 | 202 | 203 | 204 | -------------------------------------------------------------------------------- /docs/_sources/dynamic_rest.txt: -------------------------------------------------------------------------------- 1 | dynamic_rest package 2 | ==================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | dynamic_rest.bases module 8 | ------------------------- 9 | 10 | .. automodule:: dynamic_rest.bases 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | dynamic_rest.conf module 16 | ------------------------ 17 | 18 | .. automodule:: dynamic_rest.conf 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | dynamic_rest.constants module 24 | ----------------------------- 25 | 26 | .. automodule:: dynamic_rest.constants 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | dynamic_rest.datastructures module 32 | ---------------------------------- 33 | 34 | .. automodule:: dynamic_rest.datastructures 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | dynamic_rest.fields module 40 | -------------------------- 41 | 42 | .. automodule:: dynamic_rest.fields 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | dynamic_rest.filters module 48 | --------------------------- 49 | 50 | .. automodule:: dynamic_rest.filters 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | dynamic_rest.links module 56 | ------------------------- 57 | 58 | .. automodule:: dynamic_rest.links 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | 63 | dynamic_rest.meta module 64 | ------------------------ 65 | 66 | .. automodule:: dynamic_rest.meta 67 | :members: 68 | :undoc-members: 69 | :show-inheritance: 70 | 71 | dynamic_rest.metadata module 72 | ---------------------------- 73 | 74 | .. automodule:: dynamic_rest.metadata 75 | :members: 76 | :undoc-members: 77 | :show-inheritance: 78 | 79 | dynamic_rest.pagination module 80 | ------------------------------ 81 | 82 | .. automodule:: dynamic_rest.pagination 83 | :members: 84 | :undoc-members: 85 | :show-inheritance: 86 | 87 | dynamic_rest.patches module 88 | --------------------------- 89 | 90 | .. automodule:: dynamic_rest.patches 91 | :members: 92 | :undoc-members: 93 | :show-inheritance: 94 | 95 | dynamic_rest.processors module 96 | ------------------------------ 97 | 98 | .. automodule:: dynamic_rest.processors 99 | :members: 100 | :undoc-members: 101 | :show-inheritance: 102 | 103 | dynamic_rest.related module 104 | --------------------------- 105 | 106 | .. automodule:: dynamic_rest.related 107 | :members: 108 | :undoc-members: 109 | :show-inheritance: 110 | 111 | dynamic_rest.renderers module 112 | ----------------------------- 113 | 114 | .. automodule:: dynamic_rest.renderers 115 | :members: 116 | :undoc-members: 117 | :show-inheritance: 118 | 119 | dynamic_rest.routers module 120 | --------------------------- 121 | 122 | .. automodule:: dynamic_rest.routers 123 | :members: 124 | :undoc-members: 125 | :show-inheritance: 126 | 127 | dynamic_rest.serializers module 128 | ------------------------------- 129 | 130 | .. automodule:: dynamic_rest.serializers 131 | :members: 132 | :undoc-members: 133 | :show-inheritance: 134 | 135 | dynamic_rest.tagged module 136 | -------------------------- 137 | 138 | .. automodule:: dynamic_rest.tagged 139 | :members: 140 | :undoc-members: 141 | :show-inheritance: 142 | 143 | dynamic_rest.viewsets module 144 | ---------------------------- 145 | 146 | .. automodule:: dynamic_rest.viewsets 147 | :members: 148 | :undoc-members: 149 | :show-inheritance: 150 | 151 | 152 | Module contents 153 | --------------- 154 | 155 | .. automodule:: dynamic_rest 156 | :members: 157 | :undoc-members: 158 | :show-inheritance: 159 | -------------------------------------------------------------------------------- /docs/_sources/index.txt: -------------------------------------------------------------------------------- 1 | .. Dynamic REST documentation master file, created by 2 | sphinx-quickstart on Thu Feb 11 15:07:20 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Dynamic REST Documentation 7 | ========================== 8 | 9 | .. toctree:: 10 | :maxdepth: 4 11 | :caption: Contents 12 | 13 | tutorial 14 | dynamic_rest 15 | 16 | 17 | Indices and tables 18 | ================== 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | * :ref:`search` 23 | 24 | -------------------------------------------------------------------------------- /docs/_static/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/docs/_static/ajax-loader.gif -------------------------------------------------------------------------------- /docs/_static/comment-bright.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/docs/_static/comment-bright.png -------------------------------------------------------------------------------- /docs/_static/comment-close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/docs/_static/comment-close.png -------------------------------------------------------------------------------- /docs/_static/comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/docs/_static/comment.png -------------------------------------------------------------------------------- /docs/_static/css/badge_only.css: -------------------------------------------------------------------------------- 1 | .fa:before{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-weight:normal;font-style:normal;src:url("../font/fontawesome_webfont.eot");src:url("../font/fontawesome_webfont.eot?#iefix") format("embedded-opentype"),url("../font/fontawesome_webfont.woff") format("woff"),url("../font/fontawesome_webfont.ttf") format("truetype"),url("../font/fontawesome_webfont.svg#FontAwesome") format("svg")}.fa:before{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;text-decoration:inherit}a .fa{display:inline-block;text-decoration:inherit}li .fa{display:inline-block}li .fa-large:before,li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-0.8em}ul.fas li .fa{width:0.8em}ul.fas li .fa-large:before,ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before{content:""}.icon-book:before{content:""}.fa-caret-down:before{content:""}.icon-caret-down:before{content:""}.fa-caret-up:before{content:""}.icon-caret-up:before{content:""}.fa-caret-left:before{content:""}.icon-caret-left:before{content:""}.fa-caret-right:before{content:""}.icon-caret-right:before{content:""}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;border-top:solid 10px #343131;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;z-index:400}.rst-versions a{color:#2980B9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27AE60;*zoom:1}.rst-versions .rst-current-version:before,.rst-versions .rst-current-version:after{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book{float:left}.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#E74C3C;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#F1C40F;color:#000}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:gray;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:solid 1px #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px}.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge .fa-book{float:none}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book{float:left}.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge .rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width: 768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}img{width:100%;height:auto}} 2 | /*# sourceMappingURL=badge_only.css.map */ 3 | -------------------------------------------------------------------------------- /docs/_static/down-pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/docs/_static/down-pressed.png -------------------------------------------------------------------------------- /docs/_static/down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/docs/_static/down.png -------------------------------------------------------------------------------- /docs/_static/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/docs/_static/file.png -------------------------------------------------------------------------------- /docs/_static/fonts/Inconsolata-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/docs/_static/fonts/Inconsolata-Bold.ttf -------------------------------------------------------------------------------- /docs/_static/fonts/Inconsolata-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/docs/_static/fonts/Inconsolata-Regular.ttf -------------------------------------------------------------------------------- /docs/_static/fonts/Lato-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/docs/_static/fonts/Lato-Bold.ttf -------------------------------------------------------------------------------- /docs/_static/fonts/Lato-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/docs/_static/fonts/Lato-Regular.ttf -------------------------------------------------------------------------------- /docs/_static/fonts/RobotoSlab-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/docs/_static/fonts/RobotoSlab-Bold.ttf -------------------------------------------------------------------------------- /docs/_static/fonts/RobotoSlab-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/docs/_static/fonts/RobotoSlab-Regular.ttf -------------------------------------------------------------------------------- /docs/_static/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/docs/_static/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /docs/_static/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/docs/_static/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /docs/_static/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/docs/_static/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /docs/_static/js/theme.js: -------------------------------------------------------------------------------- 1 | require=(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o"); 77 | 78 | // Add expand links to all parents of nested ul 79 | $('.wy-menu-vertical ul').not('.simple').siblings('a').each(function () { 80 | var link = $(this); 81 | expand = $(''); 82 | expand.on('click', function (ev) { 83 | self.toggleCurrent(link); 84 | ev.stopPropagation(); 85 | return false; 86 | }); 87 | link.prepend(expand); 88 | }); 89 | }; 90 | 91 | nav.reset = function () { 92 | // Get anchor from URL and open up nested nav 93 | var anchor = encodeURI(window.location.hash); 94 | if (anchor) { 95 | try { 96 | var link = $('.wy-menu-vertical') 97 | .find('[href="' + anchor + '"]'); 98 | $('.wy-menu-vertical li.toctree-l1 li.current') 99 | .removeClass('current'); 100 | link.closest('li.toctree-l2').addClass('current'); 101 | link.closest('li.toctree-l3').addClass('current'); 102 | link.closest('li.toctree-l4').addClass('current'); 103 | } 104 | catch (err) { 105 | console.log("Error expanding nav for anchor", err); 106 | } 107 | } 108 | }; 109 | 110 | nav.onScroll = function () { 111 | this.winScroll = false; 112 | var newWinPosition = this.win.scrollTop(), 113 | winBottom = newWinPosition + this.winHeight, 114 | navPosition = this.navBar.scrollTop(), 115 | newNavPosition = navPosition + (newWinPosition - this.winPosition); 116 | if (newWinPosition < 0 || winBottom > this.docHeight) { 117 | return; 118 | } 119 | this.navBar.scrollTop(newNavPosition); 120 | this.winPosition = newWinPosition; 121 | }; 122 | 123 | nav.onResize = function () { 124 | this.winResize = false; 125 | this.winHeight = this.win.height(); 126 | this.docHeight = $(document).height(); 127 | }; 128 | 129 | nav.hashChange = function () { 130 | this.linkScroll = true; 131 | this.win.one('hashchange', function () { 132 | this.linkScroll = false; 133 | }); 134 | }; 135 | 136 | nav.toggleCurrent = function (elem) { 137 | var parent_li = elem.closest('li'); 138 | parent_li.siblings('li.current').removeClass('current'); 139 | parent_li.siblings().find('li.current').removeClass('current'); 140 | parent_li.find('> ul li.current').removeClass('current'); 141 | parent_li.toggleClass('current'); 142 | } 143 | 144 | return nav; 145 | }; 146 | 147 | module.exports.ThemeNav = ThemeNav(); 148 | 149 | if (typeof(window) != 'undefined') { 150 | window.SphinxRtdTheme = { StickyNav: module.exports.ThemeNav }; 151 | } 152 | 153 | },{"jquery":"jquery"}]},{},["sphinx-rtd-theme"]); 154 | -------------------------------------------------------------------------------- /docs/_static/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/docs/_static/minus.png -------------------------------------------------------------------------------- /docs/_static/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/docs/_static/plus.png -------------------------------------------------------------------------------- /docs/_static/pygments.css: -------------------------------------------------------------------------------- 1 | .highlight .hll { background-color: #ffffcc } 2 | .highlight { background: #eeffcc; } 3 | .highlight .c { color: #408090; font-style: italic } /* Comment */ 4 | .highlight .err { border: 1px solid #FF0000 } /* Error */ 5 | .highlight .k { color: #007020; font-weight: bold } /* Keyword */ 6 | .highlight .o { color: #666666 } /* Operator */ 7 | .highlight .ch { color: #408090; font-style: italic } /* Comment.Hashbang */ 8 | .highlight .cm { color: #408090; font-style: italic } /* Comment.Multiline */ 9 | .highlight .cp { color: #007020 } /* Comment.Preproc */ 10 | .highlight .cpf { color: #408090; font-style: italic } /* Comment.PreprocFile */ 11 | .highlight .c1 { color: #408090; font-style: italic } /* Comment.Single */ 12 | .highlight .cs { color: #408090; background-color: #fff0f0 } /* Comment.Special */ 13 | .highlight .gd { color: #A00000 } /* Generic.Deleted */ 14 | .highlight .ge { font-style: italic } /* Generic.Emph */ 15 | .highlight .gr { color: #FF0000 } /* Generic.Error */ 16 | .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 17 | .highlight .gi { color: #00A000 } /* Generic.Inserted */ 18 | .highlight .go { color: #333333 } /* Generic.Output */ 19 | .highlight .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ 20 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 21 | .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 22 | .highlight .gt { color: #0044DD } /* Generic.Traceback */ 23 | .highlight .kc { color: #007020; font-weight: bold } /* Keyword.Constant */ 24 | .highlight .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */ 25 | .highlight .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */ 26 | .highlight .kp { color: #007020 } /* Keyword.Pseudo */ 27 | .highlight .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */ 28 | .highlight .kt { color: #902000 } /* Keyword.Type */ 29 | .highlight .m { color: #208050 } /* Literal.Number */ 30 | .highlight .s { color: #4070a0 } /* Literal.String */ 31 | .highlight .na { color: #4070a0 } /* Name.Attribute */ 32 | .highlight .nb { color: #007020 } /* Name.Builtin */ 33 | .highlight .nc { color: #0e84b5; font-weight: bold } /* Name.Class */ 34 | .highlight .no { color: #60add5 } /* Name.Constant */ 35 | .highlight .nd { color: #555555; font-weight: bold } /* Name.Decorator */ 36 | .highlight .ni { color: #d55537; font-weight: bold } /* Name.Entity */ 37 | .highlight .ne { color: #007020 } /* Name.Exception */ 38 | .highlight .nf { color: #06287e } /* Name.Function */ 39 | .highlight .nl { color: #002070; font-weight: bold } /* Name.Label */ 40 | .highlight .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ 41 | .highlight .nt { color: #062873; font-weight: bold } /* Name.Tag */ 42 | .highlight .nv { color: #bb60d5 } /* Name.Variable */ 43 | .highlight .ow { color: #007020; font-weight: bold } /* Operator.Word */ 44 | .highlight .w { color: #bbbbbb } /* Text.Whitespace */ 45 | .highlight .mb { color: #208050 } /* Literal.Number.Bin */ 46 | .highlight .mf { color: #208050 } /* Literal.Number.Float */ 47 | .highlight .mh { color: #208050 } /* Literal.Number.Hex */ 48 | .highlight .mi { color: #208050 } /* Literal.Number.Integer */ 49 | .highlight .mo { color: #208050 } /* Literal.Number.Oct */ 50 | .highlight .sb { color: #4070a0 } /* Literal.String.Backtick */ 51 | .highlight .sc { color: #4070a0 } /* Literal.String.Char */ 52 | .highlight .sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */ 53 | .highlight .s2 { color: #4070a0 } /* Literal.String.Double */ 54 | .highlight .se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */ 55 | .highlight .sh { color: #4070a0 } /* Literal.String.Heredoc */ 56 | .highlight .si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */ 57 | .highlight .sx { color: #c65d09 } /* Literal.String.Other */ 58 | .highlight .sr { color: #235388 } /* Literal.String.Regex */ 59 | .highlight .s1 { color: #4070a0 } /* Literal.String.Single */ 60 | .highlight .ss { color: #517918 } /* Literal.String.Symbol */ 61 | .highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */ 62 | .highlight .vc { color: #bb60d5 } /* Name.Variable.Class */ 63 | .highlight .vg { color: #bb60d5 } /* Name.Variable.Global */ 64 | .highlight .vi { color: #bb60d5 } /* Name.Variable.Instance */ 65 | .highlight .il { color: #208050 } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /docs/_static/up-pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/docs/_static/up-pressed.png -------------------------------------------------------------------------------- /docs/_static/up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/docs/_static/up.png -------------------------------------------------------------------------------- /docs/dynamic_rest.rst: -------------------------------------------------------------------------------- 1 | dynamic_rest package 2 | ==================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | dynamic_rest.bases module 8 | ------------------------- 9 | 10 | .. automodule:: dynamic_rest.bases 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | dynamic_rest.conf module 16 | ------------------------ 17 | 18 | .. automodule:: dynamic_rest.conf 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | dynamic_rest.constants module 24 | ----------------------------- 25 | 26 | .. automodule:: dynamic_rest.constants 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | dynamic_rest.datastructures module 32 | ---------------------------------- 33 | 34 | .. automodule:: dynamic_rest.datastructures 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | dynamic_rest.fields module 40 | -------------------------- 41 | 42 | .. automodule:: dynamic_rest.fields 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | dynamic_rest.filters module 48 | --------------------------- 49 | 50 | .. automodule:: dynamic_rest.filters 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | dynamic_rest.links module 56 | ------------------------- 57 | 58 | .. automodule:: dynamic_rest.links 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | 63 | dynamic_rest.meta module 64 | ------------------------ 65 | 66 | .. automodule:: dynamic_rest.meta 67 | :members: 68 | :undoc-members: 69 | :show-inheritance: 70 | 71 | dynamic_rest.metadata module 72 | ---------------------------- 73 | 74 | .. automodule:: dynamic_rest.metadata 75 | :members: 76 | :undoc-members: 77 | :show-inheritance: 78 | 79 | dynamic_rest.pagination module 80 | ------------------------------ 81 | 82 | .. automodule:: dynamic_rest.pagination 83 | :members: 84 | :undoc-members: 85 | :show-inheritance: 86 | 87 | dynamic_rest.patches module 88 | --------------------------- 89 | 90 | .. automodule:: dynamic_rest.patches 91 | :members: 92 | :undoc-members: 93 | :show-inheritance: 94 | 95 | dynamic_rest.processors module 96 | ------------------------------ 97 | 98 | .. automodule:: dynamic_rest.processors 99 | :members: 100 | :undoc-members: 101 | :show-inheritance: 102 | 103 | dynamic_rest.related module 104 | --------------------------- 105 | 106 | .. automodule:: dynamic_rest.related 107 | :members: 108 | :undoc-members: 109 | :show-inheritance: 110 | 111 | dynamic_rest.renderers module 112 | ----------------------------- 113 | 114 | .. automodule:: dynamic_rest.renderers 115 | :members: 116 | :undoc-members: 117 | :show-inheritance: 118 | 119 | dynamic_rest.routers module 120 | --------------------------- 121 | 122 | .. automodule:: dynamic_rest.routers 123 | :members: 124 | :undoc-members: 125 | :show-inheritance: 126 | 127 | dynamic_rest.serializers module 128 | ------------------------------- 129 | 130 | .. automodule:: dynamic_rest.serializers 131 | :members: 132 | :undoc-members: 133 | :show-inheritance: 134 | 135 | dynamic_rest.tagged module 136 | -------------------------- 137 | 138 | .. automodule:: dynamic_rest.tagged 139 | :members: 140 | :undoc-members: 141 | :show-inheritance: 142 | 143 | dynamic_rest.viewsets module 144 | ---------------------------- 145 | 146 | .. automodule:: dynamic_rest.viewsets 147 | :members: 148 | :undoc-members: 149 | :show-inheritance: 150 | 151 | 152 | Module contents 153 | --------------- 154 | 155 | .. automodule:: dynamic_rest 156 | :members: 157 | :undoc-members: 158 | :show-inheritance: 159 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Dynamic REST documentation master file, created by 2 | sphinx-quickstart on Thu Feb 11 15:07:20 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Dynamic REST Documentation 7 | ========================== 8 | 9 | .. toctree:: 10 | :maxdepth: 4 11 | :caption: Contents 12 | 13 | tutorial 14 | dynamic_rest 15 | 16 | 17 | Indices and tables 18 | ================== 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | * :ref:`search` 23 | 24 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 1>NUL 2>NUL 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\DynamicREST.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\DynamicREST.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /docs/objects.inv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/docs/objects.inv -------------------------------------------------------------------------------- /docs/search.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Search — Dynamic REST 1.3.15 documentation 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 | 44 | 45 | 93 | 94 |
95 | 96 | 97 | 101 | 102 | 103 | 104 |
105 |
106 | 107 | 108 | 109 | 110 | 111 | 112 |
113 |
    114 |
  • Docs »
  • 115 | 116 |
  • 117 |
  • 118 | 119 |
  • 120 |
121 |
122 |
123 |
124 |
125 | 126 | 134 | 135 | 136 |
137 | 138 |
139 | 140 |
141 |
142 |
143 | 144 | 145 |
146 | 147 |
148 |

149 | © Copyright 2016, ant@altschool.com, ryo@altschool.com. 150 | 151 |

152 |
153 | Built with Sphinx using a theme provided by Read the Docs. 154 | 155 |
156 | 157 |
158 |
159 | 160 |
161 | 162 |
163 | 164 | 165 | 166 | 167 | 168 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 196 | 197 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | -------------------------------------------------------------------------------- /dynamic_rest/__init__.py: -------------------------------------------------------------------------------- 1 | """Dynamic REST (or DREST) is an extension of Django REST Framework. 2 | 3 | DREST offers the following features on top of the standard DRF kit: 4 | 5 | - Linked/embedded/sideloaded relationships 6 | - Field inclusions/exlusions 7 | - Field-based filtering/sorting 8 | - Directory panel for the browsable API 9 | - Optimizations 10 | """ 11 | 12 | default_app_config = "dynamic_rest.apps.DynamicRestConfig" 13 | -------------------------------------------------------------------------------- /dynamic_rest/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.core.exceptions import ImproperlyConfigured 3 | 4 | from dynamic_rest.conf import settings 5 | 6 | 7 | class DynamicRestConfig(AppConfig): 8 | name = "dynamic_rest" 9 | verbose_name = "Django Dynamic Rest" 10 | 11 | def ready(self): 12 | 13 | if hasattr(settings, 14 | "ENABLE_HASHID_FIELDS") and settings.ENABLE_HASHID_FIELDS: 15 | if not hasattr( 16 | settings, "HASHIDS_SALT") or settings.HASHIDS_SALT is None: 17 | raise ImproperlyConfigured( 18 | "ENABLED_HASHID_FIELDS is True in your settings," 19 | "but no HASHIDS_SALT string was set!") 20 | -------------------------------------------------------------------------------- /dynamic_rest/bases.py: -------------------------------------------------------------------------------- 1 | """This module contains base classes for DREST.""" 2 | 3 | from dynamic_rest.utils import model_from_definition 4 | 5 | 6 | class DynamicSerializerBase(object): 7 | 8 | """Base class for all DREST serializers.""" 9 | pass 10 | 11 | 12 | def resettable_cached_property(func): 13 | """Decorator to add cached computed properties to an object. 14 | Similar to Django's `cached_property` decorator, except stores 15 | all the data under a single well-known key so that it can easily 16 | be blown away. 17 | """ 18 | 19 | def wrapper(self): 20 | if not hasattr(self, '_resettable_cached_properties'): 21 | self._resettable_cached_properties = {} 22 | if func.__name__ not in self._resettable_cached_properties: 23 | self._resettable_cached_properties[func.__name__] = func(self) 24 | return self._resettable_cached_properties[func.__name__] 25 | 26 | # Returns a property whose getter is the 'wrapper' function 27 | return property(wrapper) 28 | 29 | 30 | def cacheable_object(cls): 31 | """Decorator to add a reset() method that clears data cached by 32 | the @resettable_cached_property decorator. Technically this could 33 | be a mixin... 34 | """ 35 | 36 | def reset(self): 37 | if hasattr(self, '_resettable_cached_properties'): 38 | self._resettable_cached_properties = {} 39 | 40 | cls.reset = reset 41 | return cls 42 | 43 | 44 | @cacheable_object 45 | class CacheableFieldMixin(object): 46 | """Overide Field.root and Field.context to make fields/serializers 47 | cacheable and reusable. The DRF version uses @cached_property which 48 | doesn't have a public API for resetting. This version uses normal 49 | object variables with and adds a `reset()` API. 50 | """ 51 | 52 | @resettable_cached_property 53 | def root(self): 54 | root = self 55 | while root.parent is not None: 56 | root = root.parent 57 | return root 58 | 59 | @resettable_cached_property 60 | def context(self): 61 | return getattr(self.root, '_context', {}) 62 | 63 | 64 | class GetModelMixin(object): 65 | """ 66 | Mixin to retrieve model hashid 67 | 68 | Implementation from 69 | https://github.com/evenicoulddoit/django-rest-framework-serializer-extensions 70 | """ 71 | 72 | def __init__(self, *args, **kwargs): 73 | self.model = kwargs.pop('model', None) 74 | super(GetModelMixin, self).__init__(*args, **kwargs) 75 | 76 | def get_model(self): 77 | """ 78 | Return the model to generate the HashId for. 79 | 80 | By default, this will equal the model defined within the Meta of the 81 | ModelSerializer, but can be redefined either during initialisation 82 | of the Field, or by providing a get__model method on the 83 | parent serializer. 84 | 85 | The Meta can either explicitly define a model, or provide a 86 | dot-delimited string path to it. 87 | """ 88 | if self.model is None: 89 | custom_fn_name = 'get_{0}_model'.format(self.field_name) 90 | 91 | if hasattr(self.parent, custom_fn_name): 92 | return getattr(self.parent, custom_fn_name)() 93 | else: 94 | try: 95 | return self.parent.Meta.model 96 | except AttributeError: 97 | raise AssertionError( 98 | 'No "model" value passed to field "{0}"'.format( 99 | type(self).__name__ 100 | ) 101 | ) 102 | elif isinstance(self.model, str): 103 | return model_from_definition(self.model) 104 | else: 105 | return self.model 106 | -------------------------------------------------------------------------------- /dynamic_rest/blueprints/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/dynamic_rest/blueprints/api/__init__.py -------------------------------------------------------------------------------- /dynamic_rest/blueprints/api/context.py: -------------------------------------------------------------------------------- 1 | import click 2 | import inflection 3 | 4 | 5 | @click.command() 6 | @click.argument('version') 7 | @click.argument('name') 8 | @click.option('--model') 9 | @click.option('--plural-name') 10 | def get_context(version, name, model, plural_name): 11 | name = inflection.underscore(inflection.singularize(name)) 12 | model = model or name 13 | model_class_name = inflection.camelize(model) 14 | class_name = inflection.camelize(name) 15 | serializer_class_name = class_name + 'Serializer' 16 | viewset_class_name = class_name + 'ViewSet' 17 | plural_name = plural_name or inflection.pluralize(name) 18 | return { 19 | 'version': version, 20 | 'serializer_class_name': serializer_class_name, 21 | 'viewset_class_name': viewset_class_name, 22 | 'model_class_name': model_class_name, 23 | 'name': name, 24 | 'plural_name': plural_name 25 | } 26 | -------------------------------------------------------------------------------- /dynamic_rest/blueprints/api/templates/{{app}}/__init__.py.j2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/dynamic_rest/blueprints/api/templates/{{app}}/__init__.py.j2 -------------------------------------------------------------------------------- /dynamic_rest/blueprints/api/templates/{{app}}/api/__init__.py.j2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/dynamic_rest/blueprints/api/templates/{{app}}/api/__init__.py.j2 -------------------------------------------------------------------------------- /dynamic_rest/blueprints/api/templates/{{app}}/api/urls.py.j2: -------------------------------------------------------------------------------- 1 | from djx.urls import load_urls 2 | urlpatterns = load_urls(__file__) 3 | -------------------------------------------------------------------------------- /dynamic_rest/blueprints/api/templates/{{app}}/api/{{version}}/__init__.py.j2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/dynamic_rest/blueprints/api/templates/{{app}}/api/{{version}}/__init__.py.j2 -------------------------------------------------------------------------------- /dynamic_rest/blueprints/api/templates/{{app}}/api/{{version}}/serializers/__init__.py.j2: -------------------------------------------------------------------------------- 1 | from .{{name}} import {{serializer_class_name}} 2 | -------------------------------------------------------------------------------- /dynamic_rest/blueprints/api/templates/{{app}}/api/{{version}}/serializers/base.py.j2: -------------------------------------------------------------------------------- 1 | from dynamic_rest.serializers import DynamicModelSerializer 2 | 3 | class Serializer(DynamicModelSerializer): 4 | pass 5 | -------------------------------------------------------------------------------- /dynamic_rest/blueprints/api/templates/{{app}}/api/{{version}}/serializers/{{name}}.py.j2: -------------------------------------------------------------------------------- 1 | from .base import Serializer 2 | from {{app}}.models import {{model_class_name}} 3 | 4 | class {{serializer_class_name}}(Serializer): 5 | class Meta: 6 | model = {{model_class_name}} 7 | name = '{{name}}' 8 | plural_name = '{{plural_name}}' 9 | fields = ('id', ) 10 | -------------------------------------------------------------------------------- /dynamic_rest/blueprints/api/templates/{{app}}/api/{{version}}/urls.py.j2: -------------------------------------------------------------------------------- 1 | from . import views 2 | from dynamic_rest.routers import DynamicRouter 3 | from inspect import isclass 4 | 5 | router = DynamicRouter() 6 | for name in dir(views): 7 | view = getattr(views, name, None) 8 | if ( 9 | isclass(view) and 10 | hasattr(view, 'serializer_class') 11 | ): 12 | router.register_resource(view) 13 | 14 | urlpatterns = router.urls 15 | -------------------------------------------------------------------------------- /dynamic_rest/blueprints/api/templates/{{app}}/api/{{version}}/views/__init__.py.j2: -------------------------------------------------------------------------------- 1 | from .{{name}} import {{viewset_class_name}} 2 | -------------------------------------------------------------------------------- /dynamic_rest/blueprints/api/templates/{{app}}/api/{{version}}/views/base.py.j2: -------------------------------------------------------------------------------- 1 | from dynamic_rest.viewsets import DynamicModelViewSet 2 | 3 | class ViewSet(DynamicModelViewSet): 4 | pass 5 | -------------------------------------------------------------------------------- /dynamic_rest/blueprints/api/templates/{{app}}/api/{{version}}/views/{{name}}.py.j2: -------------------------------------------------------------------------------- 1 | from ..serializers import {{serializer_class_name}} 2 | from .base import ViewSet 3 | from {{app}}.models import {{model_class_name}} 4 | 5 | class {{viewset_class_name}}(ViewSet): 6 | serializer_class = {{serializer_class_name}} 7 | model = {{model_class_name}} 8 | queryset = {{model_class_name}}.objects.all() 9 | -------------------------------------------------------------------------------- /dynamic_rest/blueprints/init/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/dynamic_rest/blueprints/init/__init__.py -------------------------------------------------------------------------------- /dynamic_rest/blueprints/init/context.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | 4 | @click.command() 5 | def get_context(): 6 | return {} 7 | -------------------------------------------------------------------------------- /dynamic_rest/blueprints/init/templates/{{app}}/settings.py.j2: -------------------------------------------------------------------------------- 1 | INSTALLED_APPS = [ 2 | 'dynamic_rest', 3 | 'rest_framework' 4 | ] 5 | -------------------------------------------------------------------------------- /dynamic_rest/conf.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | from django.conf import settings as django_settings 4 | from django.test.signals import setting_changed 5 | 6 | DYNAMIC_REST = { 7 | # DEBUG: enable/disable internal debugging 8 | 'DEBUG': False, 9 | 10 | # ENABLE_BROWSABLE_API: enable/disable the browsable API. 11 | # It can be useful to disable it in production. 12 | 'ENABLE_BROWSABLE_API': True, 13 | 14 | # ENABLE_LINKS: enable/disable relationship links 15 | 'ENABLE_LINKS': True, 16 | 17 | # ENABLE_SERIALIZER_CACHE: enable/disable caching of related serializers 18 | 'ENABLE_SERIALIZER_CACHE': True, 19 | 20 | # ENABLE_SERIALIZER_OBJECT_CACHE: enable/disable caching of serialized 21 | # objects within a serializer instance/context. This can yield 22 | # significant performance improvements in cases where the same objects 23 | # are sideloaded repeatedly. 24 | 'ENABLE_SERIALIZER_OBJECT_CACHE': True, 25 | 26 | # ENABLE_SERIALIZER_OPTIMIZATIONS: enable/disable representation speedups 27 | 'ENABLE_SERIALIZER_OPTIMIZATIONS': True, 28 | 29 | # ENABLE_BULK_PARTIAL_CREATION: enable/disable partial creation in bulk 30 | 'ENABLE_BULK_PARTIAL_CREATION': False, 31 | 32 | # ENABLE_BULK_UPDATE: enable/disable update in bulk 33 | 'ENABLE_BULK_UPDATE': True, 34 | 35 | # ENABLE_PATCH_ALL: enable/disable patch by queryset 36 | 'ENABLE_PATCH_ALL': False, 37 | 38 | # DEFER_MANY_RELATIONS: automatically defer many-relations, unless 39 | # `deferred=False` is explicitly set on the field. 40 | 'DEFER_MANY_RELATIONS': False, 41 | 42 | # LIST_SERIALIZER_CLASS: Globally override the list serializer class. 43 | # Default is `DynamicListSerializer` and also can be overridden for 44 | # each serializer class by setting `Meta.list_serializer_class`. 45 | 'LIST_SERIALIZER_CLASS': None, 46 | 47 | # MAX_PAGE_SIZE: global setting for max page size. 48 | # Can be overriden at the viewset level. 49 | 'MAX_PAGE_SIZE': None, 50 | 51 | # PAGE_QUERY_PARAM: global setting for the pagination query parameter. 52 | # Can be overriden at the viewset level. 53 | 'PAGE_QUERY_PARAM': 'page', 54 | 55 | # PAGE_SIZE: global setting for page size. 56 | # Can be overriden at the viewset level. 57 | 'PAGE_SIZE': None, 58 | 59 | # PAGE_SIZE_QUERY_PARAM: global setting for the page size query parameter. 60 | # Can be overriden at the viewset level. 61 | 'PAGE_SIZE_QUERY_PARAM': 'per_page', 62 | 63 | # EXCLUDE_COUNT_QUERY_PARAM: global setting for the query parameter 64 | # that disables counting during PageNumber pagination 65 | 'EXCLUDE_COUNT_QUERY_PARAM': 'exclude_count', 66 | 67 | # ADDITIONAL_PRIMARY_RESOURCE_PREFIX: String to prefix additional 68 | # instances of the primary resource when sideloading. 69 | 'ADDITIONAL_PRIMARY_RESOURCE_PREFIX': '+', 70 | 71 | # Enables host-relative links. Only compatible with resources registered 72 | # through the dynamic router. If a resource doesn't have a canonical 73 | # path registered, links will default back to being resource-relative urls 74 | 'ENABLE_HOST_RELATIVE_LINKS': True, 75 | 76 | # Enables caching of serializer fields to speed up serializer usage 77 | # Needs to also be configured on a per-serializer basis 78 | 'ENABLE_FIELDS_CACHE': False, 79 | 80 | # Enables use of hashid fields 81 | 'ENABLE_HASHID_FIELDS': False, 82 | 83 | # Salt value to salt hash ids. 84 | # Needs to be non-nullable if 'ENABLE_HASHID_FIELDS' is set to True 85 | 'HASHIDS_SALT': None, 86 | } 87 | 88 | 89 | # Attributes where the value should be a class (or path to a class) 90 | CLASS_ATTRS = [ 91 | 'LIST_SERIALIZER_CLASS', 92 | ] 93 | 94 | 95 | class Settings(object): 96 | def __init__(self, name, defaults, settings, class_attrs=None): 97 | self.name = name 98 | self.defaults = defaults 99 | self.keys = set(defaults.keys()) 100 | self.class_attrs = class_attrs 101 | 102 | self._cache = {} 103 | self._reload(getattr(settings, self.name, {})) 104 | 105 | setting_changed.connect(self._settings_changed) 106 | 107 | def _reload(self, value): 108 | """Reload settings after a change.""" 109 | self.settings = value 110 | self._cache = {} 111 | 112 | def _load_class(self, attr, val): 113 | if inspect.isclass(val): 114 | return val 115 | elif isinstance(val, str): 116 | parts = val.split('.') 117 | module_path = '.'.join(parts[:-1]) 118 | class_name = parts[-1] 119 | mod = __import__(module_path, fromlist=[class_name]) 120 | return getattr(mod, class_name) 121 | elif val: 122 | raise Exception("%s must be string or a class" % attr) 123 | 124 | def __getattr__(self, attr): 125 | """Get a setting.""" 126 | if attr not in self._cache: 127 | 128 | if attr not in self.keys: 129 | raise AttributeError("Invalid API setting: '%s'" % attr) 130 | 131 | if attr in self.settings: 132 | val = self.settings[attr] 133 | else: 134 | val = self.defaults[attr] 135 | 136 | if attr in self.class_attrs and val: 137 | val = self._load_class(attr, val) 138 | 139 | # Cache the result 140 | self._cache[attr] = val 141 | 142 | return self._cache[attr] 143 | 144 | def _settings_changed(self, *args, **kwargs): 145 | """Handle changes to core settings.""" 146 | setting, value = kwargs['setting'], kwargs['value'] 147 | if setting == self.name: 148 | self._reload(value) 149 | 150 | 151 | settings = Settings('DYNAMIC_REST', DYNAMIC_REST, django_settings, CLASS_ATTRS) 152 | -------------------------------------------------------------------------------- /dynamic_rest/constants.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/dynamic_rest/constants.py -------------------------------------------------------------------------------- /dynamic_rest/datastructures.py: -------------------------------------------------------------------------------- 1 | """This module contains custom data-structures.""" 2 | import six 3 | 4 | 5 | class TreeMap(dict): 6 | """Tree structure implemented with nested dictionaries.""" 7 | 8 | def get_paths(self): 9 | """Get all paths from the root to the leaves. 10 | 11 | For example, given a chain like `{'a':{'b':{'c':None}}}`, 12 | this method would return `[['a', 'b', 'c']]`. 13 | 14 | Returns: 15 | A list of lists of paths. 16 | """ 17 | paths = [] 18 | for key, child in six.iteritems(self): 19 | if isinstance(child, TreeMap) and child: 20 | # current child is an intermediate node 21 | for path in child.get_paths(): 22 | path.insert(0, key) 23 | paths.append(path) 24 | else: 25 | # current child is an endpoint 26 | paths.append([key]) 27 | return paths 28 | 29 | def insert(self, parts, leaf_value, update=False): 30 | """Add a list of nodes into the tree. 31 | 32 | The list will be converted into a TreeMap (chain) and then 33 | merged with the current TreeMap. 34 | 35 | For example, this method would insert `['a','b','c']` as 36 | `{'a':{'b':{'c':{}}}}`. 37 | 38 | Arguments: 39 | parts: List of nodes representing a chain. 40 | leaf_value: Value to insert into the leaf of the chain. 41 | update: Whether or not to update the leaf with the given value or 42 | to replace the value. 43 | 44 | Returns: 45 | self 46 | """ 47 | tree = self 48 | if not parts: 49 | return tree 50 | 51 | cur = tree 52 | last = len(parts) - 1 53 | for i, part in enumerate(parts): 54 | if part not in cur: 55 | cur[part] = TreeMap() if i != last else leaf_value 56 | elif i == last: # found leaf 57 | if update: 58 | cur[part].update(leaf_value) 59 | else: 60 | cur[part] = leaf_value 61 | 62 | cur = cur[part] 63 | 64 | return self 65 | -------------------------------------------------------------------------------- /dynamic_rest/fields/__init__.py: -------------------------------------------------------------------------------- 1 | from dynamic_rest.fields.fields import * # noqa 2 | from dynamic_rest.fields.generic import * # noqa 3 | -------------------------------------------------------------------------------- /dynamic_rest/fields/common.py: -------------------------------------------------------------------------------- 1 | class WithRelationalFieldMixin(object): 2 | """Mostly code shared by DynamicRelationField and 3 | DynamicGenericRelationField. 4 | """ 5 | 6 | def _get_request_fields_from_parent(self): 7 | """Get request fields from the parent serializer.""" 8 | if not self.parent: 9 | return None 10 | 11 | if not getattr(self.parent, 'request_fields'): 12 | return None 13 | 14 | if not isinstance(self.parent.request_fields, dict): 15 | return None 16 | 17 | return self.parent.request_fields.get(self.field_name) 18 | -------------------------------------------------------------------------------- /dynamic_rest/fields/generic.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | 3 | from rest_framework.exceptions import ValidationError 4 | 5 | from dynamic_rest.fields.common import WithRelationalFieldMixin 6 | from dynamic_rest.fields.fields import DynamicField 7 | from dynamic_rest.routers import DynamicRouter 8 | from dynamic_rest.tagged import TaggedDict 9 | 10 | 11 | class DynamicGenericRelationField( 12 | WithRelationalFieldMixin, 13 | DynamicField 14 | ): 15 | 16 | def __init__(self, embed=False, *args, **kwargs): 17 | if 'requires' in kwargs: 18 | raise RuntimeError( 19 | "DynamicGenericRelationField does not support manual" 20 | " overriding of 'requires'." 21 | ) 22 | 23 | super(DynamicGenericRelationField, self).__init__(*args, **kwargs) 24 | self.embed = embed 25 | 26 | def bind(self, field_name, parent): 27 | super(DynamicGenericRelationField, self).bind(field_name, parent) 28 | 29 | source = self.source or field_name 30 | 31 | # Inject `requires` so required fields get prefetched properly. 32 | # TODO: It seems like we should be able to require the type and 33 | # id fields, but that seems to conflict with some internal 34 | # Django magic. Disabling `.only()` by requiring '*' seem 35 | # to work more reliably... 36 | self.requires = [ 37 | source + '.*', 38 | '*' 39 | ] 40 | 41 | # Get request fields to support sideloading, but disallow field 42 | # inclusion/exclusion. 43 | request_fields = self._get_request_fields_from_parent() 44 | if isinstance(request_fields, dict) and len(request_fields): 45 | raise ValidationError( 46 | "%s.%s does not support field inclusion/exclusion" % ( 47 | self.parent.get_name(), 48 | self.field_name 49 | ) 50 | ) 51 | self.request_fields = request_fields 52 | 53 | def id_only(self): 54 | # For DynamicRelationFields, id_only() is a serializer responsibility 55 | # but for generic relations, we want IDs to be represented differently 56 | # and that is a field-level concern, not an object-level concern, 57 | # so we handle it here. 58 | return not self.parent.is_field_sideloaded(self.field_name) 59 | 60 | def get_pk_object(self, type_key, id_value): 61 | return { 62 | 'type': type_key, 63 | 'id': id_value 64 | } 65 | 66 | def get_serializer_class_for_instance(self, instance): 67 | return DynamicRouter.get_canonical_serializer( 68 | resource_key=None, 69 | instance=instance 70 | ) 71 | 72 | def to_representation(self, instance): 73 | try: 74 | # Find serializer for the instance 75 | serializer_class = self.get_serializer_class_for_instance(instance) 76 | if not serializer_class: 77 | # Can't find canonical serializer! For now, just return 78 | # object name and ID, and hope the client knows what to do 79 | # with it. 80 | return self.get_pk_object( 81 | instance._meta.object_name, 82 | instance.pk 83 | ) 84 | 85 | # We want the pk to be represented as an object with type, 86 | # rather than just the ID. 87 | pk_value = self.get_pk_object( 88 | serializer_class.get_name(), 89 | instance.pk 90 | ) 91 | if self.id_only(): 92 | return pk_value 93 | 94 | # Serialize the object. Note that request_fields is set, but 95 | # field inclusion/exclusion is disallowed via check in bind() 96 | r = serializer_class( 97 | dynamic=True, 98 | request_fields=self.request_fields, 99 | context=self.context, 100 | embed=self.embed 101 | ).to_representation( 102 | instance 103 | ) 104 | 105 | # Pass pk object that contains type and ID to TaggedDict object 106 | # so that Processor can use it when the field gets sideloaded. 107 | if isinstance(r, TaggedDict): 108 | r.pk_value = pk_value 109 | return r 110 | except BaseException: 111 | # This feature should be considered to be in Beta so don't break 112 | # if anything unexpected happens. 113 | # TODO: Remove once we have more confidence. 114 | traceback.print_exc() 115 | return None 116 | 117 | def to_internal_value(self, data): 118 | model_name = data.get('type', None) 119 | model_id = data.get('id', None) 120 | if model_name and model_id: 121 | serializer_class = DynamicRouter.get_canonical_serializer( 122 | resource_key=None, 123 | resource_name=model_name 124 | ) 125 | if serializer_class: 126 | model = serializer_class.get_model() 127 | return model.objects.get(id=model_id) if model else None 128 | 129 | return None 130 | -------------------------------------------------------------------------------- /dynamic_rest/links.py: -------------------------------------------------------------------------------- 1 | """This module contains utilities to support API links.""" 2 | import six 3 | from dynamic_rest.conf import settings 4 | from dynamic_rest.routers import DynamicRouter 5 | 6 | 7 | def merge_link_object(serializer, data, instance): 8 | """Add a 'links' attribute to the data that maps field names to URLs. 9 | 10 | NOTE: This is the format that Ember Data supports, but alternative 11 | implementations are possible to support other formats. 12 | """ 13 | 14 | link_object = {} 15 | 16 | if not getattr(instance, 'pk', None): 17 | # If instance doesn't have a `pk` field, we'll assume it doesn't 18 | # have a canonical resource URL to hang a link off of. 19 | # This generally only affectes Ephemeral Objects. 20 | return data 21 | 22 | link_fields = serializer.get_link_fields() 23 | for name, field in six.iteritems(link_fields): 24 | # For included fields, omit link if there's no data. 25 | if name in data and not data[name]: 26 | continue 27 | 28 | link = getattr(field, 'link', None) 29 | if link is None: 30 | base_url = '' 31 | if settings.ENABLE_HOST_RELATIVE_LINKS: 32 | # if the resource isn't registered, this will default back to 33 | # using resource-relative urls for links. 34 | base_url = DynamicRouter.get_canonical_path( 35 | serializer.get_resource_key(), 36 | instance.pk 37 | ) or '' 38 | link = '%s%s/' % (base_url, name) 39 | # Default to DREST-generated relation endpoints. 40 | elif callable(link): 41 | link = link(name, field, data, instance) 42 | 43 | link_object[name] = link 44 | 45 | if link_object: 46 | data['links'] = link_object 47 | return data 48 | -------------------------------------------------------------------------------- /dynamic_rest/meta.py: -------------------------------------------------------------------------------- 1 | """Module containing Django meta helpers.""" 2 | from itertools import chain 3 | 4 | from django.db.models import ManyToOneRel # tested in 1.9 5 | from django.db.models import OneToOneRel # tested in 1.9 6 | from django.db.models import ( 7 | ForeignKey, 8 | ManyToManyField, 9 | ManyToManyRel, 10 | OneToOneField 11 | ) 12 | 13 | from dynamic_rest.related import RelatedObject 14 | 15 | 16 | def is_model_field(model, field_name): 17 | """Check whether a given field exists on a model. 18 | 19 | Arguments: 20 | model: a Django model 21 | field_name: the name of a field 22 | 23 | Returns: 24 | True if `field_name` exists on `model`, False otherwise. 25 | """ 26 | try: 27 | get_model_field(model, field_name) 28 | return True 29 | except AttributeError: 30 | return False 31 | 32 | 33 | def get_model_field(model, field_name): 34 | """Return a field given a model and field name. 35 | 36 | Arguments: 37 | model: a Django model 38 | field_name: the name of a field 39 | 40 | Returns: 41 | A Django field if `field_name` is a valid field for `model`, 42 | None otherwise. 43 | """ 44 | meta = model._meta 45 | try: 46 | return meta.get_field(field_name) 47 | except BaseException: 48 | related_objs = ( 49 | f for f in meta.get_fields() 50 | if (f.one_to_many or f.one_to_one) 51 | and f.auto_created and not f.concrete 52 | ) 53 | related_m2m_objs = ( 54 | f for f in meta.get_fields(include_hidden=True) 55 | if f.many_to_many and f.auto_created 56 | ) 57 | 58 | related_objects = { 59 | o.get_accessor_name(): o 60 | for o in chain(related_objs, related_m2m_objs) 61 | } 62 | if field_name in related_objects: 63 | return related_objects[field_name] 64 | else: 65 | # check virtual fields (1.7) 66 | if hasattr(meta, 'virtual_fields'): 67 | for field in meta.virtual_fields: 68 | if field.name == field_name: 69 | return field 70 | 71 | raise AttributeError( 72 | '%s is not a valid field for %s' % (field_name, model) 73 | ) 74 | 75 | 76 | def get_model_field_and_type(model, field_name): 77 | field = get_model_field(model, field_name) 78 | 79 | # Django 1.7 (and 1.8?) 80 | if isinstance(field, RelatedObject): 81 | if isinstance(field.field, OneToOneField): 82 | return field, 'o2or' 83 | elif isinstance(field.field, ManyToManyField): 84 | return field, 'm2m' 85 | elif isinstance(field.field, ForeignKey): 86 | return field, 'm2o' 87 | else: 88 | raise RuntimeError("Unexpected field type") 89 | 90 | # Django 1.9 91 | type_map = [ 92 | (OneToOneField, 'o2o'), 93 | (OneToOneRel, 'o2or'), # is subclass of m2o so check first 94 | (ManyToManyField, 'm2m'), 95 | (ManyToOneRel, 'm2o'), 96 | (ManyToManyRel, 'm2m'), 97 | (ForeignKey, 'fk'), # check last 98 | ] 99 | for cls, type_str in type_map: 100 | if isinstance(field, cls): 101 | return field, type_str, 102 | 103 | return field, '', 104 | 105 | 106 | def is_field_remote(model, field_name): 107 | """Check whether a given model field is a remote field. 108 | 109 | A remote field is the inverse of a one-to-many or a 110 | many-to-many relationship. 111 | 112 | Arguments: 113 | model: a Django model 114 | field_name: the name of a field 115 | 116 | Returns: 117 | True if `field_name` is a remote field, False otherwise. 118 | """ 119 | if not hasattr(model, '_meta'): 120 | # ephemeral model with no metaclass 121 | return False 122 | 123 | model_field = get_model_field(model, field_name) 124 | return isinstance(model_field, (ManyToManyField, RelatedObject)) 125 | 126 | 127 | def get_related_model(field): 128 | try: 129 | # django 1.8+ 130 | return field.related_model 131 | except AttributeError: 132 | # django 1.7 133 | if hasattr(field, 'field'): 134 | return field.field.model 135 | elif hasattr(field, 'rel'): 136 | return field.rel.to 137 | elif field.__class__.__name__ == 'GenericForeignKey': 138 | return None 139 | else: 140 | raise 141 | 142 | 143 | def reverse_m2m_field_name(m2m_field): 144 | try: 145 | # Django 1.9 146 | return m2m_field.remote_field.name 147 | except BaseException: 148 | # Django 1.7 149 | if hasattr(m2m_field, 'rel'): 150 | return m2m_field.rel.related_name 151 | elif hasattr(m2m_field, 'field'): 152 | return m2m_field.field.name 153 | elif m2m_field.__class__.__name__ == 'GenericForeignKey': 154 | return None 155 | else: 156 | raise 157 | 158 | 159 | def reverse_o2o_field_name(o2or_field): 160 | try: 161 | # Django 1.9 162 | return o2or_field.remote_field.attname 163 | except BaseException: 164 | # Django 1.7 165 | return o2or_field.field.attname 166 | 167 | 168 | def get_remote_model(field): 169 | try: 170 | # Django 1.9 171 | return field.remote_field.model 172 | except BaseException: 173 | # Django 1.7 174 | if hasattr(field, 'field'): 175 | return field.field.model 176 | elif hasattr(field, 'rel'): 177 | return field.rel.to 178 | elif field.__class__.__name__ == 'GenericForeignKey': 179 | return None 180 | else: 181 | raise 182 | 183 | 184 | def get_model_table(model): 185 | try: 186 | return model._meta.db_table 187 | except BaseException: 188 | return None 189 | -------------------------------------------------------------------------------- /dynamic_rest/metadata.py: -------------------------------------------------------------------------------- 1 | """This module contains custom DRF metadata classes.""" 2 | from collections import OrderedDict 3 | 4 | try: 5 | from django.utils.encoding import force_str 6 | except ImportError: 7 | from django.utils.encoding import force_text as force_str 8 | 9 | from rest_framework.fields import empty 10 | from rest_framework.metadata import SimpleMetadata 11 | from rest_framework.serializers import ListSerializer, ModelSerializer 12 | 13 | from dynamic_rest.fields import DynamicRelationField 14 | 15 | 16 | class DynamicMetadata(SimpleMetadata): 17 | """A subclass of SimpleMetadata. 18 | 19 | Adds `properties` and `features` to the metdata. 20 | """ 21 | 22 | def determine_actions(self, request, view): 23 | """Prevent displaying action-specific details.""" 24 | return None 25 | 26 | def determine_metadata(self, request, view): 27 | """Adds `properties` and `features` to the metadata response.""" 28 | metadata = super( 29 | DynamicMetadata, 30 | self).determine_metadata( 31 | request, 32 | view) 33 | metadata['features'] = getattr(view, 'features', []) 34 | if hasattr(view, 'get_serializer'): 35 | serializer = view.get_serializer(dynamic=False) 36 | if hasattr(serializer, 'get_name'): 37 | metadata['resource_name'] = serializer.get_name() 38 | if hasattr(serializer, 'get_plural_name'): 39 | metadata['resource_name_plural'] = serializer.get_plural_name() 40 | metadata['properties'] = self.get_serializer_info(serializer) 41 | return metadata 42 | 43 | def get_field_info(self, field): 44 | """Adds `related_to` and `nullable` to the metadata response.""" 45 | field_info = OrderedDict() 46 | for attr in ('required', 'read_only', 'default', 'label'): 47 | field_info[attr] = getattr(field, attr) 48 | if field_info['default'] is empty: 49 | field_info['default'] = None 50 | if hasattr(field, 'immutable'): 51 | field_info['immutable'] = field.immutable 52 | field_info['nullable'] = field.allow_null 53 | if hasattr(field, 'choices'): 54 | field_info['choices'] = [ 55 | { 56 | 'value': choice_value, 57 | 'display_name': force_str(choice_name, strings_only=True) 58 | } 59 | for choice_value, choice_name in field.choices.items() 60 | ] 61 | many = False 62 | if isinstance(field, DynamicRelationField): 63 | field = field.serializer 64 | if isinstance(field, ListSerializer): 65 | field = field.child 66 | many = True 67 | if isinstance(field, ModelSerializer): 68 | type = 'many' if many else 'one' 69 | field_info['related_to'] = field.get_plural_name() 70 | else: 71 | type = self.label_lookup[field] 72 | 73 | field_info['type'] = type 74 | return field_info 75 | -------------------------------------------------------------------------------- /dynamic_rest/pagination.py: -------------------------------------------------------------------------------- 1 | """This module contains custom pagination classes.""" 2 | from collections import OrderedDict 3 | 4 | from rest_framework.pagination import PageNumberPagination 5 | from rest_framework.response import Response 6 | from rest_framework.settings import api_settings 7 | from rest_framework.exceptions import NotFound 8 | 9 | from django.utils.functional import cached_property 10 | from django.core.paginator import InvalidPage 11 | from dynamic_rest.conf import settings 12 | from dynamic_rest.paginator import DynamicPaginator 13 | 14 | 15 | class DynamicPageNumberPagination(PageNumberPagination): 16 | """A subclass of PageNumberPagination. 17 | 18 | Adds support for pagination metadata and overrides for 19 | pagination query parameters. 20 | """ 21 | 22 | page_size_query_param = settings.PAGE_SIZE_QUERY_PARAM 23 | exclude_count_query_param = settings.EXCLUDE_COUNT_QUERY_PARAM 24 | page_query_param = settings.PAGE_QUERY_PARAM 25 | max_page_size = settings.MAX_PAGE_SIZE 26 | page_size = settings.PAGE_SIZE or api_settings.PAGE_SIZE 27 | django_paginator_class = DynamicPaginator 28 | 29 | def get_page_metadata(self): 30 | # always returns page, per_page 31 | # also returns total_results and total_pages 32 | # (unless EXCLUDE_COUNT_QUERY_PARAM is set) 33 | meta = { 34 | 'page': self.page.number, 35 | 'per_page': self.get_page_size(self.request) 36 | } 37 | if not self.exclude_count: 38 | meta['total_results'] = self.page.paginator.count 39 | meta['total_pages'] = self.page.paginator.num_pages 40 | else: 41 | meta['more_pages'] = self.more_pages 42 | return meta 43 | 44 | def get_paginated_response(self, data): 45 | meta = self.get_page_metadata() 46 | result = None 47 | if isinstance(data, list): 48 | result = OrderedDict() 49 | if not self.exclude_count: 50 | result['count'] = self.page.paginator.count 51 | result['next'] = self.get_next_link() 52 | result['previous'] = self.get_previous_link() 53 | result['results'] = data 54 | result['meta'] = meta 55 | else: 56 | result = data 57 | if 'meta' in result: 58 | result['meta'].update(meta) 59 | else: 60 | result['meta'] = meta 61 | return Response(result) 62 | 63 | @cached_property 64 | def exclude_count(self): 65 | return self.request.query_params.get(self.exclude_count_query_param) 66 | 67 | def get_page_number(self, request, paginator): 68 | page_number = request.query_params.get(self.page_query_param, 1) 69 | if page_number in self.last_page_strings: 70 | page_number = paginator.num_pages 71 | return page_number 72 | 73 | def paginate_queryset(self, queryset, request, **other): 74 | """ 75 | Paginate a queryset if required, either returning a 76 | page object, or `None` if pagination is not configured for this view. 77 | """ 78 | if 'exclude_count' in self.__dict__: 79 | self.__dict__.pop('exclude_count') 80 | 81 | page_size = self.get_page_size(request) 82 | if not page_size: 83 | return None 84 | 85 | self.request = request 86 | paginator = self.django_paginator_class( 87 | queryset, page_size, exclude_count=self.exclude_count 88 | ) 89 | page_number = self.get_page_number(request, paginator) 90 | 91 | try: 92 | self.page = paginator.page(page_number) 93 | except InvalidPage as exc: 94 | msg = self.invalid_page_message.format( 95 | page_number=page_number, message=str(exc) 96 | ) 97 | raise NotFound(msg) 98 | 99 | if paginator.num_pages > 1 and self.template is not None: 100 | # The browsable API should display pagination controls. 101 | self.display_page_controls = True 102 | 103 | result = list(self.page) 104 | if self.exclude_count: 105 | if len(result) > page_size: 106 | # if exclude_count is set, we fetch one extra item 107 | result = result[:page_size] 108 | self.more_pages = True 109 | else: 110 | self.more_pages = False 111 | return result 112 | -------------------------------------------------------------------------------- /dynamic_rest/paginator.py: -------------------------------------------------------------------------------- 1 | # adapted from Django's django.core.paginator (2.2 - 3.2+ compatible) 2 | # adds support for the "exclude_count" parameter 3 | 4 | from math import ceil 5 | 6 | import inspect 7 | from django.utils.functional import cached_property 8 | from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage 9 | from django.utils.inspect import method_has_no_args 10 | 11 | try: 12 | from django.utils.translation import gettext_lazy as _ 13 | except ImportError: 14 | def _(x): 15 | return x 16 | 17 | 18 | class DynamicPaginator(Paginator): 19 | 20 | def __init__(self, *args, **kwargs): 21 | self.exclude_count = kwargs.pop('exclude_count', False) 22 | super().__init__(*args, **kwargs) 23 | 24 | def validate_number(self, number): 25 | """Validate the given 1-based page number.""" 26 | try: 27 | number = int(number) 28 | except (TypeError, ValueError): 29 | raise PageNotAnInteger(_('That page number is not an integer')) 30 | if number < 1: 31 | raise EmptyPage(_('That page number is less than 1')) 32 | if self.exclude_count: 33 | # skip validating against num_pages 34 | return number 35 | if number > self.num_pages: 36 | if number == 1 and self.allow_empty_first_page: 37 | pass 38 | else: 39 | raise EmptyPage(_('That page contains no results')) 40 | return number 41 | 42 | def page(self, number): 43 | """Return a Page object for the given 1-based page number.""" 44 | number = self.validate_number(number) 45 | bottom = (number - 1) * self.per_page 46 | top = bottom + self.per_page 47 | if self.exclude_count: 48 | # always fetch one extra item 49 | # to determine if more pages are available 50 | # and skip validation against count 51 | top = top + 1 52 | else: 53 | if top + self.orphans >= self.count: 54 | top = self.count 55 | return self._get_page(self.object_list[bottom:top], number, self) 56 | 57 | @cached_property 58 | def count(self): 59 | """Return the total number of objects, across all pages.""" 60 | if self.exclude_count: 61 | # always return 0, count should not be called 62 | return 0 63 | 64 | c = getattr(self.object_list, 'count', None) 65 | if callable(c) and not inspect.isbuiltin(c) and method_has_no_args(c): 66 | return c() 67 | return len(self.object_list) 68 | 69 | @cached_property 70 | def num_pages(self): 71 | """Return the total number of pages.""" 72 | if self.exclude_count: 73 | # always return 1, count should not be called 74 | return 1 75 | 76 | if self.count == 0 and not self.allow_empty_first_page: 77 | return 0 78 | hits = max(1, self.count - self.orphans) 79 | return int(ceil(hits / float(self.per_page))) 80 | -------------------------------------------------------------------------------- /dynamic_rest/patches.py: -------------------------------------------------------------------------------- 1 | """This module contains patches for Django issues. 2 | 3 | These patches are meant to be short-lived and are 4 | extracted from Django code changes. 5 | """ 6 | 7 | 8 | def patch_prefetch_one_level(): 9 | """ 10 | This patch address Django bug https://code.djangoproject.com/ticket/24873, 11 | which was merged into Django master 12 | in commit 025c6553771a09b80563baedb5b8300a8b01312f 13 | into django.db.models.query. 14 | 15 | The code that follows is identical to the code in the above commit, 16 | with all comments stripped out. 17 | """ 18 | import copy 19 | import django 20 | 21 | def prefetch_one_level(instances, prefetcher, lookup, level): 22 | rel_qs, rel_obj_attr, instance_attr, single, cache_name = ( 23 | prefetcher.get_prefetch_queryset( 24 | instances, lookup.get_current_queryset(level))) 25 | 26 | additional_lookups = [ 27 | copy.copy(additional_lookup) for additional_lookup 28 | in getattr(rel_qs, '_prefetch_related_lookups', []) 29 | ] 30 | if additional_lookups: 31 | rel_qs._prefetch_related_lookups = [] 32 | 33 | all_related_objects = list(rel_qs) 34 | 35 | rel_obj_cache = {} 36 | for rel_obj in all_related_objects: 37 | rel_attr_val = rel_obj_attr(rel_obj) 38 | rel_obj_cache.setdefault(rel_attr_val, []).append(rel_obj) 39 | 40 | for obj in instances: 41 | instance_attr_val = instance_attr(obj) 42 | vals = rel_obj_cache.get(instance_attr_val, []) 43 | to_attr, as_attr = lookup.get_current_to_attr(level) 44 | if single: 45 | val = vals[0] if vals else None 46 | to_attr = to_attr if as_attr else cache_name 47 | setattr(obj, to_attr, val) 48 | else: 49 | if as_attr: 50 | setattr(obj, to_attr, vals) 51 | else: 52 | qs = getattr(obj, to_attr).all() 53 | qs._result_cache = vals 54 | qs._prefetch_done = True 55 | obj._prefetched_objects_cache[cache_name] = qs 56 | return all_related_objects, additional_lookups 57 | 58 | # apply the patch 59 | from django.db.models import query 60 | 61 | if django.VERSION < (2, 0, 0): 62 | query.prefetch_one_level = prefetch_one_level 63 | -------------------------------------------------------------------------------- /dynamic_rest/processors.py: -------------------------------------------------------------------------------- 1 | """This module contains response processors.""" 2 | from collections import defaultdict 3 | 4 | import six 5 | from rest_framework.serializers import ListSerializer 6 | from rest_framework.utils.serializer_helpers import ReturnDict 7 | 8 | from dynamic_rest.conf import settings 9 | from dynamic_rest.tagged import TaggedDict 10 | 11 | 12 | POST_PROCESSORS = {} 13 | 14 | 15 | def register_post_processor(func): 16 | """ 17 | Register a post processor function to be run as the final step in 18 | serialization. The data passed in will already have gone through the 19 | sideloading processor. 20 | 21 | Usage: 22 | @register_post_processor 23 | def my_post_processor(data): 24 | # do stuff with `data` 25 | return data 26 | """ 27 | 28 | global POST_PROCESSORS 29 | 30 | key = func.__name__ 31 | POST_PROCESSORS[key] = func 32 | return func 33 | 34 | 35 | def post_process(data): 36 | """Apply registered post-processors to data.""" 37 | 38 | for post_processor in POST_PROCESSORS.values(): 39 | data = post_processor(data) 40 | 41 | return data 42 | 43 | 44 | class SideloadingProcessor(object): 45 | """A processor that sideloads serializer data. 46 | 47 | Sideloaded records are returned under top-level 48 | response keys and produces responses that are 49 | typically smaller than their nested equivalent. 50 | """ 51 | 52 | def __init__(self, serializer, data): 53 | """Initializes and runs the processor. 54 | 55 | Arguments: 56 | serializer: a DREST serializer 57 | data: the serializer's representation 58 | """ 59 | 60 | if isinstance(serializer, ListSerializer): 61 | serializer = serializer.child 62 | self.data = {} 63 | self.seen = defaultdict(set) 64 | self.plural_name = serializer.get_plural_name() 65 | self.name = serializer.get_name() 66 | 67 | # process the data, optionally sideloading 68 | self.process(data) 69 | 70 | # add the primary resource data into the response data 71 | resource_name = self.name if isinstance( 72 | data, 73 | dict 74 | ) else self.plural_name 75 | self.data[resource_name] = data 76 | 77 | def is_dynamic(self, data): 78 | """Check whether the given data dictionary is a DREST structure. 79 | 80 | Arguments: 81 | data: A dictionary representation of a DRF serializer. 82 | """ 83 | return isinstance(data, TaggedDict) 84 | 85 | def process(self, obj, parent=None, parent_key=None, depth=0): 86 | """Recursively process the data for sideloading. 87 | 88 | Converts the nested representation into a sideloaded representation. 89 | """ 90 | if isinstance(obj, list): 91 | for key, o in enumerate(obj): 92 | # traverse into lists of objects 93 | self.process(o, parent=obj, parent_key=key, depth=depth) 94 | elif isinstance(obj, dict): 95 | dynamic = self.is_dynamic(obj) 96 | returned = isinstance(obj, ReturnDict) 97 | if dynamic or returned: 98 | # recursively check all fields 99 | for key, o in six.iteritems(obj): 100 | if isinstance(o, list) or isinstance(o, dict): 101 | # lists or dicts indicate a relation 102 | self.process( 103 | o, 104 | parent=obj, 105 | parent_key=key, 106 | depth=depth + 107 | 1 108 | ) 109 | 110 | if not dynamic or getattr(obj, 'embed', False): 111 | return 112 | 113 | serializer = obj.serializer 114 | name = serializer.get_plural_name() 115 | instance = getattr(obj, 'instance', serializer.instance) 116 | instance_pk = instance.pk if instance else None 117 | pk = getattr(obj, 'pk_value', instance_pk) or instance_pk 118 | 119 | # For polymorphic relations, `pk` can be a dict, so use the 120 | # string representation (dict isn't hashable). 121 | pk_key = repr(pk) 122 | 123 | # sideloading 124 | seen = True 125 | # if this object has not yet been seen 126 | if pk_key not in self.seen[name]: 127 | seen = False 128 | self.seen[name].add(pk_key) 129 | 130 | # prevent sideloading the primary objects 131 | if depth == 0: 132 | return 133 | 134 | # TODO: spec out the exact behavior for secondary instances of 135 | # the primary resource 136 | 137 | # if the primary resource is embedded, add it to a prefixed key 138 | if name == self.plural_name: 139 | name = '%s%s' % ( 140 | settings.ADDITIONAL_PRIMARY_RESOURCE_PREFIX, 141 | name 142 | ) 143 | 144 | if not seen: 145 | # allocate a top-level key in the data for this resource 146 | # type 147 | if name not in self.data: 148 | self.data[name] = [] 149 | 150 | # move the object into a new top-level bucket 151 | # and mark it as seen 152 | self.data[name].append(obj) 153 | else: 154 | # obj sideloaded, but maybe with other fields 155 | for o in self.data.get(name, []): 156 | if o.instance.pk == pk: 157 | o.update(obj) 158 | break 159 | 160 | # replace the object with a reference 161 | if parent is not None and parent_key is not None: 162 | parent[parent_key] = pk 163 | -------------------------------------------------------------------------------- /dynamic_rest/related.py: -------------------------------------------------------------------------------- 1 | """This module provides backwards compatibility for RelatedObject.""" 2 | # flake8: noqa 3 | 4 | try: 5 | # Django <= 1.7 6 | from django.db.models.related import RelatedObject 7 | except: 8 | # Django >= 1.8 9 | # See: https://code.djangoproject.com/ticket/21414 10 | from django.db.models.fields.related import ( 11 | ForeignObjectRel as RelatedObject 12 | ) 13 | -------------------------------------------------------------------------------- /dynamic_rest/renderers.py: -------------------------------------------------------------------------------- 1 | """This module contains custom renderer classes.""" 2 | from rest_framework.renderers import BrowsableAPIRenderer 3 | 4 | 5 | class DynamicBrowsableAPIRenderer(BrowsableAPIRenderer): 6 | """Renderer class that adds directory support to the Browsable API.""" 7 | 8 | template = 'dynamic_rest/api.html' 9 | 10 | def get_context(self, data, media_type, context): 11 | from dynamic_rest.routers import get_directory 12 | 13 | context = super(DynamicBrowsableAPIRenderer, self).get_context( 14 | data, 15 | media_type, 16 | context 17 | ) 18 | request = context['request'] 19 | context['directory'] = get_directory(request) 20 | return context 21 | -------------------------------------------------------------------------------- /dynamic_rest/static/dynamic_rest/css/default.css: -------------------------------------------------------------------------------- 1 | .main ul.breadcrumb { 2 | margin-top: 0px; 3 | } 4 | 5 | @media screen and (max-width: 1800px) { 6 | .directory { 7 | display: none; 8 | } 9 | } 10 | 11 | .directory { 12 | font-family: 'Open Sans', 'Helvetica Neue', Helvetica, sans-serif; 13 | position: fixed; 14 | left: 0; 15 | top: 50px; 16 | bottom: 0; 17 | overflow: auto; 18 | min-width: 320px; 19 | padding-top: 20px; 20 | background-color: #222; 21 | } 22 | 23 | .directory ul { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | .directory li { 29 | margin: 0; 30 | padding-left: 10px; 31 | list-style-type: none; 32 | } 33 | 34 | .directory li a:hover { 35 | color: white; 36 | text-decoration: none; 37 | } 38 | 39 | .directory li ul { 40 | padding-left: 20px; 41 | } 42 | 43 | a { 44 | text-decoration: none; 45 | } 46 | 47 | .directory a, .directory li { 48 | display: block; 49 | font-size: 16px; 50 | color: rgba(255, 255, 255, .60); 51 | } 52 | 53 | .directory a.active, .directory li.active { 54 | color: white; 55 | } 56 | 57 | html, body { 58 | margin: 0; 59 | padding: 0; 60 | height: 100%; 61 | } 62 | -------------------------------------------------------------------------------- /dynamic_rest/tagged.py: -------------------------------------------------------------------------------- 1 | """This module contains tagging utilities for DREST data structures.""" 2 | from collections import OrderedDict 3 | 4 | 5 | def tag_dict(obj, *args, **kwargs): 6 | """Create a TaggedDict instance. Will either be a TaggedOrderedDict 7 | or TaggedPlainDict depending on the type of `obj`.""" 8 | 9 | if isinstance(obj, OrderedDict): 10 | return _TaggedOrderedDict(obj, *args, **kwargs) 11 | else: 12 | return _TaggedPlainDict(obj, *args, **kwargs) 13 | 14 | 15 | class TaggedDict(object): 16 | 17 | """ 18 | Return object from `to_representation` for the `Serializer` class. 19 | Includes a reference to the `instance` and the `serializer` represented. 20 | """ 21 | 22 | def __init__(self, *args, **kwargs): 23 | self.serializer = kwargs.pop('serializer') 24 | self.instance = kwargs.pop('instance') 25 | self.embed = kwargs.pop('embed', False) 26 | self.pk_value = kwargs.pop('pk_value', None) 27 | if not isinstance(self, dict): 28 | raise Exception( 29 | "TaggedDict constructed not as a dict" 30 | ) 31 | super(TaggedDict, self).__init__(*args, **kwargs) 32 | 33 | def copy(self): 34 | return tag_dict( 35 | self, 36 | serializer=self.serializer, 37 | instance=self.instance, 38 | embed=self.embed, 39 | pk_value=self.pk_value 40 | ) 41 | 42 | def __repr__(self): 43 | return dict.__repr__(self) 44 | 45 | def __reduce__(self): 46 | return (dict, (dict(self),)) 47 | 48 | 49 | class _TaggedPlainDict(TaggedDict, dict): 50 | pass 51 | 52 | 53 | class _TaggedOrderedDict(TaggedDict, OrderedDict): 54 | pass 55 | -------------------------------------------------------------------------------- /dynamic_rest/templates/dynamic_rest/api.html: -------------------------------------------------------------------------------- 1 | {% extends "rest_framework/base.html" %} 2 | 3 | {% load rest_framework %} 4 | {% load static %} 5 | 6 | 7 | {% block breadcrumbs %} 8 | 9 | 18 | {# the directory is shoehorned into the breadcrumbs block because there is no other block to fit it into #} 19 | {% block directory %} 20 |
21 |
  • / 22 |
      23 | {% for l1_name, l1_link, l1_items, l1_active in directory %} 24 |
    • 25 | {% if l1_link %} 26 | {{l1_name}}/ 27 | {% else %} 28 | {{l1_name}}/ 29 | {% endif %} 30 |
        31 | {% for l2_name, l2_link, l2_items, l2_active in l1_items %} 32 |
      • 33 | {% if l2_link %} 34 | {{l2_name}}/ 35 | {% else %} 36 | {{l2_name}}/ 37 | {% endif %} 38 |
      • 39 | {% endfor %} 40 |
      41 |
    • 42 | {% endfor %} 43 |
    44 |
  • 45 |
    46 | {% endblock %} 47 | {% endblock %} 48 | -------------------------------------------------------------------------------- /dynamic_rest/utils.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.contenttypes.models import ContentType 3 | from django.utils.module_loading import import_string 4 | 5 | from hashids import Hashids 6 | 7 | from six import string_types 8 | 9 | from dynamic_rest.conf import settings 10 | 11 | 12 | FALSEY_STRINGS = ( 13 | '0', 14 | 'false', 15 | '', 16 | ) 17 | 18 | 19 | def is_truthy(x): 20 | if isinstance(x, string_types): 21 | return x.lower() not in FALSEY_STRINGS 22 | return bool(x) 23 | 24 | 25 | def unpack(content): 26 | if not content: 27 | # empty values pass through 28 | return content 29 | 30 | keys = [k for k in content.keys() if k != 'meta'] 31 | unpacked = content[keys[0]] 32 | return unpacked 33 | 34 | 35 | def external_id_from_model_and_internal_id(model, internal_id): 36 | """ 37 | Return a hash for the model and internal ID combination. 38 | """ 39 | hashids = Hashids(salt=settings.HASHIDS_SALT) 40 | 41 | if hashids is None: 42 | raise AssertionError( 43 | "To use hashids features you must set " 44 | "ENABLE_HASHID_FIELDS to true " 45 | "and provide a HASHIDS_SALT in your dynamic_rest settings.") 46 | return hashids.encode( 47 | ContentType.objects.get_for_model(model).id, internal_id) 48 | 49 | 50 | def internal_id_from_model_and_external_id(model, external_id): 51 | """ 52 | Return the internal ID from the external ID and model combination. 53 | 54 | Because the HashId is a combination of the model's content type and the 55 | internal ID, we validate here that the external ID decodes as expected, 56 | and that the content type corresponds to the model we're expecting. 57 | """ 58 | hashids = Hashids(salt=settings.HASHIDS_SALT) 59 | 60 | if hashids is None: 61 | raise AssertionError( 62 | "To use hashids features you must set " 63 | "ENABLE_HASHID_FIELDS to true " 64 | "and provide a HASHIDS_SALT in your dynamic_rest settings.") 65 | 66 | try: 67 | content_type_id, instance_id = hashids.decode(external_id) 68 | except (TypeError, ValueError): 69 | raise model.DoesNotExist 70 | 71 | content_type = ContentType.objects.get_for_id(content_type_id) 72 | 73 | if content_type.model_class() != model: 74 | raise model.DoesNotExist 75 | 76 | return instance_id 77 | 78 | 79 | def model_from_definition(model_definition): 80 | """ 81 | Return a Django model corresponding to model_definition. 82 | 83 | Model definition can either be a string defining how to import the model, 84 | or a model class. 85 | 86 | Arguments: 87 | model_definition: (str|django.db.models.Model) 88 | 89 | Returns: 90 | (django.db.models.Model) 91 | 92 | Implementation from 93 | https://github.com/evenicoulddoit/django-rest-framework-serializer-extensions 94 | """ 95 | if isinstance(model_definition, str): 96 | model = import_string(model_definition) 97 | else: 98 | model = model_definition 99 | 100 | try: 101 | assert issubclass(model, models.Model) 102 | except (AssertionError, TypeError): 103 | raise AssertionError( 104 | '"{0}"" is not a Django model'.format(model_definition)) 105 | 106 | return model 107 | -------------------------------------------------------------------------------- /images/benchmark-cubic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/images/benchmark-cubic.png -------------------------------------------------------------------------------- /images/benchmark-linear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/images/benchmark-linear.png -------------------------------------------------------------------------------- /images/benchmark-quadratic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/images/benchmark-quadratic.png -------------------------------------------------------------------------------- /images/directory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/images/directory.png -------------------------------------------------------------------------------- /install_requires.txt: -------------------------------------------------------------------------------- 1 | Django>=3.2,<4.3 2 | djangorestframework>=3.13,<3.16 3 | inflection>=0.4.0 4 | requests 5 | hashids>=1.3.1 6 | six>=1.16.0 7 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") 7 | from django.core.management import execute_from_command_line 8 | execute_from_command_line(sys.argv) 9 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --tb=short -q -s -rw 3 | DJANGO_SETTINGS_MODULE=tests.settings 4 | -------------------------------------------------------------------------------- /requirements.benchmark.txt: -------------------------------------------------------------------------------- 1 | dj-database-url==0.3.0 2 | django-debug-toolbar==1.7 3 | Django>=2.2,<4.3 4 | djangorestframework>=3.11.2,<3.15 5 | djay>=0.0.9 6 | flake8>=3.0 7 | psycopg2-binary==2.9.3 8 | pytest-cov==3.0.0 9 | pytest-django==4.5.2 10 | pytest-sugar==0.9.5 11 | pytest==7.1.2 12 | Sphinx==1.7.5 13 | tox-pyenv==1.1.0 14 | tox==3.25.1 15 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | dj-database-url==0.5.0 2 | djay>=0.0.9 3 | flake8>=3.0 4 | hashids==1.3.1 5 | mock==2.0.0 6 | psycopg2-binary==2.9.3 7 | pytest-cov==3.0.0 8 | pytest-django==4.5.2 9 | pytest-sugar==0.9.5 10 | pytest>=7.0.0 11 | six==1.16.0 12 | Sphinx==1.7.5 13 | tox-pyenv==1.1.0 14 | tox==3.25.1 15 | twine==3.8.0 16 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # Adopted from Django REST Framework: 3 | # https://github.com/tomchristie/django-rest-framework/blob/master/runtests.py 4 | from __future__ import print_function 5 | 6 | import os 7 | import subprocess 8 | import sys 9 | 10 | import pytest 11 | 12 | APP_NAME = 'dynamic_rest' 13 | TESTS = 'tests' 14 | BENCHMARKS = 'benchmarks' 15 | PYTEST_ARGS = { 16 | 'default': [ 17 | TESTS, '--tb=short', '-s', '-rw' 18 | ], 19 | 'fast': [ 20 | TESTS, '--tb=short', '-q', '-s', '-rw' 21 | ], 22 | } 23 | 24 | FLAKE8_ARGS = [APP_NAME, TESTS] 25 | 26 | sys.path.append(os.path.dirname(__file__)) 27 | 28 | 29 | def exit_on_failure(ret, message=None): 30 | if ret: 31 | sys.exit(ret) 32 | 33 | 34 | def flake8_main(args): 35 | print('Running flake8 code linting') 36 | ret = subprocess.call(['flake8'] + args) 37 | print('flake8 failed' if ret else 'flake8 passed') 38 | return ret 39 | 40 | 41 | def split_class_and_function(string): 42 | class_string, function_string = string.split('.', 1) 43 | return "%s and %s" % (class_string, function_string) 44 | 45 | 46 | def is_function(string): 47 | # `True` if it looks like a test function is included in the string. 48 | return string.startswith('test_') or '.test_' in string 49 | 50 | 51 | def is_class(string): 52 | # `True` if first character is uppercase - assume it's a class name. 53 | return string[0] == string[0].upper() 54 | 55 | 56 | if __name__ == "__main__": 57 | try: 58 | sys.argv.remove('--nolint') 59 | except ValueError: 60 | run_flake8 = True 61 | else: 62 | run_flake8 = False 63 | 64 | try: 65 | sys.argv.remove('--lintonly') 66 | except ValueError: 67 | run_tests = True 68 | else: 69 | run_tests = False 70 | 71 | try: 72 | sys.argv.remove('--benchmarks') 73 | except ValueError: 74 | run_benchmarks = False 75 | else: 76 | run_benchmarks = True 77 | 78 | try: 79 | sys.argv.remove('--fast') 80 | except ValueError: 81 | style = 'default' 82 | else: 83 | style = 'fast' 84 | run_flake8 = False 85 | 86 | if len(sys.argv) > 1: 87 | pytest_args = sys.argv[1:] 88 | first_arg = pytest_args[0] 89 | 90 | try: 91 | pytest_args.remove('--coverage') 92 | except ValueError: 93 | pass 94 | else: 95 | pytest_args = [ 96 | '--cov-report', 97 | 'xml', 98 | '--cov', 99 | APP_NAME 100 | ] + 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_benchmarks: 117 | pytest_args[0] = BENCHMARKS 118 | pytest_args.append('--ds=%s.settings' % BENCHMARKS) 119 | 120 | if run_tests: 121 | exit_on_failure(pytest.main(pytest_args)) 122 | 123 | if run_flake8: 124 | exit_on_failure(flake8_main(FLAKE8_ARGS)) 125 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.rst 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | NAME = 'dynamic-rest' 4 | DESCRIPTION = 'Dynamic API support to Django REST Framework.' 5 | URL = 'http://github.com/AltSchool/dynamic-rest' 6 | VERSION = '2.3.0' 7 | SCRIPTS = ['manage.py'] 8 | 9 | setup( 10 | description=DESCRIPTION, 11 | include_package_data=True, 12 | install_requires=open('install_requires.txt').readlines(), 13 | long_description=open('README.rst').read(), 14 | name=NAME, 15 | packages=find_packages(), 16 | scripts=SCRIPTS, 17 | url=URL, 18 | version=VERSION, 19 | classifiers=[ 20 | 'Framework :: Django', 21 | 'Intended Audience :: Developers', 22 | 'License :: OSI Approved :: BSD License', 23 | 'Programming Language :: Python', 24 | 'Programming Language :: Python :: 3', 25 | 'Programming Language :: Python :: 3.7', 26 | 'Programming Language :: Python :: 3.8', 27 | 'Programming Language :: Python :: 3.9', 28 | 'Programming Language :: Python :: 3.10', 29 | 'Programming Language :: Python :: 3.11', 30 | 'Topic :: Software Development :: Libraries :: Python Modules', 31 | ], 32 | ) 33 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/tests/__init__.py -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/tests/integration/__init__.py -------------------------------------------------------------------------------- /tests/integration/test_blueprints.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase, skipIf 2 | from django.conf import settings 3 | import requests 4 | import time 5 | import json 6 | import os 7 | 8 | try: 9 | from djay.test import TemporaryApplication 10 | except ImportError: 11 | from dj.test import TemporaryApplication 12 | 13 | 14 | class DJBlueprintsTestCase(TestCase): 15 | 16 | @skipIf( 17 | not settings.ENABLE_INTEGRATION_TESTS, 18 | 'Integration tests disabled' 19 | ) 20 | def test_blueprints(self): 21 | params = { 22 | "app": "dummy", 23 | "description": "dummy", 24 | "author": "dummy", 25 | "email": "dummy@foo.com", 26 | "version": "0.0.1", 27 | "django_version": "2.2", 28 | } 29 | # generate a test application 30 | application = TemporaryApplication(params=params) 31 | # add a model 32 | application.execute('generate model foo --not-interactive') 33 | # create and apply migrations 34 | application.execute('migrate') 35 | # add this project as a dependency 36 | # this file is ROOT/tests/integration/test_blueprints.py 37 | root = os.path.abspath(os.path.join(__file__, '../../..')) 38 | application.execute('add %s --dev --not-interactive' % root) 39 | # generate an API endpoint for the generated model 40 | application.execute('generate api v0 foo --not-interactive') 41 | # start the server 42 | server = application.execute('serve 9123', run_async=True) 43 | time.sleep(2) 44 | 45 | # verify a simple POST flow for the "foo" resource 46 | response = requests.post('http://localhost:9123/api/v0/foos/') 47 | self.assertTrue(response.status_code, 201) 48 | content = json.loads(response.content) 49 | self.assertEquals(content, {'foo': {'id': 1}}) 50 | 51 | # stop the server 52 | server.terminate() 53 | -------------------------------------------------------------------------------- /tests/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/tests/management/__init__.py -------------------------------------------------------------------------------- /tests/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/tests/management/commands/__init__.py -------------------------------------------------------------------------------- /tests/management/commands/initialize_fixture.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | from tests.setup import create_fixture 4 | 5 | 6 | class Command(BaseCommand): 7 | help = 'Loads fixture data' 8 | 9 | def handle(self, *args, **options): 10 | create_fixture() 11 | 12 | self.stdout.write("Loaded fixtures.") 13 | -------------------------------------------------------------------------------- /tests/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # flake8: noqa 3 | # Generated by Django 1.9 on 2016-01-27 16:29 4 | from __future__ import unicode_literals 5 | 6 | import django.db.models.deletion 7 | from django.db import migrations, models 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='A', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('name', models.TextField(blank=True)), 23 | ], 24 | ), 25 | migrations.CreateModel( 26 | name='B', 27 | fields=[ 28 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 29 | ('a', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='b', to='tests.A')), 30 | ], 31 | ), 32 | migrations.CreateModel( 33 | name='C', 34 | fields=[ 35 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 36 | ('b', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cs', to='tests.B')), 37 | ], 38 | ), 39 | migrations.CreateModel( 40 | name='Cat', 41 | fields=[ 42 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 43 | ('name', models.TextField()), 44 | ], 45 | ), 46 | migrations.CreateModel( 47 | name='D', 48 | fields=[ 49 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 50 | ('name', models.TextField(blank=True)), 51 | ], 52 | ), 53 | migrations.CreateModel( 54 | name='Dog', 55 | fields=[ 56 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 57 | ('name', models.TextField()), 58 | ('fur_color', models.TextField()), 59 | ('origin', models.TextField()), 60 | ], 61 | ), 62 | migrations.CreateModel( 63 | name='Event', 64 | fields=[ 65 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 66 | ('name', models.TextField()), 67 | ('status', models.TextField(default=b'current')), 68 | ], 69 | ), 70 | migrations.CreateModel( 71 | name='Group', 72 | fields=[ 73 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 74 | ('name', models.TextField()), 75 | ], 76 | ), 77 | migrations.CreateModel( 78 | name='Horse', 79 | fields=[ 80 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 81 | ('name', models.TextField()), 82 | ('origin', models.TextField()), 83 | ], 84 | ), 85 | migrations.CreateModel( 86 | name='Location', 87 | fields=[ 88 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 89 | ('name', models.TextField()), 90 | ('blob', models.TextField()), 91 | ], 92 | ), 93 | migrations.CreateModel( 94 | name='Permission', 95 | fields=[ 96 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 97 | ('name', models.TextField()), 98 | ('code', models.IntegerField()), 99 | ], 100 | ), 101 | migrations.CreateModel( 102 | name='Profile', 103 | fields=[ 104 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 105 | ('display_name', models.TextField()), 106 | ('thumbnail_url', models.TextField(blank=True, null=True)), 107 | ], 108 | ), 109 | migrations.CreateModel( 110 | name='User', 111 | fields=[ 112 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 113 | ('name', models.TextField()), 114 | ('last_name', models.TextField()), 115 | ('groups', models.ManyToManyField(related_name='users', to='tests.Group')), 116 | ('location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='tests.Location')), 117 | ('permissions', models.ManyToManyField(related_name='users', to='tests.Permission')), 118 | ], 119 | ), 120 | migrations.CreateModel( 121 | name='Zebra', 122 | fields=[ 123 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 124 | ('name', models.TextField()), 125 | ('origin', models.TextField()), 126 | ], 127 | ), 128 | migrations.AddField( 129 | model_name='profile', 130 | name='user', 131 | field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='tests.User'), 132 | ), 133 | migrations.AddField( 134 | model_name='group', 135 | name='permissions', 136 | field=models.ManyToManyField(related_name='groups', to='tests.Permission'), 137 | ), 138 | migrations.AddField( 139 | model_name='event', 140 | name='location', 141 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='tests.Location'), 142 | ), 143 | migrations.AddField( 144 | model_name='event', 145 | name='users', 146 | field=models.ManyToManyField(to='tests.User'), 147 | ), 148 | migrations.AddField( 149 | model_name='cat', 150 | name='backup_home', 151 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='friendly_cats', to='tests.Location'), 152 | ), 153 | migrations.AddField( 154 | model_name='cat', 155 | name='home', 156 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tests.Location'), 157 | ), 158 | migrations.AddField( 159 | model_name='cat', 160 | name='hunting_grounds', 161 | field=models.ManyToManyField(related_name='annoying_cats', related_query_name=b'getoffmylawn', to='tests.Location'), 162 | ), 163 | migrations.AddField( 164 | model_name='cat', 165 | name='parent', 166 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='kittens', to='tests.Cat'), 167 | ), 168 | migrations.AddField( 169 | model_name='c', 170 | name='d', 171 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tests.D'), 172 | ), 173 | ] 174 | -------------------------------------------------------------------------------- /tests/migrations/0002_auto_20160310_1052.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9 on 2016-03-10 10:52 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('tests', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='user', 17 | name='date_of_birth', 18 | field=models.DateField(blank=True, null=True), 19 | ), 20 | migrations.AlterField( 21 | model_name='group', 22 | name='name', 23 | field=models.TextField(unique=True), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /tests/migrations/0003_auto_20160401_1656.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('contenttypes', '0001_initial'), 12 | ('tests', '0002_auto_20160310_1052'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='user', 18 | name='favorite_pet_id', 19 | field=models.TextField(null=True, blank=True), 20 | preserve_default=True, 21 | ), 22 | migrations.AddField( 23 | model_name='user', 24 | name='favorite_pet_type', 25 | field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType', null=True), # noqa 26 | preserve_default=True, 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /tests/migrations/0004_user_is_dead.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9 on 2016-08-08 13:28 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('tests', '0003_auto_20160401_1656'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='user', 17 | name='is_dead', 18 | field=models.NullBooleanField(default=False), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /tests/migrations/0005_auto_20170712_0759.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.3 on 2017-07-12 07:59 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('tests', '0004_user_is_dead'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Car', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), # noqa 20 | ('name', models.CharField(max_length=60)), 21 | ], 22 | ), 23 | migrations.CreateModel( 24 | name='Country', 25 | fields=[ 26 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), # noqa 27 | ('name', models.CharField(max_length=60)), 28 | ('short_name', models.CharField(max_length=30)), 29 | ], 30 | ), 31 | migrations.CreateModel( 32 | name='Part', 33 | fields=[ 34 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), # noqa 35 | ('name', models.CharField(max_length=60)), 36 | ('car', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tests.Car')), # noqa 37 | ('country', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tests.Country')), # noqa 38 | ], 39 | ), 40 | migrations.AddField( 41 | model_name='car', 42 | name='country', 43 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tests.Country'), # noqa 44 | ), 45 | ] 46 | -------------------------------------------------------------------------------- /tests/migrations/0006_auto_20210921_1026.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.7 on 2021-09-21 10:26 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('tests', '0005_auto_20170712_0759'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='cat', 15 | name='hunting_grounds', 16 | field=models.ManyToManyField( 17 | related_name='annoying_cats', 18 | related_query_name='getoffmylawn', 19 | to='tests.Location'), 20 | ), 21 | migrations.AlterField( 22 | model_name='event', 23 | name='status', 24 | field=models.TextField( 25 | default='current'), 26 | ), 27 | migrations.AlterField( 28 | model_name='user', 29 | name='is_dead', 30 | field=models.BooleanField( 31 | default=False, 32 | null=True), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /tests/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AltSchool/dynamic-rest/c7cc0edc99d420aa2ca7a575de5d75b6c8974e43/tests/migrations/__init__.py -------------------------------------------------------------------------------- /tests/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.contenttypes.fields import GenericForeignKey 2 | from django.contrib.contenttypes.models import ContentType 3 | from django.db import models 4 | 5 | 6 | class User(models.Model): 7 | name = models.TextField() 8 | last_name = models.TextField() 9 | groups = models.ManyToManyField('Group', related_name='users') 10 | permissions = models.ManyToManyField('Permission', related_name='users') 11 | date_of_birth = models.DateField(null=True, blank=True) 12 | # 'related_name' intentionally left unset in location field below: 13 | location = models.ForeignKey( 14 | 'Location', 15 | null=True, 16 | blank=True, 17 | on_delete=models.CASCADE 18 | ) 19 | favorite_pet_type = models.ForeignKey( 20 | ContentType, 21 | null=True, 22 | blank=True, 23 | on_delete=models.CASCADE 24 | ) 25 | favorite_pet_id = models.TextField(null=True, blank=True) 26 | favorite_pet = GenericForeignKey( 27 | 'favorite_pet_type', 28 | 'favorite_pet_id', 29 | ) 30 | is_dead = models.BooleanField(null=True, default=False) 31 | 32 | 33 | class Profile(models.Model): 34 | user = models.OneToOneField(User, on_delete=models.CASCADE) 35 | display_name = models.TextField() 36 | thumbnail_url = models.TextField(null=True, blank=True) 37 | 38 | 39 | class Cat(models.Model): 40 | name = models.TextField() 41 | home = models.ForeignKey('Location', on_delete=models.CASCADE) 42 | backup_home = models.ForeignKey( 43 | 'Location', 44 | related_name='friendly_cats', 45 | on_delete=models.CASCADE 46 | ) 47 | hunting_grounds = models.ManyToManyField( 48 | 'Location', 49 | related_name='annoying_cats', 50 | related_query_name='getoffmylawn' 51 | ) 52 | parent = models.ForeignKey( 53 | 'Cat', 54 | null=True, 55 | blank=True, 56 | related_name='kittens', 57 | on_delete=models.CASCADE 58 | ) 59 | 60 | 61 | class Dog(models.Model): 62 | name = models.TextField() 63 | fur_color = models.TextField() 64 | origin = models.TextField() 65 | 66 | 67 | class Horse(models.Model): 68 | name = models.TextField() 69 | origin = models.TextField() 70 | 71 | 72 | class Zebra(models.Model): 73 | name = models.TextField() 74 | origin = models.TextField() 75 | 76 | 77 | class Group(models.Model): 78 | name = models.TextField(unique=True) 79 | permissions = models.ManyToManyField('Permission', related_name='groups') 80 | 81 | 82 | class Permission(models.Model): 83 | name = models.TextField() 84 | code = models.IntegerField() 85 | 86 | 87 | class Location(models.Model): 88 | name = models.TextField() 89 | blob = models.TextField() 90 | 91 | 92 | class Event(models.Model): 93 | """ 94 | Event model -- Intentionally missing serializer and viewset, so they 95 | can be added as part of a codelab. 96 | """ 97 | 98 | name = models.TextField() 99 | status = models.TextField(default='current') 100 | location = models.ForeignKey( 101 | 'Location', 102 | null=True, 103 | blank=True, 104 | on_delete=models.CASCADE 105 | ) 106 | users = models.ManyToManyField('User') 107 | 108 | 109 | class A(models.Model): 110 | name = models.TextField(blank=True) 111 | 112 | 113 | class B(models.Model): 114 | a = models.OneToOneField('A', related_name='b', on_delete=models.CASCADE) 115 | 116 | 117 | class C(models.Model): 118 | b = models.ForeignKey('B', related_name='cs', on_delete=models.CASCADE) 119 | d = models.ForeignKey('D', on_delete=models.CASCADE) 120 | 121 | 122 | class D(models.Model): 123 | name = models.TextField(blank=True) 124 | 125 | 126 | class Country(models.Model): 127 | name = models.CharField(max_length=60) 128 | short_name = models.CharField(max_length=30) 129 | 130 | 131 | class Car(models.Model): 132 | name = models.CharField(max_length=60) 133 | country = models.ForeignKey(Country, on_delete=models.CASCADE) 134 | 135 | 136 | class Part(models.Model): 137 | car = models.ForeignKey(Car, on_delete=models.CASCADE) 138 | name = models.CharField(max_length=60) 139 | country = models.ForeignKey(Country, on_delete=models.CASCADE) 140 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | BASE_DIR = os.path.dirname(__file__) 4 | 5 | SECRET_KEY = 'test' 6 | INSTALL_DIR = '/usr/local/altschool/dynamic-rest/' 7 | 8 | STATIC_URL = '/static/' 9 | STATIC_ROOT = os.environ.get('STATIC_ROOT', INSTALL_DIR + 'www/static') 10 | 11 | ENABLE_INTEGRATION_TESTS = os.environ.get('ENABLE_INTEGRATION_TESTS', False) 12 | 13 | DEBUG = True 14 | USE_TZ = False 15 | 16 | DATABASES = {} 17 | if os.environ.get('DATABASE_URL'): 18 | # remote database 19 | import dj_database_url 20 | 21 | DATABASES['default'] = dj_database_url.config() 22 | else: 23 | # local sqlite database file 24 | DATABASES['default'] = { 25 | 'ENGINE': 'django.db.backends.sqlite3', 26 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 27 | 'USER': '', 28 | 'PASSWORD': '', 29 | 'HOST': '', 30 | 'PORT': '', 31 | } 32 | 33 | INSTALLED_APPS = ( 34 | 'rest_framework', 35 | 'django.contrib.staticfiles', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.auth', 38 | 'django.contrib.sites', 39 | 'dynamic_rest', 40 | 'tests', 41 | ) 42 | 43 | DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' 44 | 45 | REST_FRAMEWORK = { 46 | 'DEFAULT_PAGINATION_CLASS': 47 | 'rest_framework.pagination.PageNumberPagination', 48 | 'PAGE_SIZE': 50, 49 | 'DEFAULT_RENDERER_CLASSES': ( 50 | 'rest_framework.renderers.JSONRenderer', 51 | 'dynamic_rest.renderers.DynamicBrowsableAPIRenderer', 52 | ), 53 | } 54 | 55 | ROOT_URLCONF = 'tests.urls' 56 | 57 | STATICFILES_DIRS = ( 58 | os.path.abspath( 59 | os.path.join( 60 | BASE_DIR, '../dynamic_rest/static' 61 | ) 62 | ), 63 | ) 64 | 65 | TEMPLATES = [ 66 | { 67 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 68 | 'DIRS': os.path.abspath( 69 | os.path.join(BASE_DIR, '../dynamic_rest/templates') 70 | ), 71 | 'APP_DIRS': True, 72 | } 73 | ] 74 | 75 | DYNAMIC_REST = { 76 | 'ENABLE_LINKS': True, 77 | 'DEBUG': os.environ.get('DYNAMIC_REST_DEBUG', 'false').lower() == 'true', 78 | 'ENABLE_HASHID_FIELDS': True, 79 | 'HASHIDS_SALT': "It's your kids, Marty!", 80 | } 81 | -------------------------------------------------------------------------------- /tests/setup.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | from tests.models import ( 4 | Car, 5 | Cat, 6 | Country, 7 | Dog, 8 | Event, 9 | Group, 10 | Horse, 11 | Location, 12 | Part, 13 | Permission, 14 | User, 15 | Zebra 16 | ) 17 | 18 | 19 | def create_fixture(): 20 | # 4 users sharing 2 groups, 4 permissions, and 3 locations 21 | # each group has 1 permission 22 | # one location has a cat. 23 | # 2 of the users share the same location 24 | # 2 of the users have their own locations 25 | # Create 4 dogs. 26 | # Create 2 Country 27 | # Create 1 Car has 2 Parts each from different Country 28 | 29 | types = [ 30 | 'users', 'groups', 'locations', 'permissions', 31 | 'events', 'cats', 'dogs', 'horses', 'zebras', 32 | 'cars', 'countries', 'parts', 33 | ] 34 | Fixture = namedtuple('Fixture', types) 35 | 36 | fixture = Fixture( 37 | users=[], groups=[], locations=[], permissions=[], 38 | events=[], cats=[], dogs=[], horses=[], zebras=[], 39 | cars=[], countries=[], parts=[] 40 | ) 41 | 42 | for i in range(0, 4): 43 | fixture.users.append( 44 | User.objects.create( 45 | name=str(i), 46 | last_name=str(i))) 47 | 48 | for i in range(0, 4): 49 | fixture.permissions.append( 50 | Permission.objects.create( 51 | name=str(i), 52 | code=i)) 53 | 54 | for i in range(0, 2): 55 | fixture.groups.append(Group.objects.create(name=str(i))) 56 | 57 | for i in range(0, 3): 58 | fixture.locations.append(Location.objects.create(name=str(i))) 59 | 60 | for i in range(0, 2): 61 | fixture.cats.append(Cat.objects.create( 62 | name=str(i), 63 | home_id=fixture.locations[i].id, 64 | backup_home_id=( 65 | fixture.locations[len(fixture.locations) - 1 - i].id))) 66 | 67 | dogs = [{ 68 | 'name': 'Clifford', 69 | 'fur_color': 'red', 70 | 'origin': 'Clifford the big red dog' 71 | }, { 72 | 'name': 'Air-Bud', 73 | 'fur_color': 'gold', 74 | 'origin': 'Air Bud 4: Seventh Inning Fetch' 75 | }, { 76 | 'name': 'Spike', 77 | 'fur_color': 'brown', 78 | 'origin': 'Rugrats' 79 | }, { 80 | 'name': 'Pluto', 81 | 'fur_color': 'brown and white', 82 | 'origin': 'Mickey Mouse' 83 | }, { 84 | 'name': 'Spike', 85 | 'fur_color': 'light-brown', 86 | 'origin': 'Tom and Jerry' 87 | }] 88 | 89 | horses = [{ 90 | 'name': 'Seabiscuit', 91 | 'origin': 'LA' 92 | }, { 93 | 'name': 'Secretariat', 94 | 'origin': 'Kentucky' 95 | }] 96 | 97 | zebras = [{ 98 | 'name': 'Ralph', 99 | 'origin': 'new york' 100 | }, { 101 | 'name': 'Ted', 102 | 'origin': 'africa' 103 | }] 104 | 105 | events = [{ 106 | 'name': 'Event 1', 107 | 'status': 'archived', 108 | 'location': 2 109 | }, { 110 | 'name': 'Event 2', 111 | 'status': 'current', 112 | 'location': 1 113 | }, { 114 | 'name': 'Event 3', 115 | 'status': 'current', 116 | 'location': 1 117 | }, { 118 | 'name': 'Event 4', 119 | 'status': 'archived', 120 | 'location': 2 121 | }, { 122 | 'name': 'Event 5', 123 | 'status': 'current', 124 | 'location': 2 125 | }] 126 | 127 | for dog in dogs: 128 | fixture.dogs.append(Dog.objects.create( 129 | name=dog.get('name'), 130 | fur_color=dog.get('fur_color'), 131 | origin=dog.get('origin') 132 | )) 133 | 134 | for horse in horses: 135 | fixture.horses.append(Horse.objects.create( 136 | name=horse.get('name'), 137 | origin=horse.get('origin') 138 | )) 139 | 140 | for zebra in zebras: 141 | fixture.zebras.append(Zebra.objects.create( 142 | name=zebra.get('name'), 143 | origin=zebra.get('origin') 144 | )) 145 | 146 | for event in events: 147 | fixture.events.append(Event.objects.create( 148 | name=event['name'], 149 | status=event['status'], 150 | location_id=event['location'] 151 | )) 152 | fixture.events[1].users.add(fixture.users[0]) 153 | fixture.events[1].users.add(fixture.users[1]) 154 | fixture.events[2].users.add(fixture.users[0]) 155 | fixture.events[3].users.add(fixture.users[0]) 156 | fixture.events[3].users.add(fixture.users[2]) 157 | fixture.events[4].users.add(fixture.users[0]) 158 | fixture.events[4].users.add(fixture.users[1]) 159 | fixture.events[4].users.add(fixture.users[2]) 160 | 161 | fixture.locations[0].blob = 'here' 162 | fixture.locations[0].save() 163 | 164 | fixture.users[0].location = fixture.locations[0] 165 | fixture.users[0].save() 166 | fixture.users[0].groups.add(fixture.groups[0]) 167 | fixture.users[0].groups.add(fixture.groups[1]) 168 | fixture.users[0].permissions.add(fixture.permissions[0]) 169 | fixture.users[0].permissions.add(fixture.permissions[1]) 170 | 171 | fixture.users[1].location = fixture.locations[0] 172 | fixture.users[1].save() 173 | fixture.users[1].groups.add(fixture.groups[0]) 174 | fixture.users[1].groups.add(fixture.groups[1]) 175 | fixture.users[1].permissions.add(fixture.permissions[2]) 176 | fixture.users[1].permissions.add(fixture.permissions[3]) 177 | 178 | fixture.users[2].location = fixture.locations[1] 179 | fixture.users[2].save() 180 | fixture.users[2].groups.add(fixture.groups[0]) 181 | fixture.users[2].groups.add(fixture.groups[1]) 182 | fixture.users[2].permissions.add(fixture.permissions[0]) 183 | fixture.users[2].permissions.add(fixture.permissions[3]) 184 | 185 | fixture.users[3].location = fixture.locations[2] 186 | fixture.users[3].save() 187 | fixture.users[3].groups.add(fixture.groups[0]) 188 | fixture.users[3].groups.add(fixture.groups[1]) 189 | fixture.users[3].permissions.add(fixture.permissions[1]) 190 | fixture.users[3].permissions.add(fixture.permissions[2]) 191 | 192 | fixture.groups[0].permissions.add(fixture.permissions[0]) 193 | fixture.groups[1].permissions.add(fixture.permissions[1]) 194 | 195 | countries = [{ 196 | 'id': 1, 197 | 'name': 'United States', 198 | 'short_name': 'US', 199 | }, { 200 | 'id': 2, 201 | 'name': 'China', 202 | 'short_name': 'CN', 203 | }] 204 | 205 | cars = [{ 206 | 'id': 1, 207 | 'name': 'Porshe', 208 | 'country': 1 209 | }] 210 | 211 | parts = [{ 212 | 'car': 1, 213 | 'name': 'wheel', 214 | 'country': 1 215 | }, { 216 | 'car': 1, 217 | 'name': 'tire', 218 | 'country': 2 219 | }] 220 | 221 | for country in countries: 222 | fixture.countries.append(Country.objects.create(**country)) 223 | 224 | for car in cars: 225 | fixture.cars.append(Car.objects.create( 226 | id=car.get('id'), 227 | name=car.get('name'), 228 | country_id=car.get('country') 229 | )) 230 | 231 | for part in parts: 232 | fixture.parts.append(Part.objects.create( 233 | car_id=part.get('car'), 234 | name=part.get('name'), 235 | country_id=part.get('country') 236 | )) 237 | 238 | return fixture 239 | -------------------------------------------------------------------------------- /tests/test_fields.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase, override_settings 2 | 3 | from rest_framework import serializers 4 | 5 | from dynamic_rest.fields import DynamicHashIdField 6 | from dynamic_rest.utils import ( 7 | external_id_from_model_and_internal_id, 8 | ) 9 | from tests.models import Dog 10 | 11 | 12 | @override_settings( 13 | ENABLE_HASHID_FIELDS=True, 14 | HASHIDS_SALT="I guess you guys aren’t ready for that yet, " 15 | "but your kids are gonna love it.", 16 | ) 17 | class FieldsTestCase(TestCase): 18 | def test_dynamic_hash_id_field_with_model_parameter(self): 19 | class DogModelTestSerializer(serializers.ModelSerializer): 20 | """ 21 | A custom model serializer simply for testing purposes. 22 | """ 23 | 24 | id = DynamicHashIdField(model=Dog) 25 | 26 | class Meta: 27 | model = Dog 28 | fields = ["id", "name", "fur_color", "origin"] 29 | 30 | dog = Dog.objects.create( 31 | name="Kazan", 32 | fur_color="brown", 33 | origin="Abuelos") 34 | serializer = DogModelTestSerializer(dog) 35 | 36 | self.assertEqual( 37 | serializer.data["id"], 38 | external_id_from_model_and_internal_id( 39 | Dog, 40 | dog.id)) 41 | 42 | def test_dynamic_hash_id_field_without_model_parameter(self): 43 | class DogModelTestSerializer(serializers.ModelSerializer): 44 | """ 45 | A custom model serializer simply for testing purposes. 46 | """ 47 | 48 | id = DynamicHashIdField() 49 | 50 | class Meta: 51 | model = Dog 52 | fields = ["id", "name", "fur_color", "origin"] 53 | 54 | dog = Dog.objects.create( 55 | name="Kazan", 56 | fur_color="brown", 57 | origin="Abuelos") 58 | serializer = DogModelTestSerializer(dog) 59 | 60 | self.assertEqual( 61 | serializer.data["id"], 62 | external_id_from_model_and_internal_id( 63 | Dog, 64 | dog.id)) 65 | -------------------------------------------------------------------------------- /tests/test_generic.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from rest_framework.test import APITestCase 4 | 5 | from dynamic_rest.fields import DynamicGenericRelationField 6 | from dynamic_rest.routers import DynamicRouter 7 | from tests.models import User, Zebra 8 | from tests.serializers import UserSerializer 9 | from tests.setup import create_fixture 10 | 11 | 12 | class TestGenericRelationFieldAPI(APITestCase): 13 | 14 | def setUp(self): 15 | self.fixture = create_fixture() 16 | f = self.fixture 17 | f.users[0].favorite_pet = f.cats[0] 18 | f.users[0].save() 19 | 20 | f.users[1].favorite_pet = f.cats[1] 21 | f.users[1].save() 22 | 23 | f.users[2].favorite_pet = f.dogs[1] 24 | f.users[2].save() 25 | 26 | def test_id_only(self): 27 | """ 28 | In the id_only case, the favorite_pet field looks like: 29 | 30 | ``` 31 | "favorite_animal" : { 32 | "type": "cats", 33 | "id": "1" 34 | } 35 | ``` 36 | """ 37 | url = ( 38 | '/users/?include[]=favorite_pet' 39 | '&filter{favorite_pet_id.isnull}=false' 40 | ) 41 | response = self.client.get(url) 42 | self.assertEqual(200, response.status_code) 43 | content = json.loads(response.content.decode('utf-8')) 44 | self.assertTrue( 45 | all( 46 | [_['favorite_pet'] for _ in content['users']] 47 | ) 48 | ) 49 | self.assertFalse('cats' in content) 50 | self.assertFalse('dogs' in content) 51 | self.assertTrue('type' in content['users'][0]['favorite_pet']) 52 | self.assertTrue('id' in content['users'][0]['favorite_pet']) 53 | 54 | def test_sideload(self): 55 | url = ( 56 | '/users/?include[]=favorite_pet.' 57 | '&filter{favorite_pet_id.isnull}=false' 58 | ) 59 | response = self.client.get(url) 60 | self.assertEqual(200, response.status_code) 61 | content = json.loads(response.content.decode('utf-8')) 62 | self.assertTrue( 63 | all( 64 | [_['favorite_pet'] for _ in content['users']] 65 | ) 66 | ) 67 | self.assertTrue('cats' in content) 68 | self.assertEqual(2, len(content['cats'])) 69 | self.assertTrue('dogs' in content) 70 | self.assertEqual(1, len(content['dogs'])) 71 | self.assertTrue('type' in content['users'][0]['favorite_pet']) 72 | self.assertTrue('id' in content['users'][0]['favorite_pet']) 73 | 74 | def test_multi_sideload_include(self): 75 | url = ( 76 | '/cars/1/?include[]=name&include[]=country.short_name' 77 | '&include[]=parts.name&include[]=parts.country.name' 78 | ) 79 | response = self.client.get(url) 80 | self.assertEqual(200, response.status_code) 81 | content = json.loads(response.content.decode('utf-8')) 82 | self.assertTrue('countries' in content) 83 | 84 | country = None 85 | for _ in content['countries']: 86 | if _['id'] == 1: 87 | country = _ 88 | 89 | self.assertTrue(country) 90 | self.assertTrue('short_name' in country) 91 | self.assertTrue('name' in country) 92 | 93 | def test_query_counts(self): 94 | # NOTE: Django doesn't seem to prefetch ContentType objects 95 | # themselves, and rather caches internally. That means 96 | # this call could do 5 SQL queries if the Cat and Dog 97 | # ContentType objects haven't been cached. 98 | with self.assertNumQueries(3): 99 | url = ( 100 | '/users/?include[]=favorite_pet.' 101 | '&filter{favorite_pet_id.isnull}=false' 102 | ) 103 | response = self.client.get(url) 104 | self.assertEqual(200, response.status_code) 105 | 106 | with self.assertNumQueries(3): 107 | url = '/users/?include[]=favorite_pet.' 108 | response = self.client.get(url) 109 | self.assertEqual(200, response.status_code) 110 | 111 | def test_unknown_resource(self): 112 | """Test case where polymorhpic relation pulls in an object for 113 | which there is no known canonical serializer. 114 | """ 115 | 116 | zork = Zebra.objects.create( 117 | name='Zork', 118 | origin='San Francisco Zoo' 119 | ) 120 | 121 | user = self.fixture.users[0] 122 | user.favorite_pet = zork 123 | user.save() 124 | 125 | self.assertIsNone(DynamicRouter.get_canonical_serializer(Zebra)) 126 | 127 | url = '/users/%s/?include[]=favorite_pet' % user.pk 128 | response = self.client.get(url) 129 | self.assertEqual(200, response.status_code) 130 | content = json.loads(response.content.decode('utf-8')) 131 | self.assertTrue('user' in content) 132 | self.assertFalse('zebras' in content) # Not sideloaded 133 | user_obj = content['user'] 134 | self.assertTrue('favorite_pet' in user_obj) 135 | self.assertEqual('Zebra', user_obj['favorite_pet']['type']) 136 | self.assertEqual(zork.pk, user_obj['favorite_pet']['id']) 137 | 138 | def test_dgrf_with_requires_raises(self): 139 | with self.assertRaises(Exception): 140 | DynamicGenericRelationField(requires=['foo', 'bar']) 141 | 142 | def test_if_field_inclusion_then_error(self): 143 | url = ( 144 | '/users/?include[]=favorite_pet.name' 145 | '&filter{favorite_pet_id.isnull}=false' 146 | ) 147 | response = self.client.get(url) 148 | self.assertEqual(400, response.status_code) 149 | 150 | def test_patch_resource(self): 151 | """ 152 | Test that patching a content-type field updates the underlying 153 | relationship 154 | """ 155 | user = self.fixture.users[0] 156 | 157 | url = '/users/%s/?include[]=favorite_pet.' % user.pk 158 | response = self.client.patch( 159 | url, 160 | json.dumps({ 161 | 'id': user.id, 162 | 'favorite_pet': { 163 | 'type': 'dog', 164 | 'id': 1 165 | } 166 | }), 167 | content_type='application/json' 168 | ) 169 | self.assertEqual(200, response.status_code) 170 | content = json.loads(response.content.decode('utf-8')) 171 | self.assertTrue('user' in content) 172 | self.assertFalse('cats' in content) 173 | self.assertTrue('dogs' in content) 174 | self.assertEqual(1, content['dogs'][0]['id']) 175 | 176 | def test_non_deferred_generic_field(self): 177 | class FooUserSerializer(UserSerializer): 178 | 179 | class Meta: 180 | model = User 181 | name = 'user' 182 | fields = ( 183 | 'id', 184 | 'favorite_pet', 185 | ) 186 | 187 | user = User.objects.filter( 188 | favorite_pet_id__isnull=False 189 | ).prefetch_related( 190 | 'favorite_pet' 191 | ).first() 192 | 193 | data = FooUserSerializer(user, envelope=True).data['user'] 194 | self.assertIsNotNone(data) 195 | self.assertTrue('favorite_pet' in data) 196 | self.assertTrue(isinstance(data['favorite_pet'], dict)) 197 | self.assertEqual( 198 | set(['id', 'type']), 199 | set(data['favorite_pet'].keys()) 200 | ) 201 | -------------------------------------------------------------------------------- /tests/test_meta.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from dynamic_rest.meta import ( 4 | get_model_field, 5 | get_model_field_and_type, 6 | get_remote_model, 7 | reverse_m2m_field_name 8 | ) 9 | from tests.models import Group, Location, Profile, User 10 | 11 | 12 | class TestMeta(TestCase): 13 | 14 | def test_get_remote_model(self): 15 | tests = [ 16 | (Location, 'user_set', User), 17 | (User, 'location', Location), 18 | (User, 'profile', Profile), 19 | (User, 'groups', Group), 20 | (Group, 'users', User), 21 | (Profile, 'user', User), 22 | ] 23 | 24 | for model, field_name, expected in tests: 25 | remote_model = get_remote_model( 26 | get_model_field(model, field_name) 27 | ) 28 | self.assertEqual( 29 | expected, 30 | remote_model, 31 | "For %s.%s expected %s got %s" % ( 32 | model, 33 | field_name, 34 | expected, 35 | remote_model 36 | ) 37 | ) 38 | 39 | def test_model_field_and_type(self): 40 | 41 | tests = [ 42 | (Location, 'user_set', 'm2o'), 43 | (User, 'location', 'fk'), 44 | (User, 'profile', 'o2or'), 45 | (User, 'groups', 'm2m'), 46 | (Group, 'users', 'm2m'), 47 | (Profile, 'user', 'o2o'), 48 | (User, 'id', '') 49 | ] 50 | 51 | for model, field_name, expected in tests: 52 | field, typestr = get_model_field_and_type(model, field_name) 53 | self.assertEqual( 54 | expected, 55 | typestr, 56 | "%s.%s should be '%s', got '%s'" % ( 57 | model, 58 | field_name, 59 | expected, 60 | typestr, 61 | ) 62 | ) 63 | 64 | def test_reverse_m2m_field_name(self): 65 | m2m_field = get_model_field(User, 'groups') 66 | reverse = reverse_m2m_field_name(m2m_field) 67 | self.assertEqual('users', reverse) 68 | -------------------------------------------------------------------------------- /tests/test_prefetch.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Prefetch 2 | from django.test import TestCase 3 | 4 | from tests.models import A, B, C, D 5 | 6 | 7 | class TestPrefetch(TestCase): 8 | """Tests prefetch corner-case bugs introduced in Django 1.7 9 | 10 | See dynamic_rest.patches for details. 11 | """ 12 | 13 | def test_nested_prefetch(self): 14 | a = A.objects.create(name="a") 15 | b = B.objects.create(a=a) 16 | d = D.objects.create(name="d") 17 | C.objects.create(b=b, d=d) 18 | 19 | # This fails 20 | A.objects.prefetch_related( 21 | Prefetch( 22 | 'b', 23 | queryset=B.objects.prefetch_related( 24 | Prefetch( 25 | 'cs', 26 | queryset=C.objects.prefetch_related( 27 | Prefetch( 28 | 'd', 29 | queryset=D.objects.all() 30 | ) 31 | ) 32 | ) 33 | ) 34 | ) 35 | )[0] 36 | -------------------------------------------------------------------------------- /tests/test_prefetch2.py: -------------------------------------------------------------------------------- 1 | import six 2 | 3 | from rest_framework.test import APITestCase 4 | 5 | from dynamic_rest.prefetch import FastPrefetch, FastQuery 6 | from tests.models import Group, Location, Profile, User, Cat 7 | from tests.setup import create_fixture 8 | 9 | 10 | class TestFastQuery(APITestCase): 11 | 12 | def setUp(self): 13 | self.fixture = create_fixture() 14 | 15 | def _user_keys(self): 16 | return set([ 17 | 'last_name', 18 | 'name', 19 | 'favorite_pet_id', 20 | 'date_of_birth', 21 | 'favorite_pet_type_id', 22 | 'location_id', 23 | 'id', 24 | 'is_dead', 25 | ]) 26 | 27 | def test_fk_prefetch(self): 28 | with self.assertNumQueries(2): 29 | q = FastQuery(User.objects.all()) 30 | q.prefetch_related( 31 | FastPrefetch( 32 | 'location', 33 | Location.objects.all() 34 | ) 35 | ) 36 | result = q.execute() 37 | 38 | self.assertTrue( 39 | all([_['location'] for _ in result]) 40 | ) 41 | self.assertEqual( 42 | set(['blob', 'id', 'name']), 43 | set(result[0]['location'].keys()) 44 | ) 45 | 46 | def test_m2m_prefetch(self): 47 | with self.assertNumQueries(3): 48 | q = FastQuery(User.objects.all()) 49 | q.prefetch_related( 50 | FastPrefetch( 51 | 'groups', 52 | Group.objects.all() 53 | ) 54 | ) 55 | result = q.execute() 56 | 57 | self.assertTrue( 58 | all([_['groups'] for _ in result]) 59 | ) 60 | self.assertTrue( 61 | isinstance(result[0]['groups'], list) 62 | ) 63 | self.assertEqual( 64 | set(['id', 'name']), 65 | set(result[0]['groups'][0].keys()) 66 | ) 67 | 68 | def test_o2o_prefetch(self): 69 | # Create profiles 70 | for i in range(1, 4): 71 | Profile.objects.create( 72 | user=User.objects.get(pk=i), 73 | display_name='User %s' % i 74 | ) 75 | 76 | with self.assertNumQueries(2): 77 | q = FastQuery(Profile.objects.all()) 78 | q.prefetch_related( 79 | FastPrefetch( 80 | 'user', 81 | User.objects.all() 82 | ) 83 | ) 84 | result = q.execute() 85 | 86 | self.assertTrue( 87 | all([_['user'] for _ in result]) 88 | ) 89 | self.assertEqual( 90 | self._user_keys(), 91 | set(result[0]['user'].keys()) 92 | ) 93 | 94 | def test_reverse_o2o_prefetch(self): 95 | # Create profiles 96 | for i in range(1, 4): 97 | Profile.objects.create( 98 | user=User.objects.get(pk=i), 99 | display_name='User %s' % i 100 | ) 101 | 102 | with self.assertNumQueries(2): 103 | q = FastQuery(User.objects.all()) 104 | q.prefetch_related( 105 | FastPrefetch( 106 | 'profile', 107 | Profile.objects.all() 108 | ) 109 | ) 110 | result = q.execute() 111 | 112 | self.assertTrue( 113 | all(['profile' in _ for _ in result]) 114 | ) 115 | user = sorted( 116 | result, 117 | key=lambda x: 1 if x['profile'] is None else 0 118 | )[0] 119 | self.assertEqual( 120 | set(['display_name', 'user_id', 'id', 'thumbnail_url']), 121 | set(user['profile'].keys()) 122 | ) 123 | 124 | def test_m2o_prefetch(self): 125 | with self.assertNumQueries(2): 126 | q = FastQuery(Location.objects.all()) 127 | q.prefetch_related( 128 | FastPrefetch( 129 | 'user_set', 130 | User.objects.all() 131 | ) 132 | ) 133 | result = q.execute() 134 | 135 | self.assertTrue( 136 | all(['user_set' in obj for obj in result]) 137 | ) 138 | location = six.next(( 139 | o for o in result if o['user_set'] and len(o['user_set']) > 1 140 | )) 141 | 142 | self.assertIsNotNone(location) 143 | self.assertEqual( 144 | self._user_keys(), 145 | set(location['user_set'][0].keys()) 146 | ) 147 | 148 | def test_pagination(self): 149 | r = list(FastQuery(User.objects.all())) 150 | self.assertTrue(isinstance(r, list)) 151 | 152 | r = FastQuery(User.objects.order_by('id'))[1] 153 | self.assertEqual(1, len(r)) 154 | self.assertEqual(r[0]['id'], 2) 155 | 156 | r = FastQuery(User.objects.order_by('id'))[1:3] 157 | self.assertEqual(2, len(r)) 158 | self.assertEqual(r[0]['id'], 2) 159 | self.assertEqual(r[1]['id'], 3) 160 | 161 | with self.assertRaises(TypeError): 162 | FastQuery(User.objects.all())[:10:2] 163 | 164 | def test_nested_prefetch_by_string(self): 165 | q = FastQuery(Location.objects.filter(pk=1)) 166 | q.prefetch_related('user_set__groups') 167 | out = list(q) 168 | 169 | self.assertTrue('user_set' in out[0]) 170 | self.assertTrue('groups' in out[0]['user_set'][0]) 171 | 172 | def test_get_with_prefetch(self): 173 | # FastQuery.get() should apply prefetch filters correctly 174 | self.assertTrue( 175 | Cat.objects.filter(home=1, backup_home=3).exists() 176 | ) 177 | q = FastQuery(Location.objects.all()) 178 | q.prefetch_related( 179 | FastPrefetch( 180 | 'friendly_cats', 181 | Cat.objects.filter(home__gt=1) 182 | ) 183 | ) 184 | obj = q.get(pk=3) 185 | self.assertEqual(0, obj.friendly_cats.count()) 186 | 187 | def test_first_with_prefetch(self): 188 | # FastQuery.filter() should apply prefetch filters correctly 189 | self.assertTrue( 190 | Cat.objects.filter(home=1, backup_home=3).exists() 191 | ) 192 | q = FastQuery(Location.objects.all()) 193 | q = q.filter(pk=3) 194 | q.prefetch_related( 195 | FastPrefetch( 196 | 'friendly_cats', 197 | Cat.objects.filter(home__gt=1) 198 | ) 199 | ) 200 | 201 | obj = q.first() 202 | self.assertEqual(0, obj.friendly_cats.count()) 203 | -------------------------------------------------------------------------------- /tests/test_router.py: -------------------------------------------------------------------------------- 1 | from django.urls import set_script_prefix, clear_script_prefix 2 | 3 | 4 | from rest_framework.test import APITestCase 5 | from rest_framework.routers import DefaultRouter 6 | 7 | from dynamic_rest.meta import get_model_table 8 | from dynamic_rest.routers import DynamicRouter, Route 9 | from tests.models import Dog 10 | from tests.serializers import CatSerializer, DogSerializer 11 | from tests.urls import urlpatterns # noqa force route registration 12 | 13 | 14 | class TestDynamicRouter(APITestCase): 15 | 16 | def test_get_canonical_path(self): 17 | rsrc_key = DogSerializer().get_resource_key() 18 | self.assertEqual( 19 | '/dogs', 20 | DynamicRouter.get_canonical_path(rsrc_key) 21 | ) 22 | 23 | def test_get_canonical_path_with_prefix(self): 24 | set_script_prefix('/v2/') 25 | rsrc_key = DogSerializer().get_resource_key() 26 | self.assertEqual( 27 | '/v2/dogs', 28 | DynamicRouter.get_canonical_path(rsrc_key) 29 | ) 30 | clear_script_prefix() 31 | 32 | def test_get_canonical_path_with_pk(self): 33 | rsrc_key = DogSerializer().get_resource_key() 34 | self.assertEqual( 35 | '/dogs/1/', 36 | DynamicRouter.get_canonical_path(rsrc_key, pk='1') 37 | ) 38 | 39 | def test_get_canonical_path_with_keyspace(self): 40 | rsrc_key = CatSerializer().get_resource_key() 41 | self.assertEqual( 42 | '/v2/cats', 43 | DynamicRouter.get_canonical_path(rsrc_key) 44 | ) 45 | 46 | def test_get_canonical_serializer(self): 47 | rsrc_key = get_model_table(Dog) 48 | self.assertEqual( 49 | DogSerializer, 50 | DynamicRouter.get_canonical_serializer(rsrc_key) 51 | ) 52 | 53 | def test_get_canonical_serializer_by_model(self): 54 | self.assertEqual( 55 | DogSerializer, 56 | DynamicRouter.get_canonical_serializer(None, model=Dog) 57 | ) 58 | 59 | def test_get_canonical_serializer_by_instance(self): 60 | dog = Dog.objects.create( 61 | name='Snoopy', 62 | fur_color='black and white', 63 | origin='' 64 | ) 65 | self.assertEqual( 66 | DogSerializer, 67 | DynamicRouter.get_canonical_serializer(None, instance=dog) 68 | ) 69 | 70 | def test_rest_framework_router_unmodified(self): 71 | if hasattr(self, 'assertCountEqual'): 72 | method = self.assertCountEqual 73 | else: 74 | method = self.assertItemsEqual 75 | 76 | method( 77 | [ 78 | { 79 | 'post': 'create', 80 | 'get': 'list' 81 | }, 82 | { 83 | 'put': 'update', 84 | 'patch': 'partial_update', 85 | 'delete': 'destroy', 86 | 'get': 'retrieve' 87 | } 88 | ], 89 | [ 90 | route.mapping for route in DefaultRouter.routes 91 | if isinstance(route, Route) 92 | ] 93 | ) 94 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase, override_settings 2 | 3 | from dynamic_rest.utils import ( 4 | is_truthy, 5 | unpack, 6 | internal_id_from_model_and_external_id, 7 | model_from_definition 8 | ) 9 | from tests.models import User 10 | 11 | 12 | class UtilsTestCase(TestCase): 13 | def setUp(self): 14 | User.objects.create(name="Marie") 15 | User.objects.create(name="Rosalind") 16 | 17 | def test_is_truthy(self): 18 | self.assertTrue(is_truthy("faux")) 19 | self.assertTrue(is_truthy(1)) 20 | self.assertFalse(is_truthy("0")) 21 | self.assertFalse(is_truthy("False")) 22 | self.assertFalse(is_truthy("false")) 23 | self.assertFalse(is_truthy("")) 24 | 25 | def test_unpack_empty_value(self): 26 | self.assertIsNone(unpack(None)) 27 | 28 | def test_unpack_non_empty_value(self): 29 | content = {"hello": "world", "meta": "worldpeace", "missed": "a 't'"} 30 | self.assertIsNotNone(unpack(content)) 31 | 32 | def test_unpack_meta_first_key(self): 33 | content = {"meta": "worldpeace", "missed": "a 't'"} 34 | self.assertEqual(unpack(content), "a 't'") 35 | 36 | def test_unpack_meta_not_first_key(self): 37 | content = {"hello": "world", "meta": "worldpeace", "missed": "a 't'"} 38 | self.assertEqual(unpack(content), "world") 39 | 40 | @override_settings( 41 | ENABLE_HASHID_FIELDS=True, 42 | HASHIDS_SALT="If my calculations are correct, " 43 | "when this vaby hits 88 miles per hour, " 44 | "you're gonna see some serious s***.", 45 | ) 46 | def test_int_id_from_model_ext_id_obj_does_not_exits( 47 | self): 48 | self.assertRaises( 49 | User.DoesNotExist, 50 | internal_id_from_model_and_external_id, 51 | model=User, 52 | external_id="skdkahh", 53 | ) 54 | 55 | def test_model_from_definition(self): 56 | self.assertEqual(model_from_definition('tests.models.User'), User) 57 | self.assertEqual(model_from_definition(User), User) 58 | self.assertRaises( 59 | AssertionError, 60 | model_from_definition, 61 | model_definition='django.test.override_settings' 62 | ) 63 | self.assertRaises( 64 | AssertionError, 65 | model_from_definition, 66 | model_definition=User() 67 | ) 68 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | 3 | from dynamic_rest.routers import DynamicRouter 4 | from tests import viewsets 5 | 6 | router = DynamicRouter() 7 | router.register_resource(viewsets.UserViewSet) 8 | router.register_resource(viewsets.GroupViewSet) 9 | router.register_resource(viewsets.ProfileViewSet) 10 | router.register_resource(viewsets.LocationViewSet) 11 | 12 | router.register(r'cars', viewsets.CarViewSet) 13 | router.register(r'cats', viewsets.CatViewSet) 14 | router.register_resource(viewsets.DogViewSet) 15 | router.register_resource(viewsets.HorseViewSet) 16 | router.register_resource(viewsets.PermissionViewSet) 17 | router.register(r'zebras', viewsets.ZebraViewSet) # not canonical 18 | router.register(r'user_locations', viewsets.UserLocationViewSet) 19 | router.register(r'alternate_locations', viewsets.AlternateLocationViewSet) 20 | 21 | # the above routes are duplicated to test versioned prefixes 22 | router.register_resource(viewsets.CatViewSet, namespace='v2') # canonical 23 | router.register(r'v1/user_locations', viewsets.UserLocationViewSet) 24 | 25 | urlpatterns = [ 26 | path('', include(router.urls)) 27 | ] 28 | -------------------------------------------------------------------------------- /tests/viewsets.py: -------------------------------------------------------------------------------- 1 | from rest_framework import exceptions 2 | from django.db.models import Q 3 | 4 | from dynamic_rest.viewsets import DynamicModelViewSet 5 | from tests.models import ( 6 | Car, 7 | Cat, 8 | Dog, 9 | Group, 10 | Horse, 11 | Location, 12 | Permission, 13 | Profile, 14 | User, 15 | Zebra 16 | ) 17 | from tests.serializers import ( 18 | CarSerializer, 19 | CatSerializer, 20 | DogSerializer, 21 | GroupSerializer, 22 | HorseSerializer, 23 | LocationSerializer, 24 | PermissionSerializer, 25 | ProfileSerializer, 26 | UserLocationSerializer, 27 | UserSerializer, 28 | ZebraSerializer 29 | ) 30 | 31 | 32 | class UserViewSet(DynamicModelViewSet): 33 | features = ( 34 | DynamicModelViewSet.INCLUDE, DynamicModelViewSet.EXCLUDE, 35 | DynamicModelViewSet.FILTER, DynamicModelViewSet.SORT, 36 | DynamicModelViewSet.SIDELOADING, DynamicModelViewSet.DEBUG, 37 | ) 38 | model = User 39 | serializer_class = UserSerializer 40 | queryset = User.objects.all() 41 | 42 | def get_queryset(self): 43 | location = self.request.query_params.get('location') 44 | qs = self.queryset 45 | if location: 46 | qs = qs.filter(location=location) 47 | return qs 48 | 49 | def list(self, request, *args, **kwargs): 50 | query_params = self.request.query_params 51 | # for testing query param injection 52 | if query_params.get('name'): 53 | query_params.add('filter{name}', query_params.get('name')) 54 | return super(UserViewSet, self).list(request, *args, **kwargs) 55 | 56 | 57 | class GroupNoMergeDictViewSet(DynamicModelViewSet): 58 | model = Group 59 | serializer_class = GroupSerializer 60 | queryset = Group.objects.all() 61 | 62 | def create(self, request, *args, **kwargs): 63 | response = super(GroupNoMergeDictViewSet, self).create( 64 | request, 65 | *args, 66 | **kwargs 67 | ) 68 | if hasattr(request, 'data'): 69 | try: 70 | # Django<1.9, DRF<3.2 71 | from django.utils.datastructures import MergeDict 72 | if isinstance(request.data, MergeDict): 73 | raise exceptions.ValidationError( 74 | "request.data is MergeDict" 75 | ) 76 | elif not isinstance(request.data, dict): 77 | raise exceptions.ValidationError( 78 | "request.data is not a dict" 79 | ) 80 | except BaseException: 81 | pass 82 | 83 | return response 84 | 85 | 86 | class GroupViewSet(DynamicModelViewSet): 87 | features = ( 88 | DynamicModelViewSet.INCLUDE, DynamicModelViewSet.EXCLUDE, 89 | DynamicModelViewSet.FILTER, DynamicModelViewSet.SORT, 90 | ) 91 | model = Group 92 | serializer_class = GroupSerializer 93 | queryset = Group.objects.all() 94 | 95 | 96 | class LocationViewSet(DynamicModelViewSet): 97 | features = ( 98 | DynamicModelViewSet.INCLUDE, DynamicModelViewSet.EXCLUDE, 99 | DynamicModelViewSet.FILTER, DynamicModelViewSet.SORT, 100 | DynamicModelViewSet.DEBUG, DynamicModelViewSet.SIDELOADING, 101 | ) 102 | model = Location 103 | serializer_class = LocationSerializer 104 | queryset = Location.objects.all() 105 | 106 | 107 | class AlternateLocationViewSet(DynamicModelViewSet): 108 | model = Location 109 | serializer_class = LocationSerializer 110 | queryset = Location.objects.all() 111 | 112 | def filter_queryset(self, queryset): 113 | user_name_separate_filter = self.request.query_params.get( 114 | 'user_name_separate' 115 | ) 116 | if user_name_separate_filter: 117 | queryset = queryset.filter(user__name=user_name_separate_filter) 118 | return super(AlternateLocationViewSet, self).filter_queryset(queryset) 119 | 120 | def get_extra_filters(self, request): 121 | user_name = request.query_params.get('user_name') 122 | if user_name: 123 | return Q(user__name=user_name) 124 | return None 125 | 126 | 127 | class UserLocationViewSet(DynamicModelViewSet): 128 | model = User 129 | serializer_class = UserLocationSerializer 130 | queryset = User.objects.all() 131 | 132 | 133 | class ProfileViewSet(DynamicModelViewSet): 134 | features = ( 135 | DynamicModelViewSet.EXCLUDE, 136 | DynamicModelViewSet.FILTER, 137 | DynamicModelViewSet.INCLUDE, 138 | DynamicModelViewSet.SORT, 139 | ) 140 | model = Profile 141 | serializer_class = ProfileSerializer 142 | queryset = Profile.objects.all() 143 | 144 | 145 | class CatViewSet(DynamicModelViewSet): 146 | serializer_class = CatSerializer 147 | queryset = Cat.objects.all() 148 | 149 | 150 | class DogViewSet(DynamicModelViewSet): 151 | model = Dog 152 | serializer_class = DogSerializer 153 | queryset = Dog.objects.all() 154 | ENABLE_PATCH_ALL = True 155 | 156 | 157 | class HorseViewSet(DynamicModelViewSet): 158 | features = (DynamicModelViewSet.SORT,) 159 | model = Horse 160 | serializer_class = HorseSerializer 161 | queryset = Horse.objects.all() 162 | ordering_fields = ('name',) 163 | ordering = ('-name',) 164 | 165 | 166 | class ZebraViewSet(DynamicModelViewSet): 167 | features = (DynamicModelViewSet.SORT,) 168 | model = Zebra 169 | serializer_class = ZebraSerializer 170 | queryset = Zebra.objects.all() 171 | ordering_fields = '__all__' 172 | 173 | 174 | class PermissionViewSet(DynamicModelViewSet): 175 | serializer_class = PermissionSerializer 176 | queryset = Permission.objects.all() 177 | 178 | 179 | class CarViewSet(DynamicModelViewSet): 180 | serializer_class = CarSerializer 181 | queryset = Car.objects.all() 182 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts=--tb=short 3 | 4 | [tox] 5 | envlist = 6 | py310-lint, 7 | {py37,py38,py39,py310}-django{32}-drf{313,314}, 8 | {py37,py38,py39,py310}-django{40,41,42}-drf{314,315}, 9 | 10 | [testenv] 11 | commands = ./runtests.py --fast {posargs} --coverage -rw 12 | setenv = 13 | PYTHONDONTWRITEBYTECODE=1 14 | deps = 15 | django32: Django>=3.2,<3.3 16 | django40: Django>=4.0,<4.1 17 | django41: Django>=4.1,<4.2 18 | django42: Django>=4.2,<4.3 19 | drf313: djangorestframework>=3.13,<3.14 20 | drf314: djangorestframework>=3.14,<3.15 21 | drf315: djangorestframework>=3.15,<3.16 22 | -rrequirements.txt 23 | 24 | [testenv:py310-lint] 25 | commands = ./runtests.py --lintonly 26 | deps = -rrequirements.txt 27 | 28 | [testenv:py310-drf314-benchmarks] 29 | commands = ./runtests.py --benchmarks 30 | deps = 31 | Django==4.2.11 32 | djangorestframework==3.15.1 33 | -rrequirements.benchmark.txt 34 | --------------------------------------------------------------------------------