├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── LICENSE.md ├── MANIFEST.in ├── README.md ├── README.rst ├── code_of_conduct.md ├── docs ├── community │ ├── changelogs.md │ └── contributing_to_django_to_rest.md ├── full_guide │ ├── adding_auth.md │ ├── adding_custom_classes.md │ ├── adding_custom_filtering.md │ ├── adding_custom_serializer.md │ ├── adding_methods.md │ ├── adding_permission.md │ ├── adding_throttling.md │ ├── marking_model_for_REST.md │ ├── relationships.md │ ├── versioning.md │ └── viewnames.md ├── img │ ├── large_logo.png │ ├── large_logo_black.png │ ├── large_logo_blackq.png │ └── small_logo_grey.png ├── index.md └── quickstart.md ├── mkdocs.yml ├── pyproject.toml ├── setup.cfg ├── setup.py ├── tests ├── db.sqlite3 ├── manage.py ├── test_basics │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── filterset.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_studentwithcustomserializer.py │ │ ├── 0003_studentwithcustomauthandpermission.py │ │ ├── 0004_studentwithcustomthrottling.py │ │ ├── 0005_studentwithcustomfiltering.py │ │ ├── 0006_studentwithfiltersetclassvsfiltersetfield.py │ │ ├── 0007_studentwithcustommethod.py │ │ ├── 0008_studentwithcustomaction.py │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── tests.py │ └── view_params.py ├── test_basics_defaults │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── tests.py │ └── views.py ├── test_many_to_many │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── tests.py │ ├── view_params.py │ └── views.py ├── test_many_to_one │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_question1_choice1.py │ │ └── __init__.py │ ├── models.py │ ├── tests.py │ ├── view_params.py │ └── views.py ├── test_one_to_one │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_alter_system1_student.py │ │ └── __init__.py │ ├── models.py │ ├── tests.py │ └── views.py └── tests │ ├── __init__.py │ ├── asgi.py │ ├── settings.py │ ├── settings_test_basics_defaults.py │ ├── urls.py │ └── wsgi.py └── to_rest ├── __init__.py ├── __pycache__ ├── __init__.cpython-38.pyc ├── apps.cpython-38.pyc ├── cfg.cpython-38.pyc ├── constants.cpython-38.pyc ├── decorators.cpython-38.pyc ├── exceptions.cpython-38.pyc ├── serializers.cpython-38.pyc ├── utils.cpython-38.pyc └── views.cpython-38.pyc ├── apps.py ├── cfg.py ├── constants.py ├── decorators.py ├── exceptions.py ├── serializers.py ├── utils.py └── views.py /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [master] 5 | pull_request: 6 | branches: [master] 7 | 8 | jobs: 9 | build-test: 10 | name: Build and run test 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: ["3.8","3.9","3.10"] 15 | steps: 16 | - 17 | name: Checkout 18 | uses: actions/checkout@v3 19 | - 20 | name: Setup python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v3 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - 25 | name: Setup build env 26 | run: | 27 | python3 -m pip install --upgrade pip 28 | python3 -m pip install setuptools 29 | - 30 | name: Perform testing 31 | run: | 32 | python3 -m pip install ./ 33 | cd tests 34 | python3 manage.py test test_basics 35 | python3 manage.py test test_basics_defaults --settings=tests.settings_test_basics_defaults 36 | python3 manage.py test test_many_to_many 37 | python3 manage.py test test_many_to_one 38 | python3 manage.py test test_one_to_one 39 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - "v*" 6 | 7 | jobs: 8 | build-test-publish: 9 | name: Publish Python distributions to PyPI and TestPyPI 10 | runs-on: ubuntu-latest 11 | steps: 12 | - 13 | name: Checkout 14 | uses: actions/checkout@v3 15 | - 16 | name: Setup python 3.8.10 17 | uses: actions/setup-python@v3 18 | with: 19 | python-version: "3.8.10" 20 | - 21 | name: Setup build env 22 | run: | 23 | python3 -m pip install --upgrade pip 24 | python3 -m pip install setuptools 25 | - 26 | name: Perform testing 27 | run: | 28 | python3 -m pip install ./ 29 | cd tests 30 | python3 manage.py test test_basics 31 | python3 manage.py test test_basics_defaults --settings=tests.settings_test_basics_defaults 32 | python3 manage.py test test_many_to_many 33 | python3 manage.py test test_many_to_one 34 | python3 manage.py test test_one_to_one 35 | - 36 | name: Build the distribution 37 | run: | 38 | python3 setup.py sdist 39 | - 40 | name: Publish to PyPI 41 | uses: pypa/gh-action-pypi-publish@release/v1 42 | with: 43 | user: __token__ 44 | password: ${{ secrets.PYPI_API_TOKEN }} 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.tar.gz. 2 | /dist 3 | /django_to_rest.egg-info 4 | /virtualenv 5 | /tests/**/__pycache__ 6 | /site/ 7 | /build 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Anupam Sharma 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## django-to-rest 2 | 3 | 4 | 5 | * * * 6 | [![PyPI version](https://badge.fury.io/py/django-to-rest.svg)](https://badge.fury.io/py/django-to-rest) ![CI Passing](https://github.com/anp-scp/django-to-rest/actions/workflows/ci.yml/badge.svg) 7 | * * * 8 | Django To Rest is a small tool that helps to expose REST api(s) for 9 | django models with minimum effort. This utility is for one who uses `Django REST Framework` for writing REST APIs. The tool enables you to focus only on the code needed explicitly. The tool handles all boilerplate for writing CRUD APIs. Some of the features are: 10 | 11 | * Just add a decorator at top of a model and REST api(s) are created. That's the work!!! 12 | * Options like filtering and ordering are avilable by default for model fields 13 | * Easy customisations via the decorator itself 14 | * *Summary:* Less time??? Just install the tool and use the decorator. Done!!! 15 | 16 | * * * 17 | * *Documentation:* [https://anp-scp.github.io/django-to-rest](https://anp-scp.github.io/django-to-rest) 18 | * *Source Code:* [https://github.com/anp-scp/django-to-rest](https://github.com/anp-scp/django-to-rest) 19 | * * * 20 | 21 | ## **Requirements** 22 | 23 | Django to Rest need following requirements : 24 | 25 | * Python 3.8+ 26 | * Django 4.0.5+ 27 | * djangorestframework 3.13.1+ 28 | * django-filter 22.1 29 | 30 | * * * 31 | 32 | ## **Installation** 33 | 34 | django-to-rest is published as a package and can be installed using pip. Install with (consider creating a virtual environment): 35 | 36 | python3 -m pip install django-to-rest 37 | 38 | ## **Example** 39 | 40 | Let us have a look on an example of how the tool can be used to expose REST API. 41 | 42 | Let us assume that the following are the requirements: 43 | 44 | 1. A polls app having certain questions and each question have some choices. 45 | 2. All CRUD URLs for question and choice objects. 46 | 3. We need an URL which simply increments a counter 47 | 48 | Make sure that `djangorestframework` is installed and included in `INSTALLED_APPS ` in settings.py as shown below: 49 | ```py title="settings.py" linenums="1" 50 | ... 51 | INSTALLED_APPS = [ 52 | 'rest_framework', 53 | ... 54 | ] 55 | ... 56 | ``` 57 | Now create two models as shown below: 58 | ```py title="models.py" linenums="1" 59 | from django.db import models 60 | from django.utils import timezone 61 | from django.contrib import admin 62 | from to_rest.decorators import restifyModel # Import the decorator from the library 63 | 64 | # Create your models here. 65 | @restifyModel # Note the way decorator is used 66 | class Question(models.Model): 67 | question_text = models.CharField(max_length=200) 68 | pub_date = models.DateTimeField('date published') 69 | 70 | def __str__(self): 71 | return self.question_text 72 | 73 | 74 | @restifyModel # Note the way decorator is used 75 | class Choice(models.Model): 76 | question = models.ForeignKey(Question, on_delete=models.CASCADE,related_name='choices') 77 | choice_text = models.CharField(max_length=200) 78 | votes = models.IntegerField(default=0) 79 | 80 | def __str__(self): 81 | return self.choice_text 82 | ``` 83 | 84 | Note the use of the decorators. We just need to use the decorator and all the views and serializers would be created during startup. But apart from that, we need one more line to add in `urls.py` of the project (not any app) as shown below: 85 | ```py title="urls.py" linenums="1" 86 | from django.urls import path 87 | from to_rest import utils 88 | from django.http import JsonResponse 89 | 90 | urlpatterns = [ 91 | ... 92 | ] 93 | urlpatterns.extend(utils.restifyApp('rest/v1')) # call this method to add the urls in url patterns. Here the parameter 'rest/v1' is the prefix to be used in the url. 94 | ``` 95 | 96 | That's all. All the above configurations will create the CRUD APIs for the classes that we marked using the decorator. For the 3rd requirement we can simply write a method the way we write in `Django` or `Django REST Framework`. We add the following lines in `urls.py`: 97 | 98 | ```py 99 | count = 0 100 | 101 | def counter(request) : 102 | global count 103 | if request.method == 'GET': 104 | count += 1 105 | return JsonResponse({'count': count}) 106 | urlpatterns.append(path('count/', counter)) 107 | ``` 108 | 109 | Now start the server. We add some data and check the dev url `http://127.0.0.1:8000/`. Below is an example with httpie: 110 | 111 | $ http -b --unsorted http://127.0.0.1:8000/ 112 | { 113 | "rest/v1/polls/question": "http://127.0.0.1:8000/rest/v1/polls/question", 114 | "rest/v1/polls/choice": "http://127.0.0.1:8000/rest/v1/polls/choice" 115 | } 116 | 117 | $ http -b --unsorted http://127.0.0.1:8000/rest/v1/polls/question 118 | [ 119 | { 120 | "id": 1, 121 | "question_text": "How is the traffic?", 122 | "pub_date": "2022-07-08T10:02:16.290713Z", 123 | "choices": "/rest/v1/polls/question/1/choices" 124 | }, 125 | { 126 | "id": 2, 127 | "question_text": "What's up?", 128 | "pub_date": "2022-07-08T10:03:15.816192Z", 129 | "choices": "/rest/v1/polls/question/2/choices" 130 | } 131 | ] 132 | 133 | $ http -b --unsorted http://127.0.0.1:8000/rest/v1/polls/question/1/choices 134 | [ 135 | { 136 | "id": 1, 137 | "choice_text": "Highly Conjested", 138 | "votes": 0, 139 | "question": 1 140 | }, 141 | { 142 | "id": 2, 143 | "choice_text": "Clear for miles", 144 | "votes": 0, 145 | "question": 1 146 | } 147 | ] 148 | 149 | $ http -b --unsorted http://127.0.0.1:8000/count/ 150 | { 151 | "count": 1 152 | } 153 | 154 | $ http -b --unsorted http://127.0.0.1:8000/count/ 155 | { 156 | "count": 2 157 | } 158 | 159 | $ http -b --unsorted http://127.0.0.1:8000/count/ 160 | { 161 | "count": 3 162 | } 163 | 164 | Here, we wrote extra code only for the `/count/` URL and other CRUD URLs where created by the utility. 165 | 166 | ## **Quickstart** 167 | 168 | The [quick start guide](https://anp-scp.github.io/django-to-rest/quickstart/) is a short tutorial which is the fastest way to get everything setup and get an overview of the tool. 169 | 170 | ## **Contributing** 171 | 172 | Check the [contribution guidelines](https://anp-scp.github.io/django-to-rest/community/contributing_to_django_to_rest/) to know about how to contribute to the project. 173 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-to-rest 2 | -------------- 3 | 4 | .. image:: https://raw.githubusercontent.com/anp-scp/django-to-rest/master/docs/img/large_logo_black.png 5 | :width: 300px 6 | :alt: Django-to-rest 7 | :align: left 8 | 9 | 10 | -------------- 11 | 12 | |PyPI version| |CI Passing| 13 | 14 | -------------- 15 | 16 | Django To Rest is small tool that helps to expose REST api(s) for 17 | django models with minimum effort. This utility is for one who uses 18 | `Django REST Framework` for writing REST APIs. The tool enables you 19 | to focus only on the code needed explicitly. The tool handles all 20 | boilerplate for writing CRUD APIs. Some of the features are: 21 | 22 | - Just add a decorator atop of a model and REST api(s) are created. 23 | That's the work!!! 24 | - Options like filtering and ordering are avilable by default for model 25 | fields 26 | - Easy customisations via the decorator itself 27 | - *Summary:* Less time??? Just install the tool and the decorator. 28 | Done!!! 29 | 30 | -------------- 31 | 32 | - *Documentation:* 33 | `https://anp-scp.github.io/django-to-rest `__ 34 | - *Source Code:* 35 | `https://github.com/anp-scp/django-to-rest `__ 36 | 37 | -------------- 38 | 39 | **Requirements** 40 | ---------------- 41 | 42 | Django to Rest need following requirements : 43 | 44 | - Python 3.8+ 45 | - Django 4.0.5+ 46 | - djangorestframework 3.13.1+ 47 | - django-filter 22.1 48 | 49 | -------------- 50 | 51 | **Installation** 52 | ---------------- 53 | 54 | django-to-rest is published as a package and can be installed using pip. 55 | Install with (consider creating a virtual environment): 56 | 57 | :: 58 | 59 | python3 -m pip install django-to-rest 60 | 61 | **Example** 62 | ----------- 63 | 64 | Let us have a look on an example of how the tool can be used to expose 65 | REST API. 66 | 67 | Let us assume that the following are the requirements: 68 | 69 | #. A polls app having certain questions and each question have some choices. 70 | #. All CRUD URLs for question and choice objects. 71 | #. We need an URL which simply increments a counter 72 | 73 | Make sure that ``djangorestframework`` is installed and included in 74 | ``INSTALLED_APPS`` in settings.py as shown below: 75 | 76 | .. code:: py 77 | 78 | ... 79 | INSTALLED_APPS = [ 80 | 'rest_framework', 81 | ... 82 | ] 83 | ... 84 | 85 | Now create two models as shown below: 86 | 87 | .. code:: py 88 | 89 | from django.db import models 90 | from django.utils import timezone 91 | from django.contrib import admin 92 | from to_rest.decorators import restifyModel # Import the decorator from the library 93 | 94 | # Create your models here. 95 | @restifyModel # Note the way decorator is used 96 | class Question(models.Model): 97 | question_text = models.CharField(max_length=200) 98 | pub_date = models.DateTimeField('date published') 99 | 100 | def __str__(self): 101 | return self.question_text 102 | 103 | 104 | @restifyModel # Note the way decorator is used 105 | class Choice(models.Model): 106 | question = models.ForeignKey(Question, on_delete=models.CASCADE,related_name='choices') 107 | choice_text = models.CharField(max_length=200) 108 | votes = models.IntegerField(default=0) 109 | 110 | def __str__(self): 111 | return self.choice_text 112 | 113 | Note the use of the decorators. We just need to use the decorator and 114 | all the views and serializers would be created during startup. But apart 115 | from that, we need one more line to add in ``urls.py`` of the project 116 | (not any app) as shown below: 117 | 118 | .. code:: py 119 | 120 | from django.urls import path 121 | from to_rest import utils 122 | from django.http import JsonResponse 123 | 124 | urlpatterns = [ 125 | ... 126 | ] 127 | urlpatterns.extend(utils.restifyApp('rest/v1')) # call this method to add the urls in url patterns. Here the parameter 'rest/v1' is the prefix to be used in the url. 128 | 129 | That's all. All the above configurations will create the CRUD APIs for the classes that we 130 | marked using the decorator. For the 3rd requirement we can simply write a method the way 131 | we write in `Django` or `Django REST Framework`. We add the following lines in `urls.py`: 132 | 133 | .. code:: py 134 | 135 | count = 0 136 | 137 | def counter(request) : 138 | global count 139 | if request.method == 'GET': 140 | count += 1 141 | return JsonResponse({'count': count}) 142 | urlpatterns.append(path('count/', counter)) 143 | 144 | Now start the server. We add some data and check the dev url `http://127.0.0.1:8000/`. 145 | Below is an example with httpie: 146 | 147 | :: 148 | 149 | $ http -b --unsorted http://127.0.0.1:8000/ 150 | { 151 | "rest/v1/polls/question": "http://127.0.0.1:8000/rest/v1/polls/question", 152 | "rest/v1/polls/choice": "http://127.0.0.1:8000/rest/v1/polls/choice" 153 | } 154 | 155 | $ http -b --unsorted http://127.0.0.1:8000/rest/v1/polls/question 156 | [ 157 | { 158 | "id": 1, 159 | "question_text": "How is the traffic?", 160 | "pub_date": "2022-07-08T10:02:16.290713Z", 161 | "choices": "/rest/v1/polls/question/1/choices" 162 | }, 163 | { 164 | "id": 2, 165 | "question_text": "What's up?", 166 | "pub_date": "2022-07-08T10:03:15.816192Z", 167 | "choices": "/rest/v1/polls/question/2/choices" 168 | } 169 | ] 170 | 171 | $ http -b --unsorted http://127.0.0.1:8000/rest/v1/polls/question/1/choices 172 | [ 173 | { 174 | "id": 1, 175 | "choice_text": "Highly Conjested", 176 | "votes": 0, 177 | "question": 1 178 | }, 179 | { 180 | "id": 2, 181 | "choice_text": "Clear for miles", 182 | "votes": 0, 183 | "question": 1 184 | } 185 | ] 186 | 187 | $ http -b --unsorted http://127.0.0.1:8000/count/ 188 | { 189 | "count": 1 190 | } 191 | 192 | $ http -b --unsorted http://127.0.0.1:8000/count/ 193 | { 194 | "count": 2 195 | } 196 | 197 | $ http -b --unsorted http://127.0.0.1:8000/count/ 198 | { 199 | "count": 3 200 | } 201 | 202 | Here, we wrote extra code only for the `/count/` URL 203 | and other CRUD URLs where created by the utility. 204 | 205 | **Quickstart** 206 | -------------- 207 | 208 | The `quick start 209 | guide `__ is a 210 | short tutorial which is the fastest way to get everything setup and get 211 | an overview of the tool. 212 | 213 | .. |PyPI version| image:: https://badge.fury.io/py/django-to-rest.svg 214 | :target: https://badge.fury.io/py/django-to-rest 215 | .. |CI Passing| image:: https://github.com/anp-scp/django-to-rest/actions/workflows/ci.yml/badge.svg 216 | 217 | **Contributing** 218 | ---------------- 219 | 220 | Check the `contribution guidelines `__ to know about how to contribute to the project. -------------------------------------------------------------------------------- /code_of_conduct.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | [djangotorest@gmail.com](mailto:djangotorest@gmail.com). 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 127 | [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | 135 | * * * 136 | Note on reporting guidelines: 137 | 138 | 1. Mention `Django-to-rest` in the subject. 139 | 2. Give suffucient details. -------------------------------------------------------------------------------- /docs/community/changelogs.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Changelogs 3 | --- 4 | Django-to-rest 1.1.0 5 | ---------------------- 6 | * Django 4.1 support 7 | * djangorestframework 3.14 support 8 | * API Versioning 9 | * Updated docs 10 | 11 | Django-to-rest 1.0.0b2 12 | ---------------------- 13 | * Fixed readme.rst file 14 | 15 | Django-to-rest 1.0.0b1 16 | ---------------------- 17 | 18 | * Bug fixes related to nested urls for relationship 19 | * Custom view parameters now applies to nested urls 20 | * Support for adding custom methods as view parameters 21 | * Support for customizing behaviour for nested urls for relationship 22 | * Any other custom parameters for view set can be passed to the decorator 23 | * Support for urls with and without trailing slashes 24 | * Updated documentation with following topics: 25 | - Adding throttling options 26 | - Adding custom mfiltering 27 | - Adding methods 28 | - Relationships 29 | 30 | Django-to-rest 0.1a3 31 | -------------------- 32 | 33 | * First basic pre-release 34 | * Nested urls for one-to-one, many-to-one, and many-to-many 35 | * Documentaion with topics: 36 | - Marking models 37 | - Custom serializer 38 | - Custom Authentication class 39 | - Custom Permission class -------------------------------------------------------------------------------- /docs/community/contributing_to_django_to_rest.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Contributing to django-to-rest 3 | --- 4 | 5 | Thank you for your interest in contrubiting to this project. Please get involved and help to make the project better. The contributions can be in the form of ideas, development, documentations and testing in the form test scripts. 6 | 7 | Code of Conduct 8 | --------------- 9 | 10 | Check this link for code of conduct: [Code of Conduct](https://github.com/anp-scp/django-to-rest/blob/master/code_of_conduct.md) 11 | 12 | Issues 13 | ------ 14 | 15 | * Ensure that you discuss the topic before raising an issue. Check [Github discussions page](https://github.com/anp-scp/django-to-rest/discussions) for discussions. 16 | * Ensure that the issues are properly explained 17 | * Before raising an issue check if similar issue already exists in [Github issues page](https://github.com/anp-scp/django-to-rest/issues) 18 | 19 | Development 20 | ----------- 21 | 22 | To start developing: 23 | 24 | 1. First create a fork 25 | 2. Clone your fork 26 | 3. It is recommended to have a virtual environment 27 | 4. Install `django 4.0.5`, `django-rest-framework 3.13.1` and `django-filter 22.1` 28 | 6. Create new branch 29 | 5. Start you development on the new branch... 30 | 6. To test your changes in a django project install the project from your local machine using the command: `python3 -m pip install path-to-project's-directory`. For example, if the directory `django-to-rest` is in `~/project/` then the command would be `python3 -m pip install ~/project/django-to-rest/` 31 | 7. And then follow the [Quickstart Guide](../quickstart.md) for better understanding. Note that the project needs to be installed from the local machine and not from PyPi. 32 | 33 | See [GitHub's Fork a Repo Guide](https://docs.github.com/en/get-started/quickstart/fork-a-repo) for more help. 34 | 35 | Testing 36 | ------- 37 | 38 | !!! Note 39 | 40 | Apart from development, you can also contribute test scripts to cover the scenarios that are not covered yet to make the project better. 41 | 42 | There are five apps in the `tests` directory for testing different scenarios: 43 | 44 | 1. `tests/test_basics` 45 | - To test generic scenarios 46 | - Command to run test: 47 | ``` 48 | $ python3 manage.py test test_basics 49 | ``` 50 | 51 | 2. `tests/test_basics_defaults` 52 | - To test generic scenarios with configuration in settings file 53 | - For this app there is a dedicated settings file at `tests/tests/settings_test_basics_defaults.py` 54 | - Command to run test: 55 | ``` 56 | $ python3 manage.py test test_basics_defaults --settings=tests.settings_test_basics_defaults 57 | ``` 58 | 59 | 3. `tests/test_many_to_many` 60 | - To test scenarios related to many-to-many relationship 61 | - Command to run test: 62 | ``` 63 | $ python3 manage.py test test_many_to_many 64 | ``` 65 | 66 | 4. `tests/test_many_to_one` 67 | - To test scenarios related to many-to-one relationship 68 | - Command to run test: 69 | ``` 70 | $ python3 manage.py test test_many_to_one 71 | ``` 72 | 73 | 5. `tests/test_one_to_one` 74 | - To test scenarios related to one-to-one relationship 75 | - Command to run test: 76 | ``` 77 | $ python3 manage.py test test_one_to_one 78 | ``` 79 | 80 | Before running any test first install django-to-rest by executing below command at the root directory of the repository (where the setup.py resides). This will also install other packages required (that are mentioned in the `Development` section): 81 | 82 | $ python3 -m pip install ./ 83 | 84 | Whenever any change in code is made, uninstall django-to-rest: 85 | 86 | $ python3 -m pip uninstall django-to-rest 87 | 88 | And then install again to run the test on the updated code. 89 | Ensure that the test scripts are well commented so that one can understand about the scenario for which it is tested. Check existing scripts for example. 90 | 91 | For creating any test one would need the view names for reversing urls. Check [this page](../full_guide/viewnames.md) for how the view names are generated by the library. 92 | 93 | Documentation 94 | ------------- 95 | 96 | The documentation is made using [Material for Mkdocs](https://squidfunk.github.io/mkdocs-material/). All the files related to documentation is inside `docs` directory. Just update the code and hit below command to preview: 97 | 98 | $ mkdocs serve 99 | 100 | Check [Material for Mkdocs](https://squidfunk.github.io/mkdocs-material/) for more help. 101 | 102 | Contribute and make pull request 103 | ------------- 104 | 105 | All the contributions have to be made via a pull request. After you have cloned the forked repository, follow below steps: 106 | 107 | 1. Go into the project's directory (that is `django-to-rest`) 108 | 2. Create a new branch using following command in the command line: `git branch new-branch-name` 109 | 3. Checkout to the new branch using following command in the command line: `git checkout new-branch-name` 110 | 4. Make the changes that you want to contribute 111 | 5. Stage your changes using the following command in command line: `git command .` 112 | 6. Check the status using the command: `git status` 113 | 7. Commit your changes using the command: `git commit -m "commit message"` 114 | 8. Push your changes to the remote branch on GitHub by using the following command: `git push -u origin branch_name` 115 | 9. Open a pull request directed to our `master` branch 116 | 117 | For tutorials on pull request check below links: 118 | 119 | * [Github: About pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests) 120 | * [W3schools: Send Pull Request](https://www.w3schools.com/git/git_remote_send_pull_request.asp?remote=github) -------------------------------------------------------------------------------- /docs/full_guide/adding_auth.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Adding Authentication class 3 | --- 4 | 5 | !!! Note 6 | 7 | It is advised to go through point 2 of [Marking models to create REST APIs](marking_model_for_REST.md) and [Adding Custom Serializer](adding_custom_serializer.md) and have an understanding of passing custom view attributes. Here, only example of `view_params` is given. 8 | 9 | To add authentication class, the dictionary returned by `getParams()` method of a `ViewParams` class must have an entry with key `constants.AUTHENTICATION_CLASSES`. For example: 10 | 11 | ``` py title="view_params.py" linenums="1" 12 | from to_rest import constants 13 | from test_basics import serializers # (1) 14 | from to_rest.utils import ViewParams 15 | 16 | class CustomAuthAndPermission(ViewParams): 17 | 18 | def getParams(): 19 | temp = dict() 20 | temp[constants.AUTHENTICATION_CLASSES] = [BasicAuthentication] 21 | temp[constants.PERMISSION_CLASSES] = [IsAuthenticatedOrReadOnly] 22 | return temp 23 | ``` 24 | 25 | 1. Here, test_basics is the directory of the app 26 | 27 | Ensure that the name of the class is passed to the decorator `restifyModel()` in `models.py`. For example: 28 | 29 | ```py title="models.py" linenums="1" 30 | from django.db import models 31 | from to_rest.decorators import restifyModel 32 | 33 | @restifyModel(customViewParams='CustomAuthAndPermission') 34 | class StudentWithCustomAuthAndPermission(models.Model): 35 | name = models.CharField(max_length=50) 36 | 37 | def __str__(self): 38 | return "[name={}]".format(self.name) 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/full_guide/adding_custom_classes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Adding custom views, viewsets, etc 3 | --- 4 | 5 | Apart from what the tool does, all the other functionalities given by `Django REST Framework` can be added in the way it is normally done. 6 | 7 | !!! Note 8 | 9 | Name of any other custom classes must not start with `DjangoToRest_`. -------------------------------------------------------------------------------- /docs/full_guide/adding_custom_filtering.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Adding custom Filtering 3 | --- 4 | 5 | !!! Note 6 | 7 | It is advised to go through point 2 of [Marking models to create REST APIs](marking_model_for_REST.md) and [Adding Custom Serializer](adding_custom_serializer.md) and have an understanding of passing custom view attributes. 8 | 9 | For filtering, following keys can be used in the dictionary that would be returned from the `getParams()` method of a `ViewParams` class: 10 | 11 | * to_rest.constants.FILTER_BACKENDS: To specify filter backends 12 | * to_rest.constants.FILTERSET_FIELDS: To specify filterset fields 13 | * to_rest.constants.SEARCH_FIELDS: To specify search fields 14 | * to_rest.constants.ORDERING_FIELDS: To specify ordering fields 15 | * to_rest.constants.ORDERING: To specify default ordering 16 | * to_rest.constants.FILTERSET_CLASS: To specify filterset class 17 | 18 | !!! Note 19 | 20 | If both `to_rest.constants.FILTERSET_CLASS` and `to_rest.constants.FILTERSET_FIELDS` are used then `to_rest.constants.FILTERSET_CLASS` is given preference and `to_rest.constants.FILTERSET_FIELDS` is ignored. 21 | 22 | For example, let us consider the below model: 23 | 24 | ```py title="models.py" linenums="1" 25 | from django.db import models 26 | 27 | class StudentWithCustomFiltering(models.Model): 28 | name = models.CharField(max_length=50) 29 | year = models.IntegerField() 30 | discipline = models.CharField(max_length=20) 31 | 32 | def __str__(self): 33 | return "[name={}]".format(self.name) 34 | ``` 35 | 36 | For above model, various filtering attributes can be provided via a `ViewParams` class. For example: 37 | 38 | ``` py title="view_params.py" linenums="1" 39 | from to_rest import constants 40 | from to_rest.utils import ViewParams 41 | from rest_framework.filters import SearchFilter, OrderingFilter 42 | from test_basics import filterset # (1) 43 | from django_filters.rest_framework import DjangoFilterBackend 44 | 45 | class CustomFiltering(ViewParams): 46 | 47 | def getParams(): 48 | temp = dict() 49 | temp[constants.FILTER_BACKENDS] = [DjangoFilterBackend, SearchFilter, OrderingFilter] 50 | temp[constants.FILTERSET_FIELDS] = ['name', 'year', 'discipline'] 51 | temp[constants.SEARCH_FIELDS] = ['name'] 52 | temp[constants.ORDERING_FIELDS] = ['discipline', 'year'] 53 | temp[constants.ORDERING] = ['year'] 54 | return temp 55 | ``` 56 | 57 | 1. Here, test_basics is the directory of app 58 | 59 | Ensure that the name of the class is passed to the decorator `restifyModel()` in `models.py`. For example: 60 | 61 | ```py title="models.py" linenums="1" 62 | from django.db import models 63 | from to_rest.decorators import restifyModel 64 | 65 | @restifyModel(customViewParams='CustomFiltering') 66 | class StudentWithCustomFiltering(models.Model): 67 | name = models.CharField(max_length=50) 68 | year = models.IntegerField() 69 | discipline = models.CharField(max_length=20) 70 | 71 | def __str__(self): 72 | return "[name={}]".format(self.name) 73 | ``` -------------------------------------------------------------------------------- /docs/full_guide/adding_custom_serializer.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Adding custom Serializer 3 | --- 4 | 5 | !!! Note 6 | 7 | It is advised to go through point 2 of [Marking models to create REST APIs](marking_model_for_REST.md) 8 | 9 | Let us consider the below model: 10 | 11 | ```py title="models.py" linenums="1" 12 | from django.db import models 13 | 14 | class StudentWithCustomSerializer(models.Model): 15 | name = models.CharField(max_length=50) 16 | 17 | def __str__(self): 18 | return "[name={}, year={}]".format(self.name, self.year) 19 | ``` 20 | 21 | A simple serializer for the model above is given below: 22 | 23 | ``` py title="serializers.py" linenums="1" 24 | from rest_framework import serializers 25 | from test_basics.models import StudentWithCustomSerializer # (1) 26 | 27 | class StudentWithCustomSerializerSerializer(serializers.Serializer): 28 | 29 | id = serializers.IntegerField(read_only=True) 30 | name = serializers.CharField() 31 | 32 | def create(self, validated_data): 33 | return StudentWithCustomSerializer.objects.create(**validated_data) 34 | ``` 35 | 36 | 1. Here, test_basics is the directory of the app 37 | 38 | As mentioned in [Marking models to create REST APIs](marking_model_for_REST.md), all the custom parameters for view needs to be mentioned in a `ViewParams` class and all such class needs to be in module `view_params` in the directory of the app. Let us create a new file in the same working directory called `view_params.py` and create a class as shown below: 39 | 40 | ``` py title="view_params.py" linenums="1" 41 | from to_rest import constants 42 | from test_basics import serializers # (1) 43 | from to_rest.utils import ViewParams 44 | 45 | class CustomSerializer(ViewParams): 46 | 47 | def getParams(): 48 | temp = dict() 49 | temp[constants.SERIALIZER_CLASS] = serializers.StudentWithCustomSerializerSerializer 50 | return temp 51 | ``` 52 | 53 | 1. Here, test_basics is the directory of the app 54 | 55 | Note that the dictionary returned by `getParams()` must contain an entry with key `to_rest.constants.SERIALIZER_CLASS`. Now, let us go back to models.py and see how to provide the custom serializer that we created. 56 | 57 | ```py title="models.py" linenums="1" 58 | from django.db import models 59 | from to_rest.decorators import restifyModel 60 | 61 | @restifyModel(customViewParams='CustomSerializer') 62 | class StudentWithCustomSerializer(models.Model): 63 | name = models.CharField(max_length=50) 64 | 65 | def __str__(self): 66 | return "[name={}, year={}]".format(self.name, self.year) 67 | ``` 68 | 69 | Note the way `CustomSerializer` is passed to the decorator at line 4. -------------------------------------------------------------------------------- /docs/full_guide/adding_methods.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Adding methods 3 | --- 4 | 5 | ## Adding a normal method 6 | 7 | Any methods can be added to the view set using a `ViewParams` class. To add a method, an element of dictionary returned by `getParams()` method of the `ViewParams` class should have a key as name of the method (str) and the value should be reference to the function object. 8 | 9 | For example, to use custom `list()` method instead of the one given by django-to-rest something similar to below example can be used: 10 | 11 | ``` py title="view_params.py" linenums="1" 12 | from to_rest import constants 13 | from to_rest.utils import ViewParams 14 | from test_basics import models # Here, test_basics is the app directory 15 | 16 | class CustomListMethod(ViewParams): 17 | 18 | def getParams(): 19 | def list(self, request, *args, **kwargs): 20 | objects = models.StudentWithCustomMethod.objects.filter(year=2) 21 | serializer = self.get_serializer(objects, many=True) 22 | return Response(serializer.data) 23 | temp = dict() 24 | temp['list'] = list 25 | return temp 26 | ``` 27 | 28 | !!! Note 29 | 30 | The name of this class has to be passed to the decorator in `models.py`. 31 | 32 | Similarly, any method can be added to the view set. 33 | 34 | ## Adding a decorated method 35 | 36 | Let us consider the following decorated method: 37 | 38 | ```py title="decorator with no parameters" linenums="1" 39 | @decorator 40 | def function(): 41 | pass 42 | ``` 43 | 44 | The above decorated function is same as: 45 | 46 | ```py linenums="1" 47 | def function(): 48 | pass 49 | function = decorator(function) 50 | ``` 51 | 52 | Moreover, following decorated method: 53 | 54 | ```py title="decorater with parameters" linenums="1" 55 | @decorator(param1=abc) 56 | def function(): 57 | pass 58 | ``` 59 | 60 | Above is same as: 61 | 62 | ```py linenums="1" 63 | def function(): 64 | pass 65 | function = decorator(param1=abc)(function) 66 | ``` 67 | 68 | Hence, to add decorated method, instead of using `@` notation we just need to use the later. Below is an example of adding an action: 69 | 70 | ``` py title="view_params.py" linenums="1" 71 | from to_rest import constants 72 | from to_rest.utils import ViewParams 73 | from test_basics import models # Here, test_basics is the app directory 74 | from rest_framework.response import Response 75 | from rest_framework.decorators import action 76 | 77 | class CustomAction(ViewParams): 78 | 79 | def getParams(): 80 | def customaction(self, request, pk=None): 81 | obj = models.StudentWithCustomAction.objects.get(pk=pk) 82 | return Response({'msg':"custom action working for " + obj.name}) 83 | customaction = action(detail=True, methods=['get'], url_name='customaction')(customaction) 84 | temp = dict() 85 | temp['customaction'] = customaction 86 | return temp 87 | ``` 88 | 89 | Note the way the decorator is used at line 13. -------------------------------------------------------------------------------- /docs/full_guide/adding_permission.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Adding Permission class 3 | --- 4 | 5 | !!! Note 6 | 7 | It is advised to go through point 2 of [Marking models to create REST APIs](marking_model_for_REST.md) and [Adding Custom Serializer](adding_custom_serializer.md) and have an understanding of passing custom view attributes. Here, only example of `view_params` is given. 8 | 9 | To add custom permission class, the dictionary returned by `getParams()` method of a `ViewParams` class must have an entry with key `constants.PERMISSION_CLASSES`. For example: 10 | 11 | ``` py title="view_params.py" linenums="1" 12 | from to_rest import constants 13 | from test_basics import serializers # (1) 14 | from to_rest.utils import ViewParams 15 | 16 | class CustomAuthAndPermission(ViewParams): 17 | 18 | def getParams(): 19 | temp = dict() 20 | temp[constants.AUTHENTICATION_CLASSES] = [BasicAuthentication] 21 | temp[constants.PERMISSION_CLASSES] = [IsAuthenticatedOrReadOnly] 22 | return temp 23 | ``` 24 | 25 | 1. Here, test_basics is the directory of the app 26 | 27 | Ensure that the name of the class is passed to the decorator `restifyModel()` in `models.py`. For example: 28 | 29 | ```py title="models.py" linenums="1" 30 | from django.db import models 31 | from to_rest.decorators import restifyModel 32 | 33 | @restifyModel(customViewParams='CustomAuthAndPermission') 34 | class StudentWithCustomAuthAndPermission(models.Model): 35 | name = models.CharField(max_length=50) 36 | 37 | def __str__(self): 38 | return "[name={}]".format(self.name) 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/full_guide/adding_throttling.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Adding throttling options 3 | --- 4 | 5 | !!! Note 6 | 7 | It is advised to go through point 2 of [Marking models to create REST APIs](marking_model_for_REST.md) and [Adding Custom Serializer](adding_custom_serializer.md) and have an understanding of passing custom view attributes. 8 | 9 | Here, an example of [ScopedRateThrottle](https://www.django-rest-framework.org/api-guide/throttling/#scopedratethrottle) class is given as other types of classes and options can be managed solely via `settings.py`. 10 | 11 | Let us consider the below model: 12 | 13 | ```py title="models.py" linenums="1" 14 | from django.db import models 15 | 16 | class StudentWithCustomThrottling(models.Model): 17 | name = models.CharField(max_length=50) 18 | 19 | def __str__(self): 20 | return "[name={}]".format(self.name) 21 | ``` 22 | 23 | To add throttling class, the dictionary returned by `getParams()` method of a `ViewParams` class must have an entry with key `constants.THROTTLE_SCOPE`. For example: 24 | 25 | ``` py title="view_params.py" linenums="1" 26 | from to_rest import constants 27 | from to_rest.utils import ViewParams 28 | 29 | class CustomThrottling(ViewParams): 30 | 31 | def getParams(): 32 | temp = dict() 33 | temp[constants.THROTTLE_SCOPE] = "studentCustomThrottle" 34 | return temp 35 | ``` 36 | 37 | Now, let us go back to models.py and see how to provide the throttle scope as viewset attribute. 38 | 39 | ```py title="models.py" linenums="1" 40 | from django.db import models 41 | from to_rest.decorators import restifyModel 42 | 43 | @restifyModel(customViewParams='CustomThrottling') 44 | class StudentWithCustomThrottling(models.Model): 45 | name = models.CharField(max_length=50) 46 | 47 | def __str__(self): 48 | return "[name={}]".format(self.name) 49 | ``` 50 | 51 | Also, ensure that throttle class and other options are specified in `settings.py`: 52 | 53 | ```py title="settings.py" linenums="1" 54 | ... 55 | REST_FRAMEWORK = { 56 | ... 57 | 'DEFAULT_THROTTLE_CLASSES': [ 58 | 'rest_framework.throttling.ScopedRateThrottle', 59 | ], 60 | 'DEFAULT_THROTTLE_RATES': { 61 | 'studentCustomThrottle': '5/min' 62 | } 63 | ... 64 | } 65 | ``` -------------------------------------------------------------------------------- /docs/full_guide/marking_model_for_REST.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Marking models to create REST APIs 3 | --- 4 | 5 | Marking custom models 6 | --------------------- 7 | 8 | To create REST APIs for a model, first we need to mark the model. And to mark the model, use the decorator `to_rest.decorators.restifyModel`. The decorator can be used in the following two ways: 9 | 10 | 1. **Without Parameters**: When used without parameters, all the defaults would be applied. 11 | For example: 12 | 13 | ```py title="/.../quickstart/mysite/polls/models.py" linenums="1" 14 | from django.db import models 15 | from django.utils import timezone 16 | from django.contrib import admin 17 | from to_rest.decorators import restifyModel # (1) 18 | 19 | # Create your models here. 20 | @restifyModel # (2) 21 | class Question(models.Model): 22 | question_text = models.CharField(max_length=200) 23 | pub_date = models.DateTimeField('date published') 24 | 25 | def __str__(self): 26 | return self.question_text 27 | 28 | 29 | @restifyModel # (3) 30 | class Choice(models.Model): 31 | question = models.ForeignKey(Question, on_delete=models.CASCADE,related_name='choices') 32 | choice_text = models.CharField(max_length=200) 33 | votes = models.IntegerField(default=0) 34 | 35 | def __str__(self): 36 | return self.choice_text 37 | ``` 38 | 39 | 1. Import the decorator from the library 40 | 2. Note the way decorator is used 41 | 3. Note the way decorator is used 42 | 43 | 2. **With Parameters**: The decorator accepts the following parameters 44 | * `customViewParams (str)`: This accepts the name of a `ViewParams` class. The `ViewParams` class needs to override the class method `getParams()` to provide customized methods and attributes for view set. For example, custom serializer, list method, create method, retreive method, update method, partial_update method, delete method, get_object method, get_queryset, etc. The `getParams()` method must return a dictionary. 45 | * `excludeFields (list)`: The fields that needs to be excluded from the JSON object. Provided fields will not be included in the serializer. If customSerializer is provided then this parameter will be ignored. 46 | * `methodFields (list)`: The list of methods as read only fields. This can be used to include the model's methods' output as field. This includes only those field that don't take any parameter. 47 | 48 | An example of passing custom serializer is given below: 49 | 50 | === "serializers.py" 51 | 52 | ``` py linenums="1" 53 | from rest_framework import serializers 54 | from test_basics.models import StudentWithCustomSerializer # (1) 55 | 56 | class StudentWithCustomSerializerSerializer(serializers.Serializer): 57 | 58 | id = serializers.IntegerField(read_only=True) 59 | name = serializers.CharField() 60 | 61 | def create(self, validated_data): 62 | return StudentWithCustomSerializer.objects.create(**validated_data) 63 | ``` 64 | 65 | 1. Here, test_basics is the directory of the app 66 | 67 | === "view_params.py" 68 | 69 | ``` py linenums="1" 70 | from to_rest import constants 71 | from test_basics import serializers # (1) 72 | from to_rest.utils import ViewParams 73 | 74 | class CustomSerializer(ViewParams): 75 | 76 | def getParams(): 77 | temp = dict() 78 | temp[constants.SERIALIZER_CLASS] = serializers.StudentWithCustomSerializerSerializer 79 | return temp 80 | ``` 81 | 82 | 1. Here, test_basics is the directory of the app 83 | 84 | === "models.py" 85 | 86 | ```py linenums="1" 87 | from django.db import models 88 | from to_rest.decorators import restifyModel 89 | 90 | @restifyModel(customViewParams='CustomSerializer') 91 | class StudentWithCustomSerializer(models.Model): 92 | name = models.CharField(max_length=50) 93 | 94 | def __str__(self): 95 | return "[name={}, year={}]".format(self.name, self.year) 96 | ``` 97 | In the above example, a custom serializer has been created in `serializers.py`. A `ViewParams` class, `CustomSerializer` is created in `view_params.py` to provide the custom serializer. And the name of the `ViewParams` class is provided in decorator at line 4 in `models.py`. 98 | 99 | !!! Note 100 | 101 | All `ViewParams` classes must be in the module `view_params` in the directory of the app. That means, in the same location where `models.py` is located. Django-to-rest will get the name of the `ViewParams` class from the decorator and will search that class in the module `view_params.` Hence, in the example above, `CustomSerializer` is created in `view_params.py`. 102 | 103 | 104 | Marking models provided by Django 105 | --------------------------------- 106 | 107 | To create REST APIs for models provided by django, the models can be marked as follows: 108 | 109 | ```py title="models.py" linenums="1" 110 | from django.db import models 111 | from to_rest.decorators import restifyModel 112 | from django.contrib.auth.models import User, Permission 113 | 114 | # Create your models here. 115 | User = restifyModel(User) 116 | Permission = restifyModel(Permission) 117 | ``` -------------------------------------------------------------------------------- /docs/full_guide/relationships.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Relationship 3 | --- 4 | 5 | One-to-one relationships 6 | ------------------------ 7 | This section shows the behaviour for one to one relationship: 8 | 9 | Consider following models: 10 | 11 | ```py title="models.py" linenums="1" 12 | from django.db import models 13 | from to_rest.decorators import restifyModel 14 | 15 | # Create your models here. 16 | @restifyModel 17 | class Student(models.Model): 18 | name = models.CharField(max_length=75) 19 | discipline = models.CharField(max_length=10) 20 | program = models.CharField(max_length=10) 21 | 22 | def __str__(self): 23 | return "[name={} ; discipline={} ; program={}]".format(self.name, self.discipline, self.program) 24 | 25 | @restifyModel 26 | class System(models.Model): 27 | name = models.CharField(max_length=75) 28 | location = models.CharField(max_length=20) 29 | student = models.OneToOneField(Student, models.CASCADE, null=True) 30 | 31 | def __str__(self): 32 | return "[name={} ; location={}]".format(self.name, self.location) 33 | 34 | ``` 35 | 36 | In the above models, there is a one-to-one relationship from System1 to Student1. Let us create a student object: 37 | 38 | $ http POST http://127.0.0.1:8000/rest/v1/lab/student/ name=John\ Doe discipline=CS program=MS 39 | HTTP/1.1 201 Created 40 | Allow: GET, POST, HEAD, OPTIONS 41 | Content-Length: 73 42 | Content-Type: application/json 43 | Cross-Origin-Opener-Policy: same-origin 44 | Date: Fri, 30 Sep 2022 09:47:29 GMT 45 | Referrer-Policy: same-origin 46 | Server: WSGIServer/0.2 CPython/3.10.6 47 | Vary: Accept, Cookie 48 | X-Content-Type-Options: nosniff 49 | X-Frame-Options: DENY 50 | 51 | { 52 | "discipline": "CS", 53 | "id": 1, 54 | "name": "John Doe", 55 | "program": "MS", 56 | "system": null 57 | } 58 | 59 | Here, the student object has got an attribute, "system" which is null as this student object is not yet mapped with any system object. This attribute is a field of type `OneToOneRel` and not of type `OneToOneField`. Thus, this is a read-only field. This field will get some value when this object is mapped with a System object. Let us create a system object: 60 | 61 | $ http POST http://127.0.0.1:8000/rest/v1/lab/system/ name=Dell\ Vostro\ 1558 location=AB1-102 62 | HTTP/1.1 201 Created 63 | Allow: GET, POST, HEAD, OPTIONS 64 | Content-Length: 70 65 | Content-Type: application/json 66 | Cross-Origin-Opener-Policy: same-origin 67 | Date: Fri, 30 Sep 2022 11:27:53 GMT 68 | Referrer-Policy: same-origin 69 | Server: WSGIServer/0.2 CPython/3.10.6 70 | Vary: Accept, Cookie 71 | X-Content-Type-Options: nosniff 72 | X-Frame-Options: DENY 73 | 74 | { 75 | "id": 1, 76 | "location": "AB1-102", 77 | "name": "Dell Vostro 1558", 78 | "student": null 79 | } 80 | 81 | !!! Note 82 | 83 | In the model, the `null` flag for the `OneToOneField` is set as `True`. Not allowing null values may have restrictions on updating relations. 84 | 85 | The "student" attribute here is `OneToOneField` and is read-write. Now, this object can be used to map "Student" and "System" object as shown below: 86 | 87 | $ http PATCH http://127.0.0.1:8000/rest/v1/lab/system/1/ student=1 88 | HTTP/1.1 200 OK 89 | Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS 90 | Content-Length: 67 91 | Content-Type: application/json 92 | Cross-Origin-Opener-Policy: same-origin 93 | Date: Fri, 30 Sep 2022 20:11:52 GMT 94 | Referrer-Policy: same-origin 95 | Server: WSGIServer/0.2 CPython/3.10.6 96 | Vary: Accept, Cookie 97 | X-Content-Type-Options: nosniff 98 | X-Frame-Options: DENY 99 | 100 | { 101 | "id": 1, 102 | "location": "AB1-102", 103 | "name": "Dell Vostro 1558", 104 | "student": 1 105 | } 106 | 107 | $ http PATCH http://127.0.0.1:8000/rest/v1/lab/student/1/ 108 | HTTP/1.1 200 OK 109 | Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS 110 | Content-Length: 70 111 | Content-Type: application/json 112 | Cross-Origin-Opener-Policy: same-origin 113 | Date: Fri, 30 Sep 2022 20:12:47 GMT 114 | Referrer-Policy: same-origin 115 | Server: WSGIServer/0.2 CPython/3.10.6 116 | Vary: Accept, Cookie 117 | X-Content-Type-Options: nosniff 118 | X-Frame-Options: DENY 119 | 120 | { 121 | "discipline": "CS", 122 | "id": 1, 123 | "name": "John Doe", 124 | "program": "MS", 125 | "system": 1 126 | } 127 | 128 | Notice that the student object now shows primary key of related "System" object. 129 | 130 | Many-to-one relationships 131 | ------------------------ 132 | This section shows the behaviour for many to one relationship: 133 | 134 | Consider the following model: 135 | 136 | ```py title="models.py" linenums="1" 137 | from django.db import models 138 | from django.utils import timezone 139 | from to_rest.decorators import restifyModel 140 | 141 | # Create your models here. 142 | @restifyModel 143 | class Question(models.Model): 144 | question_text = models.CharField(max_length=200) 145 | 146 | def pub_date_default(): 147 | return timezone.now() 148 | 149 | pub_date = models.DateTimeField('date published', default=pub_date_default) 150 | 151 | @restifyModel 152 | class Choice(models.Model): 153 | question = models.ForeignKey(Question, on_delete=models.CASCADE, related_name='choices') 154 | choice_text = models.CharField(max_length=200) 155 | votes = models.IntegerField(default=0) 156 | ``` 157 | 158 | In the above models there is a many-to-one relationship from `Choice` to `Question`. This will create a read only field called `choices` which will have link for the related `choices` object. For example: 159 | 160 | $ http GET http://127.0.0.1:8000/rest/v1/polls/question/ 161 | HTTP/1.1 200 OK 162 | Allow: GET, POST, HEAD, OPTIONS 163 | Content-Length: 136 164 | Content-Type: application/json 165 | Cross-Origin-Opener-Policy: same-origin 166 | Date: Sun, 02 Oct 2022 09:30:42 GMT 167 | Referrer-Policy: same-origin 168 | Server: WSGIServer/0.2 CPython/3.10.6 169 | Vary: Accept, Cookie 170 | X-Content-Type-Options: nosniff 171 | X-Frame-Options: DENY 172 | 173 | [ 174 | { 175 | "choices": "/rest/v1/polls/question/1/choices/", 176 | "id": 1, 177 | "pub_date": "2022-10-02T09:23:28.297936Z", 178 | "question_text": "How is the traffic?" 179 | } 180 | ] 181 | 182 | 183 | On fetching the link for choices we get all the related `Choice` object: 184 | 185 | $ http -b GET http://127.0.0.1:8000/rest/v1/polls/question/1/choices/ 186 | [ 187 | { 188 | "choice_text": "Clear for miles...", 189 | "id": 1, 190 | "question": 1, 191 | "votes": 0 192 | }, 193 | { 194 | "choice_text": "Stuck for an hour", 195 | "id": 2, 196 | "question": 1, 197 | "votes": 0 198 | } 199 | ] 200 | 201 | !!! Note 202 | 203 | This url is only for list operations as all the other operations like create, update and delete can be done from the other side of the relationship. 204 | 205 | All the other view set attributes like `permission_classes`, `filter_backends`, ... applies as provided to the decorator in models.py. For example conside following `models` and `view_params`: 206 | 207 | === "view_params.py" 208 | 209 | ``` py linenums="1" 210 | from to_rest import constants 211 | from rest_framework.permissions import DjangoModelPermissions, IsAuthenticated 212 | from to_rest.utils import ViewParams 213 | from rest_framework.authentication import BasicAuthentication 214 | 215 | class DDjangoModelPermissions(DjangoModelPermissions): 216 | perms_map = { 217 | 'GET': ['%(app_label)s.view_%(model_name)s'], 218 | 'POST': ['%(app_label)s.add_%(model_name)s'], 219 | 'PUT': ['%(app_label)s.update_%(model_name)s'], 220 | 'PATCH': ['%(app_label)s.update_%(model_name)s'], 221 | 'DELETE': ['%(app_label)s.delete_%(model_name)s'] 222 | } 223 | 224 | class CustomPermission(ViewParams): 225 | 226 | def getParams(): 227 | temp = dict() 228 | temp[constants.AUTHENTICATION_CLASSES] = [BasicAuthentication] 229 | temp[constants.PERMISSION_CLASSES] = [IsAuthenticated, DDjangoModelPermissions] 230 | return temp 231 | ``` 232 | 233 | === "models.py" 234 | 235 | ```py linenums="1" 236 | from django.db import models 237 | from django.utils import timezone 238 | from to_rest.decorators import restifyModel 239 | 240 | # Create your models here. 241 | @restifyModel(customViewParams='CustomPermission') 242 | class Question1(models.Model): 243 | question_text = models.CharField(max_length=200) 244 | 245 | def pub_date_default(): 246 | return timezone.now() 247 | 248 | pub_date = models.DateTimeField('date published', default=pub_date_default) 249 | 250 | @restifyModel(customViewParams='CustomPermission') 251 | class Choice1(models.Model): 252 | question = models.ForeignKey(Question1, on_delete=models.CASCADE, related_name='choices') 253 | choice_text = models.CharField(max_length=200) 254 | votes = models.IntegerField(default=0) 255 | ``` 256 | 257 | Now, if a user has all permissions for `Question1` and not for `Choice1` then the user can access the url(s) for `Question1` but not the nested url for related `Choice1` objects: 258 | 259 | $ http -b -a testy:test@1234 GET http://127.0.0.1:8000/rest/v1/polls/question1/ 260 | [ 261 | { 262 | "choices": "/rest/v1/polls/question1/1/choices/", 263 | "id": 1, 264 | "pub_date": "2022-10-02T09:23:47.805702Z", 265 | "question_text": "How is the traffic?" 266 | } 267 | ] 268 | 269 | $ http -b -a testy:test@1234 GET http://127.0.0.1:8000/rest/v1/polls/question1/1/choices/ 270 | { 271 | "detail": "You do not have permission to perform this action." 272 | } 273 | 274 | Many-to-many relationships 275 | ------------------------ 276 | In case of many-to-many relationships, ``through`` objects for the related objects are returned from the nested url instead of the related objects as ``through`` objects have better information about the relationship. 277 | 278 | Nested url for many-to-many relationships support following operations: 279 | 280 | * list (GET) 281 | * create (POST) 282 | * retrieve (GET) 283 | * update (PUT) 284 | * partial_update (PATCH) 285 | * delete (DELETE) 286 | 287 | All the other view set attributes like `permission_classes`, `filter_backends`, ... applies as provided to the decorator in models.py for the through model. 288 | 289 | ### Example 290 | 291 | Consider the below model: 292 | 293 | ```py title="models.py" linenums="1" 294 | from django.db import models 295 | from to_rest.decorators import restifyModel 296 | 297 | # Create your models here. 298 | @restifyModel 299 | class Student(models.Model): 300 | name = models.CharField(max_length=75) 301 | friends = models.ManyToManyField("self") 302 | 303 | def __str__(self): 304 | return self.name 305 | @restifyModel 306 | class Course(models.Model): 307 | name = models.CharField(max_length=75) 308 | student = models.ManyToManyField(Student) 309 | 310 | def __str__(self): 311 | return self.name 312 | ``` 313 | 314 | Consider the following as available data: 315 | 316 | $ http -b --unsorted GET http://127.0.0.1:8000/rest/v1/edu/student/ 317 | [ 318 | { 319 | "id": 1, 320 | "name": "John Doe", 321 | "course_set": "/rest/v1/edu/student/1/course_set/", 322 | "friends": "/rest/v1/edu/student/1/friends/" 323 | }, 324 | { 325 | "id": 2, 326 | "name": "Eva Doe", 327 | "course_set": "/rest/v1/edu/student/2/course_set/", 328 | "friends": "/rest/v1/edu/student/2/friends/" 329 | }, 330 | { 331 | "id": 3, 332 | "name": "Alice Doe", 333 | "course_set": "/rest/v1/edu/student/3/course_set/", 334 | "friends": "/rest/v1/edu/student/3/friends/" 335 | } 336 | ] 337 | 338 | $ http -b --unsorted GET http://127.0.0.1:8000/rest/v1/edu/course/ 339 | [ 340 | { 341 | "id": 1, 342 | "name": "CS601", 343 | "student": "/rest/v1/edu/course/1/student/" 344 | }, 345 | { 346 | "id": 2, 347 | "name": "CS602", 348 | "student": "/rest/v1/edu/course/2/student/" 349 | }, 350 | { 351 | "id": 3, 352 | "name": "CS603", 353 | "student": "/rest/v1/edu/course/3/student/" 354 | } 355 | ] 356 | 357 | Now, to relate `John Doe` with `CS601` and `CS602`, following can be done: 358 | 359 | $ http -b --unsorted POST http://127.0.0.1:8000/rest/v1/edu/student/1/course_set/ course=1 360 | { 361 | "id": 1, 362 | "course": 1, 363 | "student": 1 364 | } 365 | $ http -b --unsorted POST http://127.0.0.1:8000/rest/v1/edu/student/1/course_set/ course=2 366 | { 367 | "id": 2, 368 | "course": 2, 369 | "student": 1 370 | } 371 | 372 | In the above example, data for `through` objects are provided. And the relationship between `John Doe` and related courses can be listed as follows: 373 | 374 | $ http -b --unsorted GET http://127.0.0.1:8000/rest/v1/edu/student/1/course_set/ 375 | [ 376 | { 377 | "id": 1, 378 | "course": 1, 379 | "student": 1 380 | }, 381 | { 382 | "id": 2, 383 | "course": 2, 384 | "student": 1 385 | } 386 | ] 387 | 388 | Customize nested URL behaviour 389 | ------------------------------ 390 | 391 | To customize the default behaviour of the nested url(s), custom definitions for following method (decorated with the decorator `rest_framework.decorators.action`) needs to be passed as custom view parameters: 392 | 393 | * For Many-to-one: 394 | - Name of function: `to_rest.constants.ONE_TO_MANY_LIST_ACTION + relatedName` 395 | - signature: `(self,request,pk=None, *args, **kwargs)` 396 | * For Many-to-many list view: 397 | - Name of function: `to_rest.constants.MANY_TO_MANY_LIST_ACTION + relatedName` 398 | - signature: `(self,request,pk=None, *args,**kwargs)` 399 | * For Many-to-one detail view: 400 | - Name of function: `to_rest.constants.MANY_TO_MANY_DETAIL_ACTION + relatedName` 401 | - signature: `self,request,childPk,pk=None,*args,**kwargs` 402 | - NOTE: Here `pk` for primary key of the parent object and `childPk` is primar key of the related or nested object 403 | 404 | ### Example 405 | 406 | To customize the behaviour for the example shown for Many-to-many, following can be done: 407 | 408 | === "view_params.py" 409 | 410 | ``` py linenums="1" 411 | from to_rest import constants 412 | from to_rest.utils import ViewParams 413 | from rest_framework.response import Response 414 | from rest_framework.decorators import action 415 | from to_rest import constants 416 | 417 | class CustomAction(ViewParams): 418 | 419 | def getParams(): 420 | def customaction(self,request,pk=None, *args,**kwargs): 421 | if self.request.method == "GET": 422 | return Response({'msg':"Custom method working (GET)"}) 423 | elif self.request.method == 'POST': 424 | return Response({'msg':"custom method working (POST)"}) 425 | else: 426 | return Response(status=status.HTTP_400_BAD_REQUEST) 427 | customaction.__name__ = constants.MANY_TO_MANY_LIST_ACTION + 'course_set' 428 | customaction = action(detail=True, methods=['get', 'post'], url_path='course_set', url_name="student-course_set-list")(customaction) 429 | temp = dict() 430 | temp[constants.MANY_TO_MANY_LIST_ACTION + 'course_set'] = customaction 431 | return temp 432 | ``` 433 | 434 | === "models.py" 435 | 436 | ```py linenums="1" 437 | from django.db import models 438 | from to_rest.decorators import restifyModel 439 | 440 | # Create your models here. 441 | @restifyModel(customViewParams='CustomAction') 442 | class Student(models.Model): 443 | name = models.CharField(max_length=75) 444 | friends = models.ManyToManyField("self") 445 | 446 | def __str__(self): 447 | return self.name 448 | @restifyModel 449 | class Course(models.Model): 450 | name = models.CharField(max_length=75) 451 | student = models.ManyToManyField(Student) 452 | 453 | def __str__(self): 454 | return self.name 455 | ``` 456 | 457 | !!! Note 458 | 459 | * In the above example, `relatedName` is `course_set`. 460 | * While decorating the method (here at line 18): 461 | - `url_path` must be in the form (all lower case): 462 | - `` for list view 463 | - `/(?P.+)` for detail view 464 | - `url_name` must be in the form (all lower case): `--` 465 | - View type can be `list` for list view or `detail` for detail view 466 | 467 | After making the avobe change, url(s) will work as follows: 468 | 469 | $ http -b GET http://127.0.0.1:8000/rest/v1/edu/student/1/course_set/ 470 | { 471 | "msg": "Custom method working (GET)" 472 | } 473 | 474 | $ http -b POST http://127.0.0.1:8000/rest/v1/edu/student/1/course_set/ 475 | { 476 | "msg": "custom method working (POST)" 477 | } -------------------------------------------------------------------------------- /docs/full_guide/versioning.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Note on API versioning 3 | --- 4 | 5 | The API versioning works similar to Django Rest Framework except for URLPathVersioning. URLPathVersioning is not supported as of now. -------------------------------------------------------------------------------- /docs/full_guide/viewnames.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Nomenclature of default views 3 | --- 4 | 5 | View names are used for reversing the urls which may be required for difeerent purposes like testing. As the views are created automatically, the view names are created as follows: 6 | 7 | * For any model the view-name is created as follows: 8 | 9 | * For detailed view: `-` with all `.` replaced with `_` and all lower cases. For example: if there is an app with name `edu` and a model named `Student` then the view name would be `edu_student-detail`. 10 | * For list view: `-` with all `.` replaced with `_` and all lower cases. For example: if there is an app with name `edu` and a model named `Student` then the view name would be `edu_student-list`. 11 | 12 | * For any child model (in relationship with other model) a nested url is created and the view name is generated as follows: 13 | 14 | * For detailed view: `---` with all `.` replaced with `_` and all lower cases. For example: if there is an app with name `edu` and a model named `Student` with many to many relationship with another model named `Course` then the view name would be `edu_student-student-course_set-detail`. 15 | * For detailed view: `---` with all `.` replaced with `_` and all lower cases. For example: if there is an app with name `edu` and a model named `Student` with many to many relationship with another model named `Course` then the view name would be `edu_student-student-course_set-list`. 16 | 17 | -------------------------------------------------------------------------------- /docs/img/large_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anp-scp/django-to-rest/03f17f276e643607640615305a0b476ed9ba2bf6/docs/img/large_logo.png -------------------------------------------------------------------------------- /docs/img/large_logo_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anp-scp/django-to-rest/03f17f276e643607640615305a0b476ed9ba2bf6/docs/img/large_logo_black.png -------------------------------------------------------------------------------- /docs/img/large_logo_blackq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anp-scp/django-to-rest/03f17f276e643607640615305a0b476ed9ba2bf6/docs/img/large_logo_blackq.png -------------------------------------------------------------------------------- /docs/img/small_logo_grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anp-scp/django-to-rest/03f17f276e643607640615305a0b476ed9ba2bf6/docs/img/small_logo_grey.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Overview 3 | hide: 4 | - navigation 5 | --- 6 | ![Django To Rest](img/large_logo_black.png){ width="300"; align=left } 7 | 8 | Django To Rest is small tool that helps to expose REST api(s) for 9 | django models with minimum effort. This utility is for one who uses `Django REST Framework` for writing REST APIs. The tool enables you to focus only on the code needed explicitly. The tool handles all boilerplate for writing CRUD APIs. Some of the features are: 10 | 11 | * Just add a decorator at top of a model and REST api(s) are created. That's the work!!! 12 | * Options like filtering and ordering are avilable by default for model fields 13 | * Easy customisations via the decorator itself 14 | * *Summary:* Less time??? Just install the tool and use the decorator. Done!!! 15 | 16 | * * * 17 | * *Documentation:* [https://anp-scp.github.io/django-to-rest](https://anp-scp.github.io/django-to-rest) 18 | * *Source Code:* [https://github.com/anp-scp/django-to-rest](https://github.com/anp-scp/django-to-rest) 19 | * * * 20 | 21 | ## **Requirements** 22 | 23 | Django to Rest need following requirements : 24 | 25 | * Python 3.8+ 26 | * Django 4.0.5+ 27 | * djangorestframework 3.13.1+ 28 | * django-filter 22.1 29 | 30 | * * * 31 | 32 | ## **Installation** 33 | 34 | django-to-rest is published as a package and can be installed using pip. Install with (consider creating a virtual environment): 35 | 36 | python3 -m pip install django-to-rest 37 | 38 | ## **Example** 39 | 40 | Let us have a look on an example of how the tool can be used to expose REST API. 41 | 42 | Let us assume that the following are the requirements: 43 | 44 | 1. A polls app having certain questions and each question have some choices. 45 | 2. All CRUD URLs for question and choice objects. 46 | 3. We need an URL which simply increments a counter 47 | 48 | Make sure that `djangorestframework` is installed and included in `INSTALLED_APPS ` in settings.py as shown below: 49 | ```py title="settings.py" linenums="1" 50 | ... 51 | INSTALLED_APPS = [ 52 | 'rest_framework', 53 | ... 54 | ] 55 | ... 56 | ``` 57 | Now create two models as shown below: 58 | ```py title="models.py" linenums="1" 59 | from django.db import models 60 | from django.utils import timezone 61 | from django.contrib import admin 62 | from to_rest.decorators import restifyModel # (1) 63 | 64 | # Create your models here. 65 | @restifyModel # (2) 66 | class Question(models.Model): 67 | question_text = models.CharField(max_length=200) 68 | pub_date = models.DateTimeField('date published') 69 | 70 | def __str__(self): 71 | return self.question_text 72 | 73 | 74 | @restifyModel # (3) 75 | class Choice(models.Model): 76 | question = models.ForeignKey(Question, on_delete=models.CASCADE,related_name='choices') 77 | choice_text = models.CharField(max_length=200) 78 | votes = models.IntegerField(default=0) 79 | 80 | def __str__(self): 81 | return self.choice_text 82 | ``` 83 | 84 | 1. Import the decorator from the library 85 | 2. Note the way decorator is used 86 | 3. Note the way decorator is used 87 | 88 | Note the use of the decorators. We just need to use the decorator and all the views and serializers would be created during startup. But apart from that, we need one more line to add in `urls.py` of the project (not any app) as shown below: 89 | ```py title="urls.py" linenums="1" 90 | from django.urls import path 91 | from to_rest import utils 92 | from django.http import JsonResponse 93 | 94 | urlpatterns = [ 95 | ... 96 | ] 97 | urlpatterns.extend(utils.restifyApp('rest/v1')) # (1) 98 | ``` 99 | 100 | 1. call this method to add the urls in url patterns. Here the parameter 'rest/v1' is the prefix to be used in the url. 101 | 102 | That's all. All the above configurations will create the CRUD APIs for the classes that we marked using the decorator. For the 3rd requirement we can simply write a method the way we write in `Django` or `Django REST Framework`. We add the following lines in `urls.py`: 103 | 104 | ```py 105 | count = 0 106 | 107 | def counter(request) : 108 | global count 109 | if request.method == 'GET': 110 | count += 1 111 | return JsonResponse({'count': count}) 112 | urlpatterns.append(path('count/', counter)) 113 | ``` 114 | 115 | Now start the server. We add some data and check the dev url `http://127.0.0.1:8000/`. Below is an example with httpie: 116 | 117 | $ http -b --unsorted http://127.0.0.1:8000/ 118 | { 119 | "rest/v1/polls/question": "http://127.0.0.1:8000/rest/v1/polls/question", 120 | "rest/v1/polls/choice": "http://127.0.0.1:8000/rest/v1/polls/choice" 121 | } 122 | 123 | $ http -b --unsorted http://127.0.0.1:8000/rest/v1/polls/question 124 | [ 125 | { 126 | "id": 1, 127 | "question_text": "How is the traffic?", 128 | "pub_date": "2022-07-08T10:02:16.290713Z", 129 | "choices": "/rest/v1/polls/question/1/choices" 130 | }, 131 | { 132 | "id": 2, 133 | "question_text": "What's up?", 134 | "pub_date": "2022-07-08T10:03:15.816192Z", 135 | "choices": "/rest/v1/polls/question/2/choices" 136 | } 137 | ] 138 | 139 | $ http -b --unsorted http://127.0.0.1:8000/rest/v1/polls/question/1/choices 140 | [ 141 | { 142 | "id": 1, 143 | "choice_text": "Highly Conjested", 144 | "votes": 0, 145 | "question": 1 146 | }, 147 | { 148 | "id": 2, 149 | "choice_text": "Clear for miles", 150 | "votes": 0, 151 | "question": 1 152 | } 153 | ] 154 | 155 | $ http -b --unsorted http://127.0.0.1:8000/count/ 156 | { 157 | "count": 1 158 | } 159 | 160 | $ http -b --unsorted http://127.0.0.1:8000/count/ 161 | { 162 | "count": 2 163 | } 164 | 165 | $ http -b --unsorted http://127.0.0.1:8000/count/ 166 | { 167 | "count": 3 168 | } 169 | 170 | Here, we wrote extra code only for the `/count/` URL and other CRUD URLs where created by the utility. 171 | 172 | ## **Quickstart** 173 | 174 | The [quick start guide](quickstart.md) is a short tutorial which is the fastest way to get everything setup and get an overview of the tool. 175 | 176 | ## **Contributing** 177 | 178 | Check the [contribution guidelines](community/contributing_to_django_to_rest.md) to know about how to contribute to the project. -------------------------------------------------------------------------------- /docs/quickstart.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Quickstart Guide 3 | hide: 4 | - navigation 5 | --- 6 | 7 | ## **Setup** 8 | Let us start fresh. Ensure that Python 3.8.x is already installed. It is always better to use 9 | virtual environment to isolate your work with other stuffs. Let us create and activate a virtual env inside the directory `quickstart`: 10 | 11 | # Create a virtual environment 12 | $ pwd 13 | /.../quickstart 14 | $ python3 -m venv qs 15 | $ source qs/bin/activate 16 | 17 | ### Installation 18 | 19 | python3 -m pip install django-to-rest 20 | 21 | ### Creation of a django project and app 22 | 23 | # Create a django project 24 | $ pwd 25 | /.../quickstart 26 | $ (qs) django-admin startproject mysite 27 | $ (qs) cd mysite 28 | # create an app in the project 29 | $ pwd 30 | /.../quickstart/mysite 31 | $ (qs) python3 manage.py startapp polls 32 | $ (qs) python3 manage.py migrate 33 | 34 | ## **Creation of models** 35 | 36 | Now, let us create some models for our polls app. We will create one model named `Question` 37 | and another named `Choice` (This is quite similar to the tutoraial available at django documentation). Here, There will be one-to-many relationship from `Question` to `Choice`. 38 | 39 | ```py title="/.../quickstart/mysite/polls/models.py" linenums="1" 40 | from django.db import models 41 | from django.utils import timezone 42 | 43 | class Question(models.Model): 44 | question_text = models.CharField(max_length=200) 45 | pub_date = models.DateTimeField('date published') 46 | 47 | def __str__(self): 48 | return self.question_text 49 | 50 | class Choice(models.Model): 51 | question = models.ForeignKey(Question, on_delete=models.CASCADE,related_name='choices') 52 | choice_text = models.CharField(max_length=200) 53 | votes = models.IntegerField(default=0) 54 | 55 | def __str__(self): 56 | return self.choice_text 57 | 58 | 59 | ``` 60 | ### Activating the app 61 | Now, add the polls app to `INSTALLED_APPS` in `settings.py`. Also add `rest_framework` to it as 62 | `djang-to-rest` uses `djangorestframework` internally: 63 | 64 | ```py title="/.../quickstart/mysite/mysite/settings.py" linenums="1" 65 | ... 66 | INSTALLED_APPS = [ 67 | 'polls', 68 | 'rest_framework', 69 | ... 70 | ] 71 | ... 72 | ``` 73 | After adding the app to `INSTALLED_APPS`, perform migrations for creating required tables in DB. 74 | 75 | $ pwd 76 | /.../quickstart/mysite 77 | $ (qs) python3 manage.py makemigrations polls 78 | $ (qs) python3 manage.py migrate 79 | 80 | ### Add some data 81 | 82 | Since, our app and DB is setup, let us create some dummy data to play with them via REST api. 83 | 84 | $ pwd 85 | /.../quickstart/mysite 86 | $ python3 manage.py shell 87 | Python 3.8.10 (default, Mar 15 2022, 12:22:08) 88 | [GCC 9.4.0] on linux 89 | Type "help", "copyright", "credits" or "license" for more information. 90 | (InteractiveConsole) 91 | >>> from polls.models import Question, Choice 92 | >>> from django.utils import timezone 93 | >>> q = Question(question_text="How is the traffic?", pub_date=timezone.now()) 94 | >>> q.save() 95 | >>> q1 = Question(question_text="What's up?", pub_date=timezone.now()) 96 | >>> q1.save() 97 | >>> q.choices.create(choice_text="Conjested", votes=0) 98 | 99 | >>> q.choices.create(choice_text="Clear for miles", votes=0) 100 | 101 | >>> q1.choices.create(choice_text="Fine", votes=0) 102 | 103 | >>> q1.choices.create(choice_text="Nohing New", votes=0) 104 | 105 | 106 | ## **Use django-to-rest** 107 | 108 | Now as we have some data to play with, let us use `django-to-rest` to create our api. To do that, 109 | we need to mark the models for which the REST apis need to be created. Let us mark our models for 110 | restification!!! (by restification, we mean to create REST api(s) for models): 111 | 112 | ```py title="/.../quickstart/mysite/polls/models.py" linenums="1" 113 | from django.db import models 114 | from django.utils import timezone 115 | from django.contrib import admin 116 | from to_rest.decorators import restifyModel # (1) 117 | 118 | # Create your models here. 119 | @restifyModel # (2) 120 | class Question(models.Model): 121 | question_text = models.CharField(max_length=200) 122 | pub_date = models.DateTimeField('date published') 123 | 124 | def __str__(self): 125 | return self.question_text 126 | 127 | 128 | @restifyModel # (3) 129 | class Choice(models.Model): 130 | question = models.ForeignKey(Question, on_delete=models.CASCADE,related_name='choices') 131 | choice_text = models.CharField(max_length=200) 132 | votes = models.IntegerField(default=0) 133 | 134 | def __str__(self): 135 | return self.choice_text 136 | ``` 137 | 138 | 1. Import the decorator from the library 139 | 2. Note the way decorator is used 140 | 3. Note the way decorator is used 141 | 142 | 143 | Now, go to projects urls.py and use the following method to get urls for REST apis: 144 | 145 | ```py title="/.../quickstart/mysite/mysite/urls.py" linenums="1" 146 | from django.contrib import admin 147 | from django.urls import path 148 | from to_rest import utils # (1) 149 | 150 | urlpatterns = [ 151 | path('admin/', admin.site.urls), 152 | ] 153 | urlpatterns.extend(utils.restifyApp('rest/v1')) # (2) 154 | ``` 155 | 156 | 1. Import the utils from to_rest 157 | 2. Use the method to get the urls. 'rest/v1' is the prefix for the urls for REST apis 158 | 159 | 160 | Now go to project's directory and start the server. 161 | 162 | $ pwd 163 | /.../quickstart/mysite 164 | $ python3 manage.py runserver 165 | 166 | ## **Playing with REST apis** 167 | 168 | Now open a new terminal check our apis using httpie: 169 | 170 | $ http --json http://127.0.0.1:8000/ 171 | HTTP/1.1 200 OK 172 | Allow: GET, HEAD, OPTIONS 173 | Content-Length: 143 174 | Content-Type: application/json 175 | Cross-Origin-Opener-Policy: same-origin 176 | Date: Fri, 08 Jul 2022 11:02:17 GMT 177 | Referrer-Policy: same-origin 178 | Server: WSGIServer/0.2 CPython/3.8.10 179 | Vary: Accept, Cookie 180 | X-Content-Type-Options: nosniff 181 | X-Frame-Options: DENY 182 | 183 | { 184 | "rest/v1/polls/choice": "http://127.0.0.1:8000/rest/v1/polls/choice/", 185 | "rest/v1/polls/question": "http://127.0.0.1:8000/rest/v1/polls/question/" 186 | } 187 | 188 | ### List objects 189 | 190 | $ http --json http://127.0.0.1:8000/rest/v1/polls/question/ 191 | HTTP/1.1 200 OK 192 | Allow: GET, POST, HEAD, OPTIONS 193 | Content-Length: 262 194 | Content-Type: application/json 195 | Cross-Origin-Opener-Policy: same-origin 196 | Date: Fri, 08 Jul 2022 11:08:56 GMT 197 | Referrer-Policy: same-origin 198 | Server: WSGIServer/0.2 CPython/3.8.10 199 | Vary: Accept, Cookie 200 | X-Content-Type-Options: nosniff 201 | X-Frame-Options: DENY 202 | 203 | [ 204 | { 205 | "choices": "/rest/v1/polls/question/1/choices/", 206 | "id": 1, 207 | "pub_date": "2022-07-08T10:02:16.290713Z", 208 | "question_text": "How is the traffic?" 209 | }, 210 | { 211 | "choices": "/rest/v1/polls/question/2/choices/", 212 | "id": 2, 213 | "pub_date": "2022-07-08T10:03:15.816192Z", 214 | "question_text": "What's up?" 215 | } 216 | ] 217 | 218 | ### Retreive object 219 | 220 | $ http --json http://127.0.0.1:8000/rest/v1/polls/question/1/ 221 | HTTP/1.1 200 OK 222 | Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS 223 | Content-Length: 134 224 | Content-Type: application/json 225 | Cross-Origin-Opener-Policy: same-origin 226 | Date: Fri, 08 Jul 2022 11:11:49 GMT 227 | Referrer-Policy: same-origin 228 | Server: WSGIServer/0.2 CPython/3.8.10 229 | Vary: Accept, Cookie 230 | X-Content-Type-Options: nosniff 231 | X-Frame-Options: DENY 232 | 233 | { 234 | "choices": "/rest/v1/polls/question/1/choices/", 235 | "id": 1, 236 | "pub_date": "2022-07-08T10:02:16.290713Z", 237 | "question_text": "How is the traffic?" 238 | } 239 | 240 | ### List one-to-many objects 241 | 242 | $ http --json http://127.0.0.1:8000/rest/v1/polls/question/1/choices/ 243 | HTTP/1.1 200 OK 244 | Allow: GET, HEAD, OPTIONS 245 | Content-Length: 123 246 | Content-Type: application/json 247 | Cross-Origin-Opener-Policy: same-origin 248 | Date: Fri, 08 Jul 2022 11:32:31 GMT 249 | Referrer-Policy: same-origin 250 | Server: WSGIServer/0.2 CPython/3.8.10 251 | Vary: Accept, Cookie 252 | X-Content-Type-Options: nosniff 253 | X-Frame-Options: DENY 254 | 255 | [ 256 | { 257 | "choice_text": "Conjested", 258 | "id": 1, 259 | "question": 1, 260 | "votes": 0 261 | }, 262 | { 263 | "choice_text": "Clear for miles", 264 | "id": 2, 265 | "question": 1, 266 | "votes": 0 267 | } 268 | ] 269 | 270 | ### Filter using model attributes 271 | 272 | $ http --json http://127.0.0.1:8000/rest/v1/polls/question/1/choices/?choice_text=Conjested 273 | HTTP/1.1 200 OK 274 | Allow: GET, HEAD, OPTIONS 275 | Content-Length: 59 276 | Content-Type: application/json 277 | Cross-Origin-Opener-Policy: same-origin 278 | Date: Fri, 08 Jul 2022 11:34:22 GMT 279 | Referrer-Policy: same-origin 280 | Server: WSGIServer/0.2 CPython/3.8.10 281 | Vary: Accept, Cookie 282 | X-Content-Type-Options: nosniff 283 | X-Frame-Options: DENY 284 | 285 | [ 286 | { 287 | "choice_text": "Conjested", 288 | "id": 1, 289 | "question": 1, 290 | "votes": 0 291 | } 292 | ] 293 | 294 | ### Search using model attributes 295 | 296 | $ http --json http://127.0.0.1:8000/rest/v1/polls/question/1/choices/?search=miles 297 | HTTP/1.1 200 OK 298 | Allow: GET, HEAD, OPTIONS 299 | Content-Length: 65 300 | Content-Type: application/json 301 | Cross-Origin-Opener-Policy: same-origin 302 | Date: Fri, 08 Jul 2022 11:36:36 GMT 303 | Referrer-Policy: same-origin 304 | Server: WSGIServer/0.2 CPython/3.8.10 305 | Vary: Accept, Cookie 306 | X-Content-Type-Options: nosniff 307 | X-Frame-Options: DENY 308 | 309 | [ 310 | { 311 | "choice_text": "Clear for miles", 312 | "id": 2, 313 | "question": 1, 314 | "votes": 0 315 | } 316 | ] 317 | 318 | ### Ordering using model attributes 319 | 320 | $ http --json http://127.0.0.1:8000/rest/v1/polls/choice/?ordering=-choice_text 321 | HTTP/1.1 200 OK 322 | Allow: GET, POST, HEAD, OPTIONS 323 | Content-Length: 235 324 | Content-Type: application/json 325 | Cross-Origin-Opener-Policy: same-origin 326 | Date: Fri, 08 Jul 2022 11:52:26 GMT 327 | Referrer-Policy: same-origin 328 | Server: WSGIServer/0.2 CPython/3.8.10 329 | Vary: Accept, Cookie 330 | X-Content-Type-Options: nosniff 331 | X-Frame-Options: DENY 332 | 333 | [ 334 | { 335 | "choice_text": "Nohing New", 336 | "id": 4, 337 | "question": 2, 338 | "votes": 0 339 | }, 340 | { 341 | "choice_text": "Fine", 342 | "id": 3, 343 | "question": 2, 344 | "votes": 0 345 | }, 346 | { 347 | "choice_text": "Conjested", 348 | "id": 1, 349 | "question": 1, 350 | "votes": 0 351 | }, 352 | { 353 | "choice_text": "Clear for miles", 354 | "id": 2, 355 | "question": 1, 356 | "votes": 0 357 | } 358 | ] 359 | 360 | 361 | ### Partially update (PATCH) 362 | 363 | !!! Note 364 | 365 | Here, httpie is used for examples. Hence, JSON like body is not used for PUT, PATCH, POST requests for body. Instead, httpie style is used. Other clients can also be used if any difficulty is faced. 366 | * * * 367 | $ http PATCH http://127.0.0.1:8000/rest/v1/polls/choice/1/ choice_text=Highly\ Conjested 368 | HTTP/1.1 200 OK 369 | Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS 370 | Content-Length: 64 371 | Content-Type: application/json 372 | Cross-Origin-Opener-Policy: same-origin 373 | Date: Fri, 08 Jul 2022 15:06:21 GMT 374 | Referrer-Policy: same-origin 375 | Server: WSGIServer/0.2 CPython/3.8.10 376 | Vary: Accept, Cookie 377 | X-Content-Type-Options: nosniff 378 | X-Frame-Options: DENY 379 | 380 | { 381 | "choice_text": "Highly Conjested", 382 | "id": 1, 383 | "question": 1, 384 | "votes": 0 385 | } 386 | 387 | ### Create (POST) 388 | 389 | $ http POST http://127.0.0.1:8000/rest/v1/polls/choice/ choice_text=Doing\ bad question=2 votes=0 390 | HTTP/1.1 201 Created 391 | Allow: GET, POST, HEAD, OPTIONS 392 | Content-Length: 57 393 | Content-Type: application/json 394 | Cross-Origin-Opener-Policy: same-origin 395 | Date: Fri, 08 Jul 2022 15:19:40 GMT 396 | Referrer-Policy: same-origin 397 | Server: WSGIServer/0.2 CPython/3.8.10 398 | Vary: Accept, Cookie 399 | X-Content-Type-Options: nosniff 400 | X-Frame-Options: DENY 401 | 402 | { 403 | "choice_text": "Doing bad", 404 | "id": 5, 405 | "question": 2, 406 | "votes": 0 407 | } 408 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Django To Rest Docs 2 | dev_addr: 127.0.0.1:8080 3 | site_url: https://anp-scp.github.io/django-to-rest 4 | repo_url: https://github.com/anp-scp/django-to-rest 5 | 6 | nav: 7 | - index.md 8 | - quickstart.md 9 | - 'Full Guide': 10 | - full_guide/marking_model_for_REST.md 11 | - full_guide/adding_custom_serializer.md 12 | - full_guide/adding_auth.md 13 | - full_guide/adding_permission.md 14 | - full_guide/adding_throttling.md 15 | - full_guide/adding_custom_filtering.md 16 | - full_guide/adding_methods.md 17 | - full_guide/relationships.md 18 | - full_guide/viewnames.md 19 | - full_guide/adding_custom_classes.md 20 | - full_guide/versioning.md 21 | - 'Community': 22 | - community/contributing_to_django_to_rest.md 23 | - community/changelogs.md 24 | 25 | theme: 26 | name: material 27 | custom_dir: docs_overrides 28 | icon: 29 | logo: material/book 30 | favicon: img/small_logo_grey.png 31 | features: 32 | - navigation.tabs 33 | - navigation.top 34 | - header.autohide 35 | - navigation.tabs.sticky 36 | - content.code.annotate 37 | font: 38 | text: Roboto Condensed 39 | code: Roboto Mono 40 | palette: 41 | - scheme: default 42 | primary: blue grey 43 | accent: amber 44 | toggle: 45 | icon: material/brightness-7 46 | name: Switch to dark mode 47 | - scheme: slate 48 | primary: blue grey 49 | accent: amber 50 | toggle: 51 | icon: material/brightness-4 52 | name: Switch to light mode 53 | 54 | 55 | markdown_extensions: 56 | - attr_list 57 | - md_in_html 58 | - admonition 59 | - pymdownx.details 60 | - pymdownx.inlinehilite 61 | - pymdownx.snippets 62 | - pymdownx.superfences 63 | - def_list 64 | - pymdownx.tasklist: 65 | custom_checkbox: true 66 | - pymdownx.highlight: 67 | anchor_linenums: true 68 | - pymdownx.tabbed: 69 | alternate_style: true 70 | - toc: 71 | permalink: true 72 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ['setuptools>=40.8.0', 'wheel'] 3 | build-backend = 'setuptools.build_meta:__legacy__' 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = django-to-rest 3 | version = 1.1.0 4 | description = A library to expose rest api(s) for a django app (quiet quickly) with minimum efforts 5 | long_description = file: README.rst 6 | long_description_content_type = text/x-rst 7 | author = Anupam Sharma 8 | author_email = anupammrg@gmail.com 9 | license = MIT 10 | classifiers = 11 | Environment :: Web Environment 12 | Framework :: Django 13 | Intended Audience :: Developers 14 | Operating System :: OS Independent 15 | Programming Language :: Python 16 | Programming Language :: Python :: 3 17 | Programming Language :: Python :: 3 :: Only 18 | Programming Language :: Python :: 3.8 19 | Programming Language :: Python :: 3.9 20 | Programming Language :: Python :: 3.10 21 | Topic :: Internet :: WWW/HTTP 22 | Topic :: Internet :: WWW/HTTP :: Dynamic Content 23 | 24 | [options] 25 | include_package_data = true 26 | packages = 27 | to_rest 28 | python_requires = >=3.8 29 | install_requires = 30 | Django >=4.0.5,<=4.1.3 31 | djangorestframework >=3.13.1,<=3.14.0 32 | django-filter ==22.1 33 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /tests/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anp-scp/django-to-rest/03f17f276e643607640615305a0b476ed9ba2bf6/tests/db.sqlite3 -------------------------------------------------------------------------------- /tests/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /tests/test_basics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anp-scp/django-to-rest/03f17f276e643607640615305a0b476ed9ba2bf6/tests/test_basics/__init__.py -------------------------------------------------------------------------------- /tests/test_basics/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /tests/test_basics/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TestBasicsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'test_basics' 7 | -------------------------------------------------------------------------------- /tests/test_basics/filterset.py: -------------------------------------------------------------------------------- 1 | import django_filters 2 | from test_basics.models import StudentWithFilterSetClassVSFilterSetField 3 | 4 | class StudentWithFilterSetClassVSFilterSetFieldFilter(django_filters.FilterSet): 5 | class Meta: 6 | model = StudentWithFilterSetClassVSFilterSetField 7 | fields = ['year', 'discipline'] 8 | -------------------------------------------------------------------------------- /tests/test_basics/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.5 on 2022-07-09 18:57 2 | 3 | from django.db import migrations, models 4 | import test_basics.models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Student', 17 | fields=[ 18 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('name', models.CharField(max_length=50)), 20 | ('year', models.IntegerField(validators=[test_basics.models.is_valid_year])), 21 | ], 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /tests/test_basics/migrations/0002_studentwithcustomserializer.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.5 on 2022-07-17 13:55 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('test_basics', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='StudentWithCustomSerializer', 15 | fields=[ 16 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ('name', models.CharField(max_length=50)), 18 | ], 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /tests/test_basics/migrations/0003_studentwithcustomauthandpermission.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.5 on 2022-07-24 06:39 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('test_basics', '0002_studentwithcustomserializer'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='StudentWithCustomAuthAndPermission', 15 | fields=[ 16 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ('name', models.CharField(max_length=50)), 18 | ], 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /tests/test_basics/migrations/0004_studentwithcustomthrottling.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.5 on 2022-07-29 14:56 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('test_basics', '0003_studentwithcustomauthandpermission'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='StudentWithCustomThrottling', 15 | fields=[ 16 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ('name', models.CharField(max_length=50)), 18 | ], 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /tests/test_basics/migrations/0005_studentwithcustomfiltering.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.5 on 2022-07-30 04:46 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('test_basics', '0004_studentwithcustomthrottling'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='StudentWithCustomFiltering', 15 | fields=[ 16 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ('name', models.CharField(max_length=50)), 18 | ('year', models.IntegerField()), 19 | ('discipline', models.CharField(max_length=20)), 20 | ], 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /tests/test_basics/migrations/0006_studentwithfiltersetclassvsfiltersetfield.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.5 on 2022-07-30 19:02 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('test_basics', '0005_studentwithcustomfiltering'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='StudentWithFilterSetClassVSFilterSetField', 15 | fields=[ 16 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ('name', models.CharField(max_length=50)), 18 | ('year', models.IntegerField()), 19 | ('discipline', models.CharField(max_length=20)), 20 | ], 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /tests/test_basics/migrations/0007_studentwithcustommethod.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.5 on 2022-07-31 17:05 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('test_basics', '0006_studentwithfiltersetclassvsfiltersetfield'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='StudentWithCustomMethod', 15 | fields=[ 16 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ('name', models.CharField(max_length=50)), 18 | ('year', models.IntegerField()), 19 | ], 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /tests/test_basics/migrations/0008_studentwithcustomaction.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.5 on 2022-08-01 05:10 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('test_basics', '0007_studentwithcustommethod'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='StudentWithCustomAction', 15 | fields=[ 16 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ('name', models.CharField(max_length=50)), 18 | ('year', models.IntegerField()), 19 | ], 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /tests/test_basics/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anp-scp/django-to-rest/03f17f276e643607640615305a0b476ed9ba2bf6/tests/test_basics/migrations/__init__.py -------------------------------------------------------------------------------- /tests/test_basics/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.core.exceptions import ValidationError 3 | from django.utils.translation import gettext_lazy as _ 4 | from to_rest.decorators import restifyModel 5 | 6 | def is_valid_year(value): 7 | if value not in [1,2,3,4]: 8 | raise ValidationError(_('%(value)s is not correct year'), params={'value': value}) 9 | 10 | @restifyModel 11 | class Student(models.Model): 12 | name = models.CharField(max_length=50) 13 | year = models.IntegerField(validators=[is_valid_year]) 14 | 15 | def __str__(self): 16 | return "[name={}, year={}]".format(self.name, self.year) 17 | 18 | @restifyModel(customViewParams='CustomSerializer') 19 | class StudentWithCustomSerializer(models.Model): 20 | name = models.CharField(max_length=50) 21 | 22 | def __str__(self): 23 | return "[name={}]".format(self.name) 24 | 25 | @restifyModel(customViewParams='CustomAuthAndPermission') 26 | class StudentWithCustomAuthAndPermission(models.Model): 27 | name = models.CharField(max_length=50) 28 | 29 | def __str__(self): 30 | return "[name={}]".format(self.name) 31 | 32 | @restifyModel(customViewParams='CustomThrottling') 33 | class StudentWithCustomThrottling(models.Model): 34 | name = models.CharField(max_length=50) 35 | 36 | def __str__(self): 37 | return "[name={}]".format(self.name) 38 | 39 | @restifyModel(customViewParams='CustomFiltering') 40 | class StudentWithCustomFiltering(models.Model): 41 | name = models.CharField(max_length=50) 42 | year = models.IntegerField() 43 | discipline = models.CharField(max_length=20) 44 | 45 | def __str__(self): 46 | return "[name={}]".format(self.name) 47 | 48 | @restifyModel(customViewParams='CustomFiltering1') 49 | class StudentWithFilterSetClassVSFilterSetField(models.Model): 50 | name = models.CharField(max_length=50) 51 | year = models.IntegerField() 52 | discipline = models.CharField(max_length=20) 53 | 54 | def __str__(self): 55 | return "[name={}]".format(self.name) 56 | 57 | @restifyModel(customViewParams='CustomListMethod') 58 | class StudentWithCustomMethod(models.Model): 59 | name = models.CharField(max_length=50) 60 | year = models.IntegerField() 61 | 62 | def __str__(self): 63 | return "[name={}]".format(self.name) 64 | 65 | @restifyModel(customViewParams='CustomAction') 66 | class StudentWithCustomAction(models.Model): 67 | name = models.CharField(max_length=50) 68 | year = models.IntegerField() 69 | 70 | def __str__(self): 71 | return "[name={}]".format(self.name) -------------------------------------------------------------------------------- /tests/test_basics/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from test_basics.models import StudentWithCustomSerializer 3 | 4 | class StudentWithCustomSerializerSerializer(serializers.Serializer): 5 | 6 | id = serializers.IntegerField(read_only=True) 7 | name = serializers.CharField() 8 | 9 | def create(self, validated_data): 10 | return StudentWithCustomSerializer.objects.create(**validated_data) 11 | 12 | def update(self, instance, validated_data): 13 | instance.name = validated_data.get('name', instance.name) 14 | instance.save() 15 | return instance -------------------------------------------------------------------------------- /tests/test_basics/view_params.py: -------------------------------------------------------------------------------- 1 | from to_rest import constants 2 | from test_basics import serializers 3 | from rest_framework.permissions import IsAuthenticatedOrReadOnly 4 | from rest_framework.authentication import BasicAuthentication 5 | from django_filters.rest_framework import DjangoFilterBackend 6 | from rest_framework.filters import SearchFilter, OrderingFilter 7 | from test_basics import filterset 8 | from to_rest.utils import ViewParams 9 | from test_basics import models 10 | from rest_framework.response import Response 11 | from rest_framework.decorators import action 12 | 13 | class CustomSerializer(ViewParams): 14 | 15 | def getParams(): 16 | temp = dict() 17 | temp[constants.SERIALIZER_CLASS] = serializers.StudentWithCustomSerializerSerializer 18 | return temp 19 | 20 | class CustomAuthAndPermission(ViewParams): 21 | 22 | def getParams(): 23 | temp = dict() 24 | temp[constants.AUTHENTICATION_CLASSES] = [BasicAuthentication] 25 | temp[constants.PERMISSION_CLASSES] = [IsAuthenticatedOrReadOnly] 26 | return temp 27 | 28 | class CustomThrottling(ViewParams): 29 | 30 | def getParams(): 31 | temp = dict() 32 | temp[constants.THROTTLE_SCOPE] = "studentCustomThrottle" 33 | return temp 34 | 35 | class CustomFiltering(ViewParams): 36 | 37 | def getParams(): 38 | temp = dict() 39 | temp[constants.FILTER_BACKENDS] = [DjangoFilterBackend, SearchFilter, OrderingFilter] 40 | temp[constants.FILTERSET_FIELDS] = ['name', 'year', 'discipline'] 41 | temp[constants.SEARCH_FIELDS] = ['name'] 42 | temp[constants.ORDERING_FIELDS] = ['discipline', 'year'] 43 | temp[constants.ORDERING] = ['year'] 44 | return temp 45 | 46 | 47 | class CustomFiltering1(ViewParams): 48 | 49 | def getParams(): 50 | temp = dict() 51 | temp[constants.FILTER_BACKENDS] = [DjangoFilterBackend] 52 | temp[constants.FILTERSET_FIELDS] = ['name'] 53 | temp[constants.FILTERSET_CLASS] = filterset.StudentWithFilterSetClassVSFilterSetFieldFilter 54 | 55 | return temp 56 | 57 | class CustomListMethod(ViewParams): 58 | 59 | def getParams(): 60 | def list(self, request, *args, **kwargs): 61 | objects = models.StudentWithCustomMethod.objects.filter(year=2) 62 | serializer = self.get_serializer(objects, many=True) 63 | return Response(serializer.data) 64 | temp = dict() 65 | temp['list'] = list 66 | return temp 67 | 68 | class CustomAction(ViewParams): 69 | 70 | def getParams(): 71 | def customaction(self, request, pk=None): 72 | obj = models.StudentWithCustomAction.objects.get(pk=pk) 73 | return Response({'msg':"custom action working for " + obj.name}) 74 | customaction = action(detail=True, methods=['get'], url_name='customaction')(customaction) 75 | temp = dict() 76 | temp['customaction'] = customaction 77 | return temp -------------------------------------------------------------------------------- /tests/test_basics_defaults/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anp-scp/django-to-rest/03f17f276e643607640615305a0b476ed9ba2bf6/tests/test_basics_defaults/__init__.py -------------------------------------------------------------------------------- /tests/test_basics_defaults/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /tests/test_basics_defaults/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TestBasicsDefaultsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'test_basics_defaults' 7 | -------------------------------------------------------------------------------- /tests/test_basics_defaults/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.5 on 2022-07-10 11:05 2 | 3 | from django.db import migrations, models 4 | import test_basics_defaults.models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Student', 17 | fields=[ 18 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('name', models.CharField(max_length=50)), 20 | ('year', models.IntegerField(validators=[test_basics_defaults.models.is_valid_year])), 21 | ], 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /tests/test_basics_defaults/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anp-scp/django-to-rest/03f17f276e643607640615305a0b476ed9ba2bf6/tests/test_basics_defaults/migrations/__init__.py -------------------------------------------------------------------------------- /tests/test_basics_defaults/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.core.exceptions import ValidationError 3 | from django.utils.translation import gettext_lazy as _ 4 | from to_rest.decorators import restifyModel 5 | 6 | def is_valid_year(value): 7 | if value not in [1,2,3,4]: 8 | raise ValidationError(_('%(value)s is not correct year'), params={'value': value}) 9 | 10 | @restifyModel 11 | class Student(models.Model): 12 | name = models.CharField(max_length=50) 13 | year = models.IntegerField(validators=[is_valid_year]) 14 | 15 | def __str__(self): 16 | return "[name={}]".format(self.name) 17 | 18 | -------------------------------------------------------------------------------- /tests/test_basics_defaults/tests.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | from rest_framework import status 3 | from rest_framework.test import APITestCase 4 | from test_basics_defaults.models import Student 5 | from django.contrib.auth.models import User 6 | from django.test import override_settings 7 | from django.conf import settings 8 | # Create your tests here. 9 | 10 | class StudenBasicTestWithDefaultSettings(APITestCase): 11 | """ 12 | These tests are to make sure that the default tests are picked from settings or not. 13 | Command to run these tests: 14 | $ pwd 15 | /.../django-to-rest/tests 16 | $ python3 manage.py test test_basics_defaults --settings=tests.settings_test_basics_defaults 17 | """ 18 | 19 | def setUp(self): 20 | User.objects.create_superuser(username='test', password='test@1234', email=None) 21 | self.client.credentials(HTTP_AUTHORIZATION="Basic dGVzdDp0ZXN0QDEyMzQ=") 22 | 23 | def test_case_list_object(self): 24 | """ 25 | Test Case: test_basics_defaults-StudenBasicTestWithDefaultSettings-1 26 | Check if 401 error code is returned if case of no credentials 27 | """ 28 | url = reverse('test_basics_defaults_student-list') 29 | self.client.credentials() 30 | response = self.client.get(url, format='json') 31 | self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) 32 | 33 | def test_case_filter_object(self): 34 | """ 35 | Test Case: test_basics_defaults-StudenBasicTestWithDefaultSettings-2 36 | Ensure that we can filter objects created 37 | """ 38 | url = reverse('test_basics_defaults_student-list') 39 | data = {'name': 'John Doe', 'year': 2} 40 | response = self.client.post(url, data, format='json') 41 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 42 | self.assertEqual(Student.objects.count(),1) 43 | id1 = response.data['id'] 44 | url = reverse('test_basics_defaults_student-list') 45 | data = {'name': 'Ryan Doe', 'year': 1} 46 | response = self.client.post(url, data, format='json') 47 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 48 | self.assertEqual(Student.objects.count(),2) 49 | id2 = response.data['id'] 50 | url = reverse('test_basics_defaults_student-list') 51 | response = self.client.get(url+"?year=2", format='json') 52 | self.assertEqual(response.status_code, status.HTTP_200_OK) 53 | for datum in response.data: 54 | self.assertEqual(datum['year'], 2) 55 | 56 | def test_case_search_object(self): 57 | """ 58 | Test Case: test_basics_defaults-StudenBasicTestWithDefaultSettings-3 59 | Only 'django_filters.rest_framework.DjangoFilterBackend' is specified in 60 | DEFAULT_FILTER_BACKENDS in REST_FRAMEWORK in settings. Ensure that other 61 | filters like SerachFilter or OrderFilter is not picked up as part of 62 | django-to-rest's default behaviour. 63 | """ 64 | url = reverse('test_basics_defaults_student-list') 65 | data = {'name': 'John Doe', 'year': 2} 66 | response = self.client.post(url, data, format='json') 67 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 68 | self.assertEqual(Student.objects.count(),1) 69 | id1 = response.data['id'] 70 | url = reverse('test_basics_defaults_student-list') 71 | data = {'name': 'Ryan Doe', 'year': 1} 72 | response = self.client.post(url, data, format='json') 73 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 74 | self.assertEqual(Student.objects.count(),2) 75 | id2 = response.data['id'] 76 | url = reverse('test_basics_defaults_student-list') 77 | data = {'name': 'Ryan Shaw', 'year': 1} 78 | response = self.client.post(url, data, format='json') 79 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 80 | self.assertEqual(Student.objects.count(),3) 81 | id3 = response.data['id'] 82 | url = reverse('test_basics_defaults_student-list') 83 | response = self.client.get(url+"?search=Doe", format='json') 84 | self.assertEqual(response.status_code, status.HTTP_200_OK) 85 | self.assertEqual(len(response.data),3) 86 | 87 | def test_case_ordering_object(self): 88 | """ 89 | Test Case: test_basics_defaults-StudenBasicTestWithDefaultSettings-4 90 | Only 'django_filters.rest_framework.DjangoFilterBackend' is specified in 91 | DEFAULT_FILTER_BACKENDS in REST_FRAMEWORK in settings. Ensure that other 92 | filters like SerachFilter or OrderFilter is not picked up as part of 93 | django-to-rest's default behaviour. 94 | """ 95 | url = reverse('test_basics_defaults_student-list') 96 | data = {'name': 'John Doe', 'year': 2} 97 | response = self.client.post(url, data, format='json') 98 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 99 | self.assertEqual(Student.objects.count(),1) 100 | id1 = response.data['id'] 101 | url = reverse('test_basics_defaults_student-list') 102 | data = {'name': 'Ryan Doe', 'year': 1} 103 | response = self.client.post(url, data, format='json') 104 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 105 | self.assertEqual(Student.objects.count(),2) 106 | id2 = response.data['id'] 107 | url = reverse('test_basics_defaults_student-list') 108 | data = {'name': 'Ryan Shaw', 'year': 1} 109 | response = self.client.post(url, data, format='json') 110 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 111 | self.assertEqual(Student.objects.count(),3) 112 | id3 = response.data['id'] 113 | url = reverse('test_basics_defaults_student-list') 114 | response = self.client.get(url+"?ordering=year,-name", format='json') 115 | self.assertEqual(response.status_code, status.HTTP_200_OK) 116 | self.assertEqual(response.data[0]['name'], 'John Doe') 117 | self.assertEqual(response.data[1]['name'], 'Ryan Doe') 118 | self.assertEqual(response.data[2]['name'], 'Ryan Shaw') -------------------------------------------------------------------------------- /tests/test_basics_defaults/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /tests/test_many_to_many/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anp-scp/django-to-rest/03f17f276e643607640615305a0b476ed9ba2bf6/tests/test_many_to_many/__init__.py -------------------------------------------------------------------------------- /tests/test_many_to_many/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /tests/test_many_to_many/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TestManyToManyConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'test_many_to_many' 7 | -------------------------------------------------------------------------------- /tests/test_many_to_many/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.5 on 2022-10-03 07:44 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Student1', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(max_length=75)), 19 | ('friends', models.ManyToManyField(to='test_many_to_many.student1')), 20 | ], 21 | ), 22 | migrations.CreateModel( 23 | name='Student', 24 | fields=[ 25 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 26 | ('name', models.CharField(max_length=75)), 27 | ('friends', models.ManyToManyField(to='test_many_to_many.student')), 28 | ], 29 | ), 30 | migrations.CreateModel( 31 | name='Course1', 32 | fields=[ 33 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 34 | ('name', models.CharField(max_length=75)), 35 | ('student', models.ManyToManyField(to='test_many_to_many.student1')), 36 | ], 37 | ), 38 | migrations.CreateModel( 39 | name='Course', 40 | fields=[ 41 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 42 | ('name', models.CharField(max_length=75)), 43 | ('student', models.ManyToManyField(to='test_many_to_many.student')), 44 | ], 45 | ), 46 | ] 47 | -------------------------------------------------------------------------------- /tests/test_many_to_many/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anp-scp/django-to-rest/03f17f276e643607640615305a0b476ed9ba2bf6/tests/test_many_to_many/migrations/__init__.py -------------------------------------------------------------------------------- /tests/test_many_to_many/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from to_rest.decorators import restifyModel 3 | 4 | # Create your models here. 5 | @restifyModel 6 | class Student(models.Model): 7 | name = models.CharField(max_length=75) 8 | friends = models.ManyToManyField("self") 9 | 10 | def __str__(self): 11 | return self.name 12 | @restifyModel 13 | class Course(models.Model): 14 | name = models.CharField(max_length=75) 15 | student = models.ManyToManyField(Student) 16 | 17 | def __str__(self): 18 | return self.name 19 | 20 | @restifyModel 21 | class Student1(models.Model): 22 | name = models.CharField(max_length=75) 23 | friends = models.ManyToManyField("self") 24 | 25 | def __str__(self): 26 | return self.name 27 | @restifyModel 28 | class Course1(models.Model): 29 | name = models.CharField(max_length=75) 30 | student = models.ManyToManyField(Student1) 31 | 32 | def __str__(self): 33 | return self.name -------------------------------------------------------------------------------- /tests/test_many_to_many/tests.py: -------------------------------------------------------------------------------- 1 | from unicodedata import name 2 | from django.urls import reverse 3 | from rest_framework import status 4 | from rest_framework.test import APITestCase 5 | from django.contrib.auth.models import User, Permission 6 | from test_many_to_many.models import Student1, Course1, Student, Course 7 | from django.contrib.contenttypes.models import ContentType 8 | 9 | class TestCaseCRUD(APITestCase): 10 | """ 11 | These tests are to ensure that objects with many to many relationship can be created 12 | as expected 13 | Command to run these tests: 14 | $ pwd 15 | /.../django-to-rest/tests 16 | $ python3 manage.py test test_many_to_many 17 | """ 18 | 19 | def test_case_create_objects(self): 20 | """ 21 | Test Case: test_many_to_many-TestCaseCRUD-1 22 | Ensure that objects are created successfully 23 | """ 24 | 25 | url = reverse('test_many_to_many_student-list') 26 | data = {'name': "John Doe"} 27 | response = self.client.post(url, data=data, response='json') 28 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 29 | 30 | data = {'name': "Alice Doe"} 31 | response = self.client.post(url, data=data, response='json') 32 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 33 | 34 | url = reverse('test_many_to_many_course-list') 35 | data = {'name': "CS601"} 36 | response = self.client.post(url, data=data, response='json') 37 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 38 | 39 | data = {'name': "CS602"} 40 | response = self.client.post(url, data=data, response='json') 41 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 42 | 43 | class TestCaseManyToManyRelation(APITestCase): 44 | """ 45 | These tests are to ensure that many to many relationship works as expected 46 | Command to run these tests: 47 | $ pwd 48 | /.../django-to-rest/tests 49 | $ python3 manage.py test test_many_to_many 50 | """ 51 | 52 | def setUp(self): 53 | s1 = Student(name="John Doe") 54 | s1.save() 55 | s2 = Student(name="Alice Doe") 56 | s2.save() 57 | s3 = Student(name="Eva Doe") 58 | s3.save() 59 | c1 = Course(name="CS601") 60 | c1.save() 61 | c2 = Course(name="CS602") 62 | c2.save() 63 | 64 | def test_case_many_to_many_list(self): 65 | """ 66 | Test Case: test_many_to_many-ManyToManyRelation-1 67 | Ensure that nested URL works correctly for many-to-many 68 | """ 69 | host = 'http://127.0.0.1:8000' 70 | url = reverse('test_many_to_many_student-list') 71 | response = self.client.get(url, response='json') 72 | self.assertEqual(response.status_code, status.HTTP_200_OK) 73 | s1_id = response.data[0]['id'] 74 | s1_nestedUrl = response.data[0]['course_set'] 75 | s1_nestedUrl += "" if s1_nestedUrl.endswith('/') else "/" 76 | s2_id = response.data[1]['id'] 77 | s2_nestedUrl = response.data[1]['course_set'] 78 | s2_nestedUrl += "" if s2_nestedUrl.endswith('/') else "/" 79 | s3_id = response.data[2]['id'] 80 | s3_nestedUrl = response.data[2]['course_set'] 81 | s3_nestedUrl += "" if s3_nestedUrl.endswith('/') else "/" 82 | 83 | url = reverse('test_many_to_many_course-list') 84 | response = self.client.get(url, response='json') 85 | self.assertEqual(response.status_code, status.HTTP_200_OK) 86 | c1_id = response.data[0]['id'] 87 | c1_nestedUrl = response.data[0]['student'] 88 | c1_nestedUrl += "" if c1_nestedUrl.endswith('/') else "/" 89 | c2_id = response.data[1]['id'] 90 | c2_nestedUrl = response.data[1]['student'] 91 | c2_nestedUrl += "" if c2_nestedUrl.endswith('/') else "/" 92 | 93 | url = host + s1_nestedUrl 94 | response = self.client.post(url, data={'course': c1_id}, response='json') 95 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 96 | rel1_id = response.data['id'] 97 | 98 | url = host + s1_nestedUrl + str(rel1_id) + "/" 99 | response = self.client.get(url, response='json') 100 | self.assertEqual(response.status_code, status.HTTP_200_OK) 101 | self.assertEqual(response.data['student'], s1_id) 102 | 103 | url = host + c1_nestedUrl 104 | response = self.client.post(url, data={'student': s2_id}, response='json') 105 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 106 | 107 | url = host + c1_nestedUrl + "?ordering=student" 108 | response = self.client.get(url, response = 'json') 109 | self.assertEqual(response.status_code, status.HTTP_200_OK) 110 | self.assertEqual(response.data[0]['student'], s1_id) 111 | self.assertEqual(response.data[1]['student'], s2_id) 112 | 113 | url = host + c2_nestedUrl 114 | response = self.client.post(url, data={'student': s1_id}, response='json') 115 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 116 | rel2_id = response.data['id'] 117 | 118 | url = host + s1_nestedUrl + "?ordering=course" 119 | response = self.client.get(url, response = 'json') 120 | self.assertEqual(response.status_code, status.HTTP_200_OK) 121 | self.assertEqual(response.data[0]['course'], c1_id) 122 | self.assertEqual(response.data[1]['course'], c2_id) 123 | self.assertEqual(len(response.data),2) 124 | 125 | url = host + c2_nestedUrl + "?ordering=student" 126 | response = self.client.get(url, response = 'json') 127 | self.assertEqual(response.status_code, status.HTTP_200_OK) 128 | self.assertEqual(len(response.data),1) 129 | 130 | url = host + c2_nestedUrl + str(rel2_id) + "/" 131 | response = self.client.patch(url, data={'student': s2_id}, response='json') 132 | self.assertEqual(response.status_code, status.HTTP_200_OK) 133 | 134 | url = host + s1_nestedUrl + "?ordering=course" 135 | response = self.client.get(url, response = 'json') 136 | self.assertEqual(response.status_code, status.HTTP_200_OK) 137 | self.assertEqual(len(response.data),1) 138 | 139 | url = host + c1_nestedUrl + str(rel1_id) + "/" 140 | response = self.client.patch(url, data={'student': s3_id}, response='json') 141 | self.assertEqual(response.status_code, status.HTTP_200_OK) 142 | 143 | url = host + s1_nestedUrl + "?ordering=course" 144 | response = self.client.get(url, response = 'json') 145 | self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) 146 | 147 | url = host + s3_nestedUrl + str(rel1_id) + "/" 148 | response = self.client.delete(url,response='json') 149 | self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) 150 | 151 | url = host + s3_nestedUrl 152 | response = self.client.get(url,response='json') 153 | self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) 154 | -------------------------------------------------------------------------------- /tests/test_many_to_many/view_params.py: -------------------------------------------------------------------------------- 1 | from to_rest import constants 2 | from rest_framework.permissions import DjangoModelPermissions, IsAuthenticated 3 | from to_rest.utils import ViewParams 4 | from rest_framework.authentication import BasicAuthentication 5 | 6 | class DDjangoModelPermissions(DjangoModelPermissions): 7 | perms_map = { 8 | 'GET': ['%(app_label)s.view_%(model_name)s'], 9 | 'POST': ['%(app_label)s.add_%(model_name)s'], 10 | 'PUT': ['%(app_label)s.update_%(model_name)s'], 11 | 'PATCH': ['%(app_label)s.update_%(model_name)s'], 12 | 'DELETE': ['%(app_label)s.delete_%(model_name)s'] 13 | } 14 | 15 | class CustomPermission(ViewParams): 16 | 17 | def getParams(): 18 | temp = dict() 19 | temp[constants.AUTHENTICATION_CLASSES] = [BasicAuthentication] 20 | temp[constants.PERMISSION_CLASSES] = [IsAuthenticated, DDjangoModelPermissions] 21 | return temp -------------------------------------------------------------------------------- /tests/test_many_to_many/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /tests/test_many_to_one/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anp-scp/django-to-rest/03f17f276e643607640615305a0b476ed9ba2bf6/tests/test_many_to_one/__init__.py -------------------------------------------------------------------------------- /tests/test_many_to_one/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /tests/test_many_to_one/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TestManyToOneConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'test_many_to_one' 7 | -------------------------------------------------------------------------------- /tests/test_many_to_one/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.5 on 2022-10-01 07:42 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import test_many_to_one.models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Question', 18 | fields=[ 19 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('question_text', models.CharField(max_length=200)), 21 | ('pub_date', models.DateTimeField(default=test_many_to_one.models.Question.pub_date_default, verbose_name='date published')), 22 | ], 23 | ), 24 | migrations.CreateModel( 25 | name='Choice', 26 | fields=[ 27 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 28 | ('choice_text', models.CharField(max_length=200)), 29 | ('votes', models.IntegerField(default=0)), 30 | ('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='test_many_to_one.question')), 31 | ], 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /tests/test_many_to_one/migrations/0002_question1_choice1.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.5 on 2022-10-01 15:01 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import test_many_to_one.models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('test_many_to_one', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Question1', 17 | fields=[ 18 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('question_text', models.CharField(max_length=200)), 20 | ('pub_date', models.DateTimeField(default=test_many_to_one.models.Question1.pub_date_default, verbose_name='date published')), 21 | ], 22 | ), 23 | migrations.CreateModel( 24 | name='Choice1', 25 | fields=[ 26 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 27 | ('choice_text', models.CharField(max_length=200)), 28 | ('votes', models.IntegerField(default=0)), 29 | ('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='test_many_to_one.question1')), 30 | ], 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /tests/test_many_to_one/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anp-scp/django-to-rest/03f17f276e643607640615305a0b476ed9ba2bf6/tests/test_many_to_one/migrations/__init__.py -------------------------------------------------------------------------------- /tests/test_many_to_one/models.py: -------------------------------------------------------------------------------- 1 | from email.policy import default 2 | from django.db import models 3 | from django.utils import timezone 4 | from to_rest.decorators import restifyModel 5 | 6 | # Create your models here. 7 | @restifyModel 8 | class Question(models.Model): 9 | question_text = models.CharField(max_length=200) 10 | 11 | def pub_date_default(): 12 | return timezone.now() 13 | 14 | pub_date = models.DateTimeField('date published', default=pub_date_default) 15 | 16 | @restifyModel 17 | class Choice(models.Model): 18 | question = models.ForeignKey(Question, on_delete=models.CASCADE, related_name='choices') 19 | choice_text = models.CharField(max_length=200) 20 | votes = models.IntegerField(default=0) 21 | 22 | 23 | @restifyModel(customViewParams='CustomPermission') 24 | class Question1(models.Model): 25 | question_text = models.CharField(max_length=200) 26 | 27 | def pub_date_default(): 28 | return timezone.now() 29 | 30 | pub_date = models.DateTimeField('date published', default=pub_date_default) 31 | 32 | @restifyModel(customViewParams='CustomPermission') 33 | class Choice1(models.Model): 34 | question = models.ForeignKey(Question1, on_delete=models.CASCADE, related_name='choices') 35 | choice_text = models.CharField(max_length=200) 36 | votes = models.IntegerField(default=0) -------------------------------------------------------------------------------- /tests/test_many_to_one/tests.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | from rest_framework import status 3 | from rest_framework.test import APITestCase 4 | from django.contrib.auth.models import User, Permission 5 | from test_many_to_one.models import Question1, Choice1 6 | from django.contrib.contenttypes.models import ContentType 7 | 8 | class TestCaseCRUD(APITestCase): 9 | """ 10 | These tests are to ensure that many to one relationship works as expected 11 | Command to run these tests: 12 | $ pwd 13 | /.../django-to-rest/tests 14 | $ python3 manage.py test test_many_to_one 15 | """ 16 | 17 | def test_case_create_objects_with_defauls(self): 18 | """ 19 | Test Case: test_many_to_one-TestCaseCRUD-1 20 | Ensure that objects are created successfully with default values 21 | """ 22 | 23 | url = reverse('test_many_to_one_question-list') 24 | data = {'question_text': "How is the food?"} 25 | response = self.client.post(url, data=data, response='json') 26 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 27 | q_id = response.data['id'] 28 | 29 | url = reverse('test_many_to_one_question-detail', args=[q_id]) 30 | response = self.client.get(url, response='json') 31 | self.assertEqual(response.status_code, status.HTTP_200_OK) 32 | self.assertIsNotNone(response.data['pub_date']) 33 | 34 | url = reverse('test_many_to_one_choice-list') 35 | data = {'choice_text': "Bad", 'question': q_id} 36 | response = self.client.post(url, data=data, response='json') 37 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 38 | c_id = response.data['id'] 39 | 40 | url = reverse('test_many_to_one_choice-detail', args=[c_id]) 41 | response = self.client.get(url, response='json') 42 | self.assertEqual(response.status_code, status.HTTP_200_OK) 43 | self.assertIsNotNone(response.data['votes']) 44 | 45 | def test_case_one_to_many_list(self): 46 | """ 47 | Test Case: test_many_to_one-TestCaseCRUD-2 48 | Ensure that nested URL works correctly for one-to-many 49 | """ 50 | host = 'http://127.0.0.1:8000' 51 | url = reverse('test_many_to_one_question-list') 52 | data = {'question_text': "How is the food?"} 53 | response = self.client.post(url, data=data, response='json') 54 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 55 | q_id = response.data['id'] 56 | nestedUrl = response.data['choices'] 57 | 58 | c_ids = [] 59 | url = reverse('test_many_to_one_choice-list') 60 | data = {'choice_text': "Bad", 'question': q_id} 61 | response = self.client.post(url, data=data, response='json') 62 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 63 | c_ids.append(response.data['id']) 64 | 65 | data = {'choice_text': "Good", 'question': q_id} 66 | response = self.client.post(url, data=data, response='json') 67 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 68 | c_ids.append(response.data['id']) 69 | 70 | data = {'choice_text': "Great", 'question': q_id} 71 | response = self.client.post(url, data=data, response='json') 72 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 73 | c_ids.append(response.data['id']) 74 | 75 | url = host + nestedUrl 76 | response = self.client.get(url, response='json') 77 | self.assertEqual(response.status_code, status.HTTP_200_OK) 78 | for each in response.data: 79 | self.assertIn(each['id'], c_ids) 80 | 81 | # filtering 82 | url = host + nestedUrl + "?choice_text=Good" 83 | response = self.client.get(url, response='json') 84 | self.assertEqual(response.status_code, status.HTTP_200_OK) 85 | self.assertEqual(response.data[0]['id'],2) 86 | 87 | #search 88 | url = host + nestedUrl + "?search=Good" 89 | response = self.client.get(url, response='json') 90 | self.assertEqual(response.status_code, status.HTTP_200_OK) 91 | self.assertEqual(response.data[0]['id'],2) 92 | 93 | #ordering 94 | url = host + nestedUrl + "?ordering=-id" 95 | response = self.client.get(url, response='json') 96 | self.assertEqual(response.status_code, status.HTTP_200_OK) 97 | self.assertEqual(response.data[0]['id'], 3) 98 | self.assertEqual(response.data[1]['id'], 2) 99 | self.assertEqual(response.data[2]['id'], 1) 100 | 101 | class TestCaseNestedAccess(APITestCase): 102 | """ 103 | These tests are to ensure that many to one access policy works as expected 104 | Command to run these tests: 105 | $ pwd 106 | /.../django-to-rest/tests 107 | $ python3 manage.py test test_many_to_one 108 | """ 109 | 110 | def setUp(self): 111 | User.objects.create_superuser(username='test', password='test@1234', email=None) 112 | testy = User.objects.create_user(username='testy', password='test@1234', email=None) 113 | testify = User.objects.create_user(username='testify', password='test@1234', email=None) 114 | 115 | contentType = ContentType.objects.get_for_model(Question1) 116 | question1_permissions = Permission.objects.filter(content_type= contentType) 117 | contentType = ContentType.objects.get_for_model(Choice1) 118 | choice1_permissions = Permission.objects.filter(content_type= contentType) 119 | 120 | for permission in question1_permissions: 121 | testy.user_permissions.add(permission) 122 | testify.user_permissions.remove(permission) 123 | for permission in choice1_permissions: 124 | testify.user_permissions.add(permission) 125 | testy.user_permissions.remove(permission) 126 | 127 | q1 = Question1(question_text = "How is the traffic?") 128 | q1.save() 129 | c1 = Choice1(choice_text = "Clear for miles", question=q1) 130 | c1.save() 131 | c2 = Choice1(choice_text = "Not clear for even a centimetre", question=q1) 132 | c2.save() 133 | 134 | def test_case_check_access(self): 135 | """ 136 | Test Case: test_many_to_one-TestCaseNestedAccess-1 137 | Ensure that objects are accessed as acces policy 138 | """ 139 | superAdmin = "Basic dGVzdDp0ZXN0QDEyMzQ=" 140 | testy = "Basic dGVzdHk6dGVzdEAxMjM0" 141 | testify = "Basic dGVzdGlmeTp0ZXN0QDEyMzQ=" 142 | 143 | #access by super admin: 144 | self.client.credentials(HTTP_AUTHORIZATION=superAdmin) 145 | host = 'http://127.0.0.1:8000' 146 | url = reverse('test_many_to_one_question1-list') 147 | response = self.client.get(url,response='json') 148 | self.assertEqual(response.status_code, status.HTTP_200_OK) 149 | self.assertEqual(len(response.data),1) 150 | nestedUrl = host + response.data[0]['choices'] 151 | response = self.client.get(nestedUrl, response='json') 152 | self.assertEqual(len(response.data),2) 153 | self.client.credentials() 154 | 155 | #access by testy: 156 | self.client.credentials(HTTP_AUTHORIZATION=testy) 157 | response = self.client.get(url,response='json') 158 | self.assertEqual(response.status_code, status.HTTP_200_OK) 159 | self.assertEqual(len(response.data),1) 160 | response = self.client.get(nestedUrl, response='json') 161 | self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) 162 | self.client.credentials() 163 | 164 | #access by testify: 165 | self.client.credentials(HTTP_AUTHORIZATION=testify) 166 | response = self.client.get(url,response='json') 167 | self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) 168 | response = self.client.get(nestedUrl, response='json') 169 | self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) 170 | self.client.credentials() 171 | -------------------------------------------------------------------------------- /tests/test_many_to_one/view_params.py: -------------------------------------------------------------------------------- 1 | from to_rest import constants 2 | from rest_framework.permissions import DjangoModelPermissions, IsAuthenticated 3 | from to_rest.utils import ViewParams 4 | from rest_framework.authentication import BasicAuthentication 5 | 6 | class DDjangoModelPermissions(DjangoModelPermissions): 7 | perms_map = { 8 | 'GET': ['%(app_label)s.view_%(model_name)s'], 9 | 'POST': ['%(app_label)s.add_%(model_name)s'], 10 | 'PUT': ['%(app_label)s.update_%(model_name)s'], 11 | 'PATCH': ['%(app_label)s.update_%(model_name)s'], 12 | 'DELETE': ['%(app_label)s.delete_%(model_name)s'] 13 | } 14 | 15 | class CustomPermission(ViewParams): 16 | 17 | def getParams(): 18 | temp = dict() 19 | temp[constants.AUTHENTICATION_CLASSES] = [BasicAuthentication] 20 | temp[constants.PERMISSION_CLASSES] = [IsAuthenticated, DDjangoModelPermissions] 21 | return temp -------------------------------------------------------------------------------- /tests/test_many_to_one/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /tests/test_one_to_one/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anp-scp/django-to-rest/03f17f276e643607640615305a0b476ed9ba2bf6/tests/test_one_to_one/__init__.py -------------------------------------------------------------------------------- /tests/test_one_to_one/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /tests/test_one_to_one/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TestBasicsOneToOneConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'test_one_to_one' 7 | -------------------------------------------------------------------------------- /tests/test_one_to_one/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.5 on 2022-08-05 18:01 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Student', 17 | fields=[ 18 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('name', models.CharField(max_length=75)), 20 | ('discipline', models.CharField(max_length=10)), 21 | ('program', models.CharField(max_length=10)), 22 | ], 23 | ), 24 | migrations.CreateModel( 25 | name='Student1', 26 | fields=[ 27 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 28 | ('name', models.CharField(max_length=75)), 29 | ('discipline', models.CharField(max_length=10)), 30 | ('program', models.CharField(max_length=10)), 31 | ], 32 | ), 33 | migrations.CreateModel( 34 | name='System1', 35 | fields=[ 36 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 37 | ('name', models.CharField(max_length=75)), 38 | ('location', models.CharField(max_length=20)), 39 | ('student', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='test_one_to_one.student1')), 40 | ], 41 | ), 42 | migrations.CreateModel( 43 | name='System', 44 | fields=[ 45 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 46 | ('name', models.CharField(max_length=75)), 47 | ('location', models.CharField(max_length=20)), 48 | ('student', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='test_one_to_one.student')), 49 | ], 50 | ), 51 | ] 52 | -------------------------------------------------------------------------------- /tests/test_one_to_one/migrations/0002_alter_system1_student.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.5 on 2022-09-29 09:13 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('test_one_to_one', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='system1', 16 | name='student', 17 | field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to='test_one_to_one.student1'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /tests/test_one_to_one/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anp-scp/django-to-rest/03f17f276e643607640615305a0b476ed9ba2bf6/tests/test_one_to_one/migrations/__init__.py -------------------------------------------------------------------------------- /tests/test_one_to_one/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from to_rest.decorators import restifyModel 3 | 4 | # Create your models here. 5 | @restifyModel 6 | class Student(models.Model): 7 | name = models.CharField(max_length=75) 8 | discipline = models.CharField(max_length=10) 9 | program = models.CharField(max_length=10) 10 | 11 | def __str__(self): 12 | return "[name={} ; discipline={} ; program={}]".format(self.name, self.discipline, self.program) 13 | 14 | @restifyModel 15 | class System(models.Model): 16 | name = models.CharField(max_length=75) 17 | location = models.CharField(max_length=20) 18 | student = models.OneToOneField(Student, on_delete=models.CASCADE) 19 | 20 | def __str__(self): 21 | return "[name={} ; location={}]".format(self.name, self.location) 22 | 23 | @restifyModel 24 | class Student1(models.Model): 25 | name = models.CharField(max_length=75) 26 | discipline = models.CharField(max_length=10) 27 | program = models.CharField(max_length=10) 28 | 29 | def __str__(self): 30 | return "[name={} ; discipline={} ; program={}]".format(self.name, self.discipline, self.program) 31 | 32 | @restifyModel 33 | class System1(models.Model): 34 | name = models.CharField(max_length=75) 35 | location = models.CharField(max_length=20) 36 | student = models.OneToOneField(Student1, models.CASCADE, null=True) 37 | 38 | def __str__(self): 39 | return "[name={} ; location={}]".format(self.name, self.location) 40 | -------------------------------------------------------------------------------- /tests/test_one_to_one/tests.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | from rest_framework import status 3 | from rest_framework.test import APITestCase 4 | from test_one_to_one import models 5 | import json 6 | 7 | class TestCaseOneToOne(APITestCase): 8 | """ 9 | These tests are to ensure that one to one relationship works as expected 10 | Command to run these tests: 11 | $ pwd 12 | /.../django-to-rest/tests 13 | $ python3 manage.py test test_one_to_one 14 | """ 15 | 16 | def test_case_create_objects(self): 17 | """ 18 | Test Case: test_one_to_one-TestCaseOneToOne-1 19 | Ensure that objects are created successfully 20 | """ 21 | 22 | url = reverse('test_one_to_one_student-list') 23 | data = {'name': "John Doe", 'discipline': "CS", 'program': "MS"} 24 | response = self.client.post(url, data=data, response='json') 25 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 26 | studentId = response.data['id'] 27 | url = reverse('test_one_to_one_system-list') 28 | data = {'name': "Dell Vostro 1558", 'location': 'AB1-102', 'student': 1} 29 | response = self.client.post(url, data=data, response='json') 30 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 31 | 32 | 33 | 34 | def test_case_update_objects_null_relation(self): 35 | """ 36 | Test Case: test_one_to_one-TestCaseOneToOne-2 37 | Ensure that the OneToOneRel Field is read only and is updated. 38 | """ 39 | 40 | url = reverse('test_one_to_one_system1-list') 41 | data = {'name': "Dell Vostro 1558", 'location': 'AB1-102'} 42 | response = self.client.post(url, data=data, response='json') 43 | systemId1 = response.data['id'] 44 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 45 | 46 | data = {'name': "Dell Vostro 4558", 'location': 'AB1-102'} 47 | response = self.client.post(url, data=data, response='json') 48 | systemId2 = response.data['id'] 49 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 50 | 51 | url = reverse('test_one_to_one_student1-list') 52 | data = {'name': "John Doe", 'discipline': "CS", 'program': "MS"} 53 | response = self.client.post(url, data=data, response='json') 54 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 55 | studentId = response.data['id'] 56 | 57 | url = reverse('test_one_to_one_student1-detail', args=[studentId]) 58 | response = self.client.get(url, response='json') 59 | self.assertEqual(response.status_code, status.HTTP_200_OK) 60 | self.assertEqual(response.data['system1'], None) 61 | 62 | url = reverse('test_one_to_one_system1-detail', args=[systemId1]) 63 | data = {'student': studentId} 64 | response = self.client.patch(url, data=data, response='json') 65 | self.assertEqual(response.status_code, status.HTTP_200_OK) 66 | 67 | url = reverse('test_one_to_one_student1-detail', args=[studentId]) 68 | response = self.client.get(url, response='json') 69 | self.assertEqual(response.status_code, status.HTTP_200_OK) 70 | self.assertEqual(response.data['system1'], systemId1) 71 | 72 | url = reverse('test_one_to_one_system1-detail', args=[systemId1]) 73 | data = json.dumps({'student': None}) 74 | response = self.client.patch(url, data=data, response='json', content_type='application/json') 75 | print(response.data) 76 | self.assertEqual(response.status_code, status.HTTP_200_OK) 77 | 78 | url = reverse('test_one_to_one_system1-detail', args=[systemId2]) 79 | data = {'student': studentId} 80 | response = self.client.patch(url, data=data, response='json') 81 | self.assertEqual(response.status_code, status.HTTP_200_OK) 82 | 83 | url = reverse('test_one_to_one_student1-detail', args=[studentId]) 84 | response = self.client.get(url, response='json') 85 | self.assertEqual(response.status_code, status.HTTP_200_OK) 86 | self.assertEqual(response.data['system1'], systemId2) 87 | 88 | -------------------------------------------------------------------------------- /tests/test_one_to_one/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /tests/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anp-scp/django-to-rest/03f17f276e643607640615305a0b476ed9ba2bf6/tests/tests/__init__.py -------------------------------------------------------------------------------- /tests/tests/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for tests project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /tests/tests/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for tests project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.0.5. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.0/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ 21 | 22 | from django.core.management.utils import get_random_secret_key 23 | SECRET_KEY = get_random_secret_key() 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'test_many_to_many', 35 | 'test_many_to_one', 36 | 'test_one_to_one', 37 | 'test_basics', 38 | 'rest_framework', 39 | 'django.contrib.admin', 40 | 'django.contrib.auth', 41 | 'django.contrib.contenttypes', 42 | 'django.contrib.sessions', 43 | 'django.contrib.messages', 44 | 'django.contrib.staticfiles', 45 | ] 46 | 47 | MIDDLEWARE = [ 48 | 'django.middleware.security.SecurityMiddleware', 49 | 'django.contrib.sessions.middleware.SessionMiddleware', 50 | 'django.middleware.common.CommonMiddleware', 51 | 'django.middleware.csrf.CsrfViewMiddleware', 52 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 53 | 'django.contrib.messages.middleware.MessageMiddleware', 54 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 55 | ] 56 | 57 | ROOT_URLCONF = 'tests.urls' 58 | 59 | TEMPLATES = [ 60 | { 61 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 62 | 'DIRS': [], 63 | 'APP_DIRS': True, 64 | 'OPTIONS': { 65 | 'context_processors': [ 66 | 'django.template.context_processors.debug', 67 | 'django.template.context_processors.request', 68 | 'django.contrib.auth.context_processors.auth', 69 | 'django.contrib.messages.context_processors.messages', 70 | ], 71 | }, 72 | }, 73 | ] 74 | 75 | WSGI_APPLICATION = 'tests.wsgi.application' 76 | 77 | 78 | # Database 79 | # https://docs.djangoproject.com/en/4.0/ref/settings/#databases 80 | 81 | DATABASES = { 82 | 'default': { 83 | 'ENGINE': 'django.db.backends.sqlite3', 84 | 'NAME': BASE_DIR / 'db.sqlite3', 85 | } 86 | } 87 | 88 | 89 | # Password validation 90 | # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators 91 | 92 | AUTH_PASSWORD_VALIDATORS = [ 93 | { 94 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 95 | }, 96 | { 97 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 98 | }, 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 101 | }, 102 | { 103 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 104 | }, 105 | ] 106 | 107 | 108 | # Internationalization 109 | # https://docs.djangoproject.com/en/4.0/topics/i18n/ 110 | 111 | LANGUAGE_CODE = 'en-us' 112 | 113 | TIME_ZONE = 'UTC' 114 | 115 | USE_I18N = True 116 | 117 | USE_TZ = True 118 | 119 | 120 | # Static files (CSS, JavaScript, Images) 121 | # https://docs.djangoproject.com/en/4.0/howto/static-files/ 122 | 123 | STATIC_URL = 'static/' 124 | 125 | # Default primary key field type 126 | # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field 127 | 128 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 129 | 130 | # Options for REST_FRAMEWORK for testing purposes 131 | REST_FRAMEWORK = { 132 | 'DEFAULT_THROTTLE_CLASSES': [ 133 | 'rest_framework.throttling.ScopedRateThrottle', 134 | ], 135 | 'DEFAULT_THROTTLE_RATES': { 136 | 'studentCustomThrottle': '5/min' 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /tests/tests/settings_test_basics_defaults.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for tests project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.0.5. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.0/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ 21 | 22 | from django.core.management.utils import get_random_secret_key 23 | SECRET_KEY = get_random_secret_key() 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'test_basics_defaults', 35 | 'rest_framework', 36 | 'django.contrib.admin', 37 | 'django.contrib.auth', 38 | 'django.contrib.contenttypes', 39 | 'django.contrib.sessions', 40 | 'django.contrib.messages', 41 | 'django.contrib.staticfiles', 42 | ] 43 | 44 | MIDDLEWARE = [ 45 | 'django.middleware.security.SecurityMiddleware', 46 | 'django.contrib.sessions.middleware.SessionMiddleware', 47 | 'django.middleware.common.CommonMiddleware', 48 | 'django.middleware.csrf.CsrfViewMiddleware', 49 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 50 | 'django.contrib.messages.middleware.MessageMiddleware', 51 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 52 | ] 53 | 54 | ROOT_URLCONF = 'tests.urls' 55 | 56 | TEMPLATES = [ 57 | { 58 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 59 | 'DIRS': [], 60 | 'APP_DIRS': True, 61 | 'OPTIONS': { 62 | 'context_processors': [ 63 | 'django.template.context_processors.debug', 64 | 'django.template.context_processors.request', 65 | 'django.contrib.auth.context_processors.auth', 66 | 'django.contrib.messages.context_processors.messages', 67 | ], 68 | }, 69 | }, 70 | ] 71 | 72 | WSGI_APPLICATION = 'tests.wsgi.application' 73 | 74 | 75 | # Database 76 | # https://docs.djangoproject.com/en/4.0/ref/settings/#databases 77 | 78 | DATABASES = { 79 | 'default': { 80 | 'ENGINE': 'django.db.backends.sqlite3', 81 | 'NAME': BASE_DIR / 'db.sqlite3', 82 | } 83 | } 84 | 85 | 86 | # Password validation 87 | # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators 88 | 89 | AUTH_PASSWORD_VALIDATORS = [ 90 | { 91 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 92 | }, 93 | { 94 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 95 | }, 96 | { 97 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 98 | }, 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 101 | }, 102 | ] 103 | 104 | 105 | # Internationalization 106 | # https://docs.djangoproject.com/en/4.0/topics/i18n/ 107 | 108 | LANGUAGE_CODE = 'en-us' 109 | 110 | TIME_ZONE = 'UTC' 111 | 112 | USE_I18N = True 113 | 114 | USE_TZ = True 115 | 116 | 117 | # Static files (CSS, JavaScript, Images) 118 | # https://docs.djangoproject.com/en/4.0/howto/static-files/ 119 | 120 | STATIC_URL = 'static/' 121 | 122 | # Default primary key field type 123 | # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field 124 | 125 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 126 | 127 | REST_FRAMEWORK = { 128 | 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], 129 | 'DEFAULT_AUTHENTICATION_CLASSES': ['rest_framework.authentication.BasicAuthentication'], 130 | 'DEFAULT_PERMISSION_CLASSES': ['rest_framework.permissions.IsAuthenticated'] 131 | } 132 | -------------------------------------------------------------------------------- /tests/tests/urls.py: -------------------------------------------------------------------------------- 1 | """tests URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/4.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path 18 | from to_rest import utils 19 | 20 | urlpatterns = [ 21 | path('admin/', admin.site.urls), 22 | ] 23 | urlpatterns.extend(utils.restifyApp('rest/v1')) 24 | -------------------------------------------------------------------------------- /tests/tests/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for tests project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /to_rest/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anp-scp/django-to-rest/03f17f276e643607640615305a0b476ed9ba2bf6/to_rest/__init__.py -------------------------------------------------------------------------------- /to_rest/__pycache__/__init__.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anp-scp/django-to-rest/03f17f276e643607640615305a0b476ed9ba2bf6/to_rest/__pycache__/__init__.cpython-38.pyc -------------------------------------------------------------------------------- /to_rest/__pycache__/apps.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anp-scp/django-to-rest/03f17f276e643607640615305a0b476ed9ba2bf6/to_rest/__pycache__/apps.cpython-38.pyc -------------------------------------------------------------------------------- /to_rest/__pycache__/cfg.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anp-scp/django-to-rest/03f17f276e643607640615305a0b476ed9ba2bf6/to_rest/__pycache__/cfg.cpython-38.pyc -------------------------------------------------------------------------------- /to_rest/__pycache__/constants.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anp-scp/django-to-rest/03f17f276e643607640615305a0b476ed9ba2bf6/to_rest/__pycache__/constants.cpython-38.pyc -------------------------------------------------------------------------------- /to_rest/__pycache__/decorators.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anp-scp/django-to-rest/03f17f276e643607640615305a0b476ed9ba2bf6/to_rest/__pycache__/decorators.cpython-38.pyc -------------------------------------------------------------------------------- /to_rest/__pycache__/exceptions.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anp-scp/django-to-rest/03f17f276e643607640615305a0b476ed9ba2bf6/to_rest/__pycache__/exceptions.cpython-38.pyc -------------------------------------------------------------------------------- /to_rest/__pycache__/serializers.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anp-scp/django-to-rest/03f17f276e643607640615305a0b476ed9ba2bf6/to_rest/__pycache__/serializers.cpython-38.pyc -------------------------------------------------------------------------------- /to_rest/__pycache__/utils.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anp-scp/django-to-rest/03f17f276e643607640615305a0b476ed9ba2bf6/to_rest/__pycache__/utils.cpython-38.pyc -------------------------------------------------------------------------------- /to_rest/__pycache__/views.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anp-scp/django-to-rest/03f17f276e643607640615305a0b476ed9ba2bf6/to_rest/__pycache__/views.cpython-38.pyc -------------------------------------------------------------------------------- /to_rest/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class RestifyConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'to_rest' 7 | -------------------------------------------------------------------------------- /to_rest/cfg.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | djangoToRestRegistry = defaultdict(None) #a dictionary which is used as a registry to store 3 | #all the components of the project like serializers, viewsets, actions, custom parameters, etc -------------------------------------------------------------------------------- /to_rest/constants.py: -------------------------------------------------------------------------------- 1 | #options 2 | LIST_METHOD = "LIST_METHOD" 3 | RETREIVE_METHOD = "RETREIVE_METHOD" 4 | CREATE_METHOD = "CREATE_METHOD" 5 | UPDATE_METHOD = "UPDATE_METHOD" 6 | PARTIAL_UPDATE_METHOD = "PARTIAL_UPDATE_METHOD" 7 | DESTROY_METHOD = "DESTROY_METHOD" 8 | GET_OBJECT_METHOD = "GET_OBJECT_METHOD" 9 | GET_QUERYSET_METHOD = "GET_QUERYSET_METHOD" 10 | CUSTOM_VIEW_PARAMS = "CUSTOM_VIEW_PARAMS" 11 | EXCLUDE_FIELDS = "EXCLUDE_FIELDS" 12 | FILTER_SETTINGS = "FILTER_SETTINGS" 13 | METHOD_FIELDS = "METHOD_FIELDS" 14 | CUSTOM_SERIALIZER = "CUSTOM_SERIALIZER" 15 | DEFAULT_SERIALIZER = "DEFAULT_SERIALIZER" 16 | VIEW_SET_ATTRIBUTES = "VIEW_SET_ATTRIBUTES" 17 | REQUIRED_REVERSE_REL_FIELDS = "REQUIRED_REVERSE_REL_FIELDS" 18 | DEFAULT_ACTIONS = "DEFAULT_ACTIONS" 19 | CUSTOM_ACTIONS = "CUSTOM_ACTIONS" 20 | DEFAULT_VIEW_SET = "DEFAULT_VIEW_SET" 21 | 22 | #settings constants 23 | DEFAULT_FILTER_BACKENDS = "DEFAULT_FILTER_BACKENDS" 24 | 25 | #other view attributes 26 | QUERYSET = "queryset" 27 | SERIALIZER_CLASS = "serializer_class" 28 | FILTERSET_CLASS = "filterset_class" 29 | FILTERSET_FIELDS = "filterset_fields" 30 | SEARCH_FIELDS = "search_fields" 31 | ORDERING_FIELDS = "ordering_fields" 32 | FILTER_BACKENDS = "filter_backends" 33 | ORDERING = "ordering" 34 | AUTHENTICATION_CLASSES = "authentication_classes" 35 | PERMISSION_CLASSES = "permission_classes" 36 | THROTTLE_SCOPE = "throttle_scope" 37 | #affixes 38 | ONE_TO_MANY_LIST_ACTION = "oneToManyList_" 39 | MANY_TO_MANY_LIST_ACTION = "manyToManyList_" 40 | MANY_TO_MANY_DETAIL_ACTION = "manyToManyDetail_" 41 | PROJECT_NAME_PREFIX = "DjangoToRest_" 42 | 43 | #header keys 44 | CONTENT_MESSAGE = "Content-Message" 45 | 46 | 47 | #messages 48 | NOT_A_MODEL_CLASS_MSG = "The decorated class must be a sub class django.db.models.Model" 49 | NO_OBJECT_EXISTS = "No {} object exists" 50 | TYPE_ERROR_MESSAGE = "{}: Expected {} but got {}" -------------------------------------------------------------------------------- /to_rest/decorators.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from to_rest import exceptions 3 | from to_rest import constants 4 | from to_rest import cfg 5 | from rest_framework.serializers import BaseSerializer 6 | import logging 7 | from collections import defaultdict 8 | 9 | def restifyModel(_cls=None, *, customViewParams=None, excludeFields=None, methodFields=None): 10 | """ 11 | A decorator function to include the models in the registry so that the decorated models 12 | are marked for restification. By restification we mean to expose REST api(s) for that 13 | model. 14 | 15 | Parameters: 16 | _cls (object): The class that needs to be decorated. 17 | 18 | customViewParams (str): To provide class names in view_params.py for custom view parameters. 19 | 20 | excludeFields (list): The fields that needs to be excluded from the JSON object. provide 21 | fields will not be included in the serializer. If customSerializer is provided then this 22 | parameter will ne ignored. 23 | 24 | methodFields (list): The list of methods as read only fields. This can be used to include the 25 | model's methods output as field. This include only those field that don't take any parameter. 26 | 27 | Returns: 28 | decorated class or function object 29 | """ 30 | if customViewParams is not None and not isinstance(customViewParams, str): 31 | raise TypeError(constants.TYPE_ERROR_MESSAGE.format("customViewParams", "str", type(customViewParams))) 32 | if excludeFields is not None and not isinstance(excludeFields, list): 33 | raise TypeError(constants.TYPE_ERROR_MESSAGE.format("excludeFields", "list", type(excludeFields))) 34 | if methodFields is not None and not isinstance(methodFields, list): 35 | raise TypeError(constants.TYPE_ERROR_MESSAGE.format("methodFields", "list", type(methodFields))) 36 | def decorator_restifyModel(cls): 37 | """ 38 | The decorator function that does the registry/marking. 39 | """ 40 | if not issubclass(cls, models.Model): 41 | raise exceptions.DecoratorException(constants.NOT_A_MODEL_CLASS_MSG) 42 | else: 43 | options = None 44 | try: 45 | options = cfg.djangoToRestRegistry[cls._meta.label] 46 | except KeyError: 47 | logging.info("todjango.decorators.restifyModel.decorator_restifyModel:: Performing registration for :" + cls.__name__) 48 | options = defaultdict(None) 49 | options[constants.CUSTOM_VIEW_PARAMS] = customViewParams 50 | options[constants.EXCLUDE_FIELDS] = excludeFields 51 | options[constants.METHOD_FIELDS] = methodFields 52 | #options[constants.CUSTOM_SERIALIZER] = None if customViewParams is None else customViewParams.pop(constants.SERIALIZER_CLASS, None) 53 | cfg.djangoToRestRegistry[cls._meta.label] = options 54 | return cls 55 | 56 | if _cls is None: 57 | return decorator_restifyModel 58 | else: 59 | return decorator_restifyModel(_cls) -------------------------------------------------------------------------------- /to_rest/exceptions.py: -------------------------------------------------------------------------------- 1 | class DecoratorException(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /to_rest/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from to_rest import constants 3 | from django.urls import reverse 4 | from django.db.models.fields.related import OneToOneField, ForeignKey, ManyToManyField 5 | from django.db.models.fields.reverse_related import OneToOneRel, ManyToOneRel, ManyToManyRel 6 | from to_rest import cfg 7 | 8 | def createModelSerializers(model, excludedFields, methodFields): 9 | """ 10 | Method to create model serializers for the models marked for restification. 11 | 12 | Parameters: 13 | 14 | model (django.db.models.Model): The model itself 15 | 16 | excludedFields (list): The list of fields to be excluded 17 | 18 | requiredReverseRelFields (list): One to One reverse fields to be made required. 19 | 20 | Returns: 21 | 22 | serializer (rest_framework.serializers.ModelSerializer) 23 | """ 24 | 25 | def methodFieldsFactory(methodName): 26 | """ 27 | Method to create seriaizer method for additional method fields 28 | 29 | Parameters: 30 | 31 | methodName (str): name of the method field 32 | 33 | Returns: 34 | 35 | func object 36 | """ 37 | def func(self,object): 38 | temp = eval("object." + methodName + "()") 39 | return temp 40 | func.__name__ = "get_" + methodName 41 | return func 42 | 43 | def relationalMethodFieldsFactory(model, fieldName): 44 | """ 45 | Method to create a serializer method for creating a url for the relational fields 46 | in case of one to many and many to many relation 47 | 48 | Parameters: 49 | 50 | model (django.db.models.Model): model for which the serializer method is created. 51 | 52 | fieldName (str): Name of the relational field 53 | 54 | Returns: 55 | 56 | func object 57 | """ 58 | def func(self,object): 59 | tempName = model.__name__.lower() 60 | return(reverse(model._meta.label.lower().replace('.','_') + "-" + tempName + "-" + fieldName + "-list", args = [object.pk] )) 61 | func.__name__ = "get_" + fieldName 62 | return func 63 | 64 | 65 | fields = [] 66 | relationalFields = [] 67 | for field in model._meta.get_fields(): 68 | if excludedFields is not None and field.name in excludedFields: 69 | continue 70 | elif field.is_relation: 71 | if cfg.djangoToRestRegistry.get(field.related_model._meta.label, False): 72 | relationalFields.append(field) 73 | else: 74 | fields.append(field.name) 75 | if methodFields is not None: 76 | fields.extend(methodFields) 77 | #create meta attributes for the meta class 78 | metaAttributes = {} 79 | metaAttributes["model"] = model 80 | 81 | #create attributes for serializer class 82 | serializerAttribute = {} 83 | defaultActions = [] 84 | oneToOneRelations = [] 85 | for relationalField in relationalFields: 86 | if isinstance(relationalField, OneToOneField): 87 | fields.append(relationalField.name) 88 | elif isinstance(relationalField, OneToOneRel): 89 | temp = relationalField.related_name if relationalField.related_name is not None else relationalField.name 90 | fields.append(temp) 91 | oneToOneRelations.append(temp) 92 | elif isinstance(relationalField, ForeignKey): 93 | fields.append(relationalField.name) 94 | elif isinstance(relationalField, ManyToOneRel): 95 | temp = relationalField.related_name if relationalField.related_name is not None else relationalField.name + "_set" 96 | serializerAttribute[temp] = serializers.SerializerMethodField(read_only=True) 97 | serializerAttribute["get_"+temp] = relationalMethodFieldsFactory(model, temp) 98 | fields.append(temp) 99 | defaultActions.append(("oneToManyActionFactory", model, relationalField.related_model._meta.label, relationalField, temp)) 100 | elif isinstance(relationalField, ManyToManyRel): 101 | temp = relationalField.related_name if relationalField.related_name is not None else relationalField.name + "_set" 102 | serializerAttribute[temp] = serializers.SerializerMethodField(read_only=True) 103 | serializerAttribute["get_"+temp] = relationalMethodFieldsFactory(model, temp) 104 | fields.append(temp) 105 | defaultActions.append(("manyToManyActionFactory", model, relationalField,temp)) 106 | elif isinstance(relationalField, ManyToManyField): 107 | temp = relationalField.name 108 | serializerAttribute[temp] = serializers.SerializerMethodField(read_only=True) 109 | serializerAttribute["get_"+temp] = relationalMethodFieldsFactory(model, temp) 110 | fields.append(temp) 111 | defaultActions.append(("manyToManyActionFactory", model, relationalField,temp)) 112 | #in the above if conditions, certain tuples are added to defaultActions to create 113 | #corresponding action for the one to many and many to many fields 114 | cfg.djangoToRestRegistry[model._meta.label][constants.DEFAULT_ACTIONS] = defaultActions 115 | metaAttributes["fields"] = fields 116 | meta = type("Meta", (object,), metaAttributes) 117 | serializerAttribute["Meta"] = meta #create meta class for the serializer class 118 | for each in oneToOneRelations: 119 | serializerAttribute[each] = serializers.PrimaryKeyRelatedField(many=False, read_only=True) 120 | 121 | if methodFields is not None: 122 | for methodField in methodFields: 123 | serializerAttribute[methodField] = serializers.SerializerMethodField(read_only=True) 124 | serializerAttribute["get_" + methodField] = methodFieldsFactory(methodField) 125 | 126 | modelSerializer = type(constants.PROJECT_NAME_PREFIX + model._meta.label.replace('.','_'), (serializers.ModelSerializer,), serializerAttribute) 127 | 128 | return modelSerializer -------------------------------------------------------------------------------- /to_rest/utils.py: -------------------------------------------------------------------------------- 1 | from to_rest import cfg 2 | from django.apps import apps 3 | from to_rest import constants 4 | from to_rest import serializers as restifySerializer 5 | from to_rest import views 6 | from django.urls import path 7 | from rest_framework.viewsets import ModelViewSet 8 | from rest_framework import routers 9 | from abc import ABC, abstractmethod 10 | import importlib 11 | 12 | class ViewParams(ABC): 13 | """ 14 | Abstract class to provide custom view parameters. This class needs to be inherited to provide 15 | custom view params. 16 | """ 17 | 18 | @abstractmethod 19 | def getParams(): 20 | pass 21 | 22 | def restifyApp(relativeUri): 23 | """ 24 | The function to restify an app. This will iterate over the models of the appName and will 25 | create ModelSerializers for the models, will create viewsets and will hook them with 26 | routes 27 | 28 | Prameters: 29 | relativeUri: (string): The relative url for the api 30 | Return: 31 | list of urls (list) 32 | """ 33 | 34 | for entity in cfg.djangoToRestRegistry: 35 | if cfg.djangoToRestRegistry[entity].get(constants.CUSTOM_VIEW_PARAMS,False) and isinstance(cfg.djangoToRestRegistry[entity].get(constants.CUSTOM_VIEW_PARAMS,False), str): 36 | temp = cfg.djangoToRestRegistry[entity][constants.CUSTOM_VIEW_PARAMS] 37 | appName = apps.get_model(entity)._meta.app_label 38 | module = importlib.import_module(appName + '.' + 'view_params') 39 | temp = getattr(module, temp) 40 | customViewParams = temp.getParams() 41 | cfg.djangoToRestRegistry[entity][constants.CUSTOM_SERIALIZER] = customViewParams.pop(constants.SERIALIZER_CLASS, None) 42 | cfg.djangoToRestRegistry[entity][constants.CUSTOM_VIEW_PARAMS] = customViewParams 43 | 44 | for entity in cfg.djangoToRestRegistry: 45 | model = apps.get_model(entity) 46 | customSerializer = cfg.djangoToRestRegistry[entity].get(constants.CUSTOM_SERIALIZER,None) 47 | customViewParams = cfg.djangoToRestRegistry[entity].get(constants.CUSTOM_VIEW_PARAMS,None) 48 | excludedFields = cfg.djangoToRestRegistry[entity].get(constants.EXCLUDE_FIELDS, None) 49 | methodFields = cfg.djangoToRestRegistry[entity].get(constants.METHOD_FIELDS, None) 50 | modelSerializer = None 51 | if customSerializer is None: 52 | modelSerializer = restifySerializer.createModelSerializers(model, excludedFields, methodFields) 53 | cfg.djangoToRestRegistry[entity][constants.DEFAULT_SERIALIZER] = modelSerializer 54 | else: 55 | tempSerializer = restifySerializer.createModelSerializers(model, excludedFields, methodFields) 56 | cfg.djangoToRestRegistry[entity][constants.DEFAULT_SERIALIZER] = tempSerializer 57 | modelSerializer = customSerializer 58 | viewSetAttributes = views.getObjectViewSetAttributes(model, modelSerializer, customViewParams) 59 | cfg.djangoToRestRegistry[entity][constants.VIEW_SET_ATTRIBUTES] = viewSetAttributes 60 | 61 | router1 = routers.DefaultRouter(trailing_slash=True) 62 | router2 = routers.DefaultRouter(trailing_slash=False) 63 | for entity in cfg.djangoToRestRegistry: 64 | model = apps.get_model(entity) 65 | viewSetAttributes = cfg.djangoToRestRegistry[entity][constants.VIEW_SET_ATTRIBUTES] 66 | defaultActions = cfg.djangoToRestRegistry[entity].get(constants.DEFAULT_ACTIONS, None) 67 | if defaultActions is not None: 68 | for defaultAction in defaultActions: 69 | if defaultAction[0] == "oneToManyActionFactory": 70 | serializer = None 71 | customSerializer = cfg.djangoToRestRegistry[defaultAction[2]].get(constants.CUSTOM_SERIALIZER, None) 72 | defaultSerializer = cfg.djangoToRestRegistry[defaultAction[2]][constants.DEFAULT_SERIALIZER] 73 | if customSerializer is None: 74 | serializer = defaultSerializer 75 | else: 76 | serializer = customSerializer 77 | action = views.oneToManyActionFactory(defaultAction[1],serializer,defaultAction[3], defaultAction[4]) 78 | for x in action: 79 | if viewSetAttributes is not None and viewSetAttributes.get(x.__name__, None) is None: 80 | viewSetAttributes[x.__name__] = x 81 | elif defaultAction[0] == "manyToManyActionFactory": 82 | action = views.manyToManyActionFactory(defaultAction[1],defaultAction[2],defaultAction[3]) 83 | for x in action: 84 | if viewSetAttributes is not None and viewSetAttributes.get(x.__name__,None) is None: 85 | viewSetAttributes[x.__name__] = x 86 | 87 | viewSet = type(constants.PROJECT_NAME_PREFIX + entity.replace('.','_') + "ViewSet", (ModelViewSet,), viewSetAttributes) 88 | cfg.djangoToRestRegistry[entity][constants.DEFAULT_VIEW_SET] = viewSet 89 | router1.register(prefix=r'{}/{}'.format(relativeUri, model._meta.label.lower().replace('.','/')), viewset=viewSet, basename=model._meta.label.lower().replace('.','_')) 90 | router2.register(prefix=r'{}/{}'.format(relativeUri, model._meta.label.lower().replace('.','/')), viewset=viewSet, basename=model._meta.label.lower().replace('.','_')) 91 | 92 | return router1.urls + router2.urls -------------------------------------------------------------------------------- /to_rest/views.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | from django.conf import settings 3 | from rest_framework.response import Response 4 | from rest_framework import status 5 | from rest_framework.decorators import action 6 | from rest_framework import serializers 7 | from django.db.models.fields.reverse_related import ManyToManyRel 8 | from django.db.models.fields.related import ForeignKey 9 | from django_filters.rest_framework import DjangoFilterBackend 10 | from rest_framework.filters import SearchFilter, OrderingFilter 11 | from to_rest import constants 12 | from rest_framework.viewsets import ModelViewSet 13 | from to_rest import cfg 14 | from django.shortcuts import get_object_or_404 15 | 16 | REST_FRAMEWORK_SETTINGS = None 17 | try: 18 | REST_FRAMEWORK_SETTINGS = settings.REST_FRAMEWORK 19 | except Exception: 20 | pass 21 | 22 | def listMethodFactory(modelName): 23 | def list(self, request, *args, **kwargs): 24 | objects = self.filter_queryset(self.get_queryset()) 25 | if len(objects) == 0: 26 | headers = {} 27 | headers[constants.CONTENT_MESSAGE] = constants.NO_OBJECT_EXISTS.format("related "+modelName) 28 | return Response(status=status.HTTP_204_NO_CONTENT, headers=headers) 29 | else: 30 | page = self.paginate_queryset(objects) 31 | if page is not None: 32 | serializer = self.get_serializer(page, many=True) 33 | return self.get_paginated_response(serializer.data) 34 | serializer = self.get_serializer(objects, many=True) 35 | return Response(serializer.data) 36 | return list 37 | 38 | def isDefaultSerializer(serializer): 39 | """ 40 | Function to check if a serializer is default or custom. 41 | 42 | Parameters: 43 | serializer (BaseSerializer): the serializer object 44 | 45 | Returns: 46 | boolean 47 | """ 48 | return serializer.__name__.startswith(constants.PROJECT_NAME_PREFIX) 49 | 50 | 51 | def getTempViewSet(queryset, childModel, childSerializer, viewParams, extras): 52 | """ 53 | Function to create temporary views for filtering and permission purposes for actions as 54 | actions do not accept additional filterset_class or filterset_fields. 55 | 56 | Parameters: 57 | queryset: the quesryset for the view 58 | childModel (django.db.models.Model) : the model object 59 | childSerializer (rest_framework.serializers.BaseSerializer) : the serializer 60 | viewParams (dict) : a dictionary for the view attributes 61 | 62 | Returns: 63 | ViewSet 64 | """ 65 | 66 | defaultFilterBackends = [DjangoFilterBackend, SearchFilter, OrderingFilter] 67 | #set defaults 68 | attributes = dict() 69 | attributes[constants.QUERYSET] = queryset 70 | attributes[constants.SERIALIZER_CLASS] = childSerializer 71 | #attributes[constants.FILTER_BACKENDS] = defaultFilterBackends if REST_FRAMEWORK_SETTINGS is None else REST_FRAMEWORK_SETTINGS.get(constants.DEFAULT_FILTER_BACKENDS, defaultFilterBackends) 72 | if REST_FRAMEWORK_SETTINGS is None or REST_FRAMEWORK_SETTINGS.get(constants.DEFAULT_FILTER_BACKENDS, None) is None: 73 | attributes[constants.FILTER_BACKENDS] = defaultFilterBackends 74 | if isDefaultSerializer(childSerializer): 75 | attributes[constants.FILTERSET_FIELDS] = [x.name for x in childModel._meta.get_fields() if (not x.is_relation or x.many_to_one)] 76 | attributes[constants.SEARCH_FIELDS] = [x.name for x in childModel._meta.get_fields() if (not x.is_relation)] #no relational field by default as there could be large number of relational lookups possible 77 | attributes[constants.ORDERING_FIELDS] = [x.name for x in childModel._meta.get_fields() if (not x.is_relation or x.many_to_one)] 78 | #update with custom params 79 | if viewParams is not None: 80 | attributes.update(viewParams) 81 | if attributes.get(constants.FILTERSET_CLASS, False): 82 | if attributes.get(constants.FILTERSET_FIELDS, False): 83 | del attributes[constants.FILTERSET_FIELDS]#remove filterset_filds if filterset_class exists 84 | 85 | if extras is not None and extras.get('create', False): 86 | if not attributes.get('create', False): 87 | attributes['create'] = extras['create'] 88 | 89 | if extras is not None and extras.get('list', False): 90 | if not attributes.get('list', False): 91 | attributes['list'] = extras['list'] 92 | 93 | if extras is not None and extras.get('update', False): 94 | if not attributes.get('update', False): 95 | attributes['update'] = extras['update'] 96 | 97 | if extras is not None and extras.get('get_object', False): 98 | if not attributes.get('get_object', False): 99 | attributes['get_object'] = extras['get_object'] 100 | 101 | return type(constants.PROJECT_NAME_PREFIX + "temp_" + childModel._meta.label.replace('.','_'), (ModelViewSet,), attributes) 102 | 103 | 104 | def oneToManyActionFactory(parentModel,childSerializer, field, relatedName): 105 | """ 106 | Method to create actions for view set for one to many relationship. Creation and updation 107 | of relationship can be handled from the other side of the relationship. Hence, no methods 108 | for put,patch,delete. Since, there are use cases where an element of an entity can be 109 | related to an element of another entity more than once with other additional info. Hence, 110 | having a retreive method makes no sense by default. However, if required, custom actions 111 | can be provided as seen in decorators.py. 112 | 113 | Parameters: 114 | 115 | parentModel (django.db.models.Model): The model in the one to many side. 116 | 117 | childSerializer (rest_framework.serializers.ModelSerializer): Serializer for model in 118 | the other side. 119 | 120 | field (django.db.models.fields.Field): The field involved in the relationship. 121 | 122 | relatedName (str): The related name for the field. 123 | 124 | Returns: 125 | 126 | tuple of methods (tuple) 127 | 128 | """ 129 | childModel = field.related_model 130 | parentModelName = parentModel.__name__ 131 | childCustomViewParams = None 132 | if cfg.djangoToRestRegistry.get(childModel._meta.label, False): 133 | if cfg.djangoToRestRegistry[childModel._meta.label].get(constants.CUSTOM_VIEW_PARAMS, False): 134 | childCustomViewParams = cfg.djangoToRestRegistry[childModel._meta.label][constants.CUSTOM_VIEW_PARAMS] 135 | 136 | 137 | def funcRelatedList(self,request,pk=None, *args, **kwargs): 138 | parentObject = get_object_or_404(parentModel, pk=pk) 139 | childObjects = eval("parentObject.{}.all()".format(relatedName)) 140 | tempViewSet = getTempViewSet(childObjects, childModel, childSerializer, childCustomViewParams, {'list': listMethodFactory(childModel.__name__)}) 141 | tempView = tempViewSet.as_view({'get': 'list'}) 142 | return tempView(request._request,*args,**kwargs) 143 | 144 | funcRelatedList.__name__ = constants.ONE_TO_MANY_LIST_ACTION + relatedName 145 | funcRelatedList = action(detail=True, methods=['get'], url_path=relatedName, url_name=parentModelName.lower() + "-" + relatedName +"-list")(funcRelatedList) 146 | return (funcRelatedList,) 147 | 148 | def manyToManyActionFactory(parentModel, field, relatedName): 149 | """ 150 | Method to create actions for view set for many to many relationship. Since, there are 151 | use cases where an element of an entity can be related to an element of another entity 152 | more than once with other additional info. Hence, having a retreive method makes no 153 | sense by default. Also, for the same reason, for update, partial_update and delete 154 | the primary key of the through model will be used as the path parameter for the nested url. 155 | However, if required, custom actions can be provided as seen in decorators.py. 156 | 157 | Parameters: 158 | 159 | parentModel (django.db.models.Model): The model in the one to many side. 160 | 161 | field (django.db.models.fields.Field): The field involved in the relationship. 162 | 163 | relatedName (str): The related name for the field. 164 | 165 | Returns: 166 | 167 | tuple of methods (tuple) 168 | 169 | """ 170 | 171 | throughModel = eval("parentModel.{}.through".format(relatedName)) 172 | throughModelName = throughModel.__name__ 173 | parentModelName = parentModel.__name__ 174 | metaAttributes = dict() 175 | metaAttributes["model"] = throughModel 176 | metaAttributes["fields"] = [x.name for x in throughModel._meta.get_fields()] 177 | meta = type("Meta", (object,), metaAttributes) 178 | serializerAttribute = {"Meta": meta} 179 | throughSerializer = type(constants.PROJECT_NAME_PREFIX + throughModelName+"Serializer", (serializers.ModelSerializer,), serializerAttribute) 180 | 181 | if cfg.djangoToRestRegistry.get(throughModel._meta.label, False): 182 | if cfg.djangoToRestRegistry[throughModel._meta.label].get(constants.CUSTOM_SERIALIZER, cfg.djangoToRestRegistry[throughModel._meta.label].get(constants.DEFAULT_SERIALIZER, False)): 183 | throughSerializer = cfg.djangoToRestRegistry[throughModel._meta.label].get(constants.CUSTOM_SERIALIZER, cfg.djangoToRestRegistry[throughModel._meta.label][constants.DEFAULT_SERIALIZER]) 184 | 185 | viewParams = None 186 | 187 | if cfg.djangoToRestRegistry.get(throughModel.__name__, False): 188 | if cfg.djangoToRestRegistry[throughModel.__name__].get(constants.CUSTOM_VIEW_PARAMS, False): 189 | viewParams = cfg.djangoToRestRegistry[throughModel.__name__][constants.CUSTOM_VIEW_PARAMS] 190 | 191 | def funcRelatedList(self,request,pk=None, *args,**kwargs): 192 | #Kind of list view 193 | if self.request.method == "GET": 194 | #The listing will be of the through objects as it present more information about the relation when there 195 | # are additional information. 196 | parentObject = get_object_or_404(parentModel, pk=pk) #just to raise 404 197 | filter_param = field.field.m2m_reverse_field_name() + "_" + field.field.m2m_reverse_target_field_name() if isinstance(field,ManyToManyRel) else field.m2m_field_name() + "_" + field.m2m_target_field_name() 198 | throughObjects = eval("throughModel.objects.filter({}=pk)".format(filter_param)) 199 | tempViewSet = getTempViewSet(throughObjects, throughModel, throughSerializer, viewParams, {'list': listMethodFactory(throughModelName)}) 200 | tempView = tempViewSet.as_view({'get': 'list'}) 201 | return tempView(request._request,*args,**kwargs) 202 | 203 | elif self.request.method == "POST": 204 | #To create new relation ship, through objects would be used as it contains prmary key 205 | #of both sides of the relationship 206 | parentObject = get_object_or_404(parentModel, pk=pk) #just to raise 404 207 | providedData = request.data.copy() 208 | parentObjectField = field.field.m2m_reverse_field_name() if isinstance(field,ManyToManyRel) else field.m2m_field_name() 209 | providedData[parentObjectField] = pk 210 | request._full_data = providedData 211 | throughObjects = parentModel.objects.all() 212 | def create(self, request, *args, **kwargs): 213 | serializer = self.get_serializer(data=providedData) 214 | serializer.is_valid(raise_exception=True) 215 | self.perform_create(serializer) 216 | headers = self.get_success_headers(serializer.data) 217 | return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) 218 | tempViewSet = getTempViewSet(throughObjects, throughModel, throughSerializer, viewParams, {'create': create}) 219 | tempView = tempViewSet.as_view({'post': 'create'}) 220 | return tempView(request._request, *args, **kwargs) 221 | else: 222 | return Response(status=status.HTTP_400_BAD_REQUEST) 223 | 224 | 225 | funcRelatedList.__name__ = constants.MANY_TO_MANY_LIST_ACTION + relatedName 226 | funcRelatedList = action(detail=True, methods=['get', 'post'], url_path=relatedName, url_name=parentModelName.lower() + "-" + relatedName +"-list")(funcRelatedList) 227 | 228 | def funcRelatedDetail(self,request,childPk,pk=None,*args,**kwargs): 229 | #kind of detail view 230 | if self.request.method == 'GET': 231 | parentObject = get_object_or_404(parentModel, pk=pk) #just to raise 404 232 | throughObject = get_object_or_404(throughModel, pk=childPk) 233 | def get_object(self): 234 | return throughObject 235 | throughObjects = throughModel.objects.all() 236 | tempViewSet = getTempViewSet(throughObjects, throughModel, throughSerializer, viewParams, {'get_object': get_object}) 237 | tempView = tempViewSet.as_view({'get': 'retrieve'}) 238 | return tempView(request._request, *args, **kwargs) 239 | elif self.request.method == "PUT": 240 | #For, updating a relationship, through object will be used. For, that reason, the primary 241 | #key of the through object will be used as path parameter in the nested url. To get the primary 242 | #key of the through object filter can be used in with the url of list view (GET) 243 | parentObject = get_object_or_404(parentModel, pk=pk) #just to raise 404 244 | parentObjectField = field.field.m2m_reverse_field_name() if isinstance(field,ManyToManyRel) else field.m2m_field_name() 245 | throughObject = get_object_or_404(throughModel, pk=childPk) 246 | providedData = request.data.copy() 247 | providedData[parentObjectField] = pk 248 | def get_object(self): 249 | return throughObject 250 | def update(self, request, *args, **kwargs): 251 | partial = kwargs.pop('partial', False) 252 | instance = self.get_object() 253 | serializer = self.get_serializer(instance, data=providedData, partial=partial) 254 | serializer.is_valid(raise_exception=True) 255 | self.perform_update(serializer) 256 | 257 | if getattr(instance, '_prefetched_objects_cache', None): 258 | # If 'prefetch_related' has been applied to a queryset, we need to 259 | # forcibly invalidate the prefetch cache on the instance. 260 | instance._prefetched_objects_cache = {} 261 | 262 | return Response(serializer.data) 263 | throughObjects = throughModel.objects.all() 264 | tempViewSet = getTempViewSet(throughObjects, throughModel, throughSerializer, viewParams, {'update': update, 'get_object': get_object}) 265 | tempView = tempViewSet.as_view({'put': 'update'}) 266 | return tempView(request._request, *args, **kwargs) 267 | elif self.request.method == "PATCH": 268 | #For, updating a relationship, through object will be used. For, that reason, the primary 269 | #key of the through object will be used as path parameter in the nested url. To get the primary 270 | #key of the through object filter can be used in with the url of list view (GET) 271 | parentObject = get_object_or_404(parentModel, pk=pk) #just to raise 404 272 | parentObjectField = field.field.m2m_reverse_field_name() if isinstance(field,ManyToManyRel) else field.m2m_field_name() 273 | throughObject = get_object_or_404(throughModel, pk=childPk) 274 | providedData = request.data.copy() 275 | providedData[parentObjectField] = pk 276 | def get_object(self): 277 | return throughObject 278 | def update(self, request, *args, **kwargs): 279 | partial = kwargs.pop('partial', False) 280 | instance = self.get_object() 281 | serializer = self.get_serializer(instance, data=providedData, partial=partial) 282 | serializer.is_valid(raise_exception=True) 283 | self.perform_update(serializer) 284 | 285 | if getattr(instance, '_prefetched_objects_cache', None): 286 | # If 'prefetch_related' has been applied to a queryset, we need to 287 | # forcibly invalidate the prefetch cache on the instance. 288 | instance._prefetched_objects_cache = {} 289 | 290 | return Response(serializer.data) 291 | throughObjects = throughModel.objects.all() 292 | tempViewSet = getTempViewSet(throughObjects, throughModel, throughSerializer, viewParams, {'update': update, 'get_object': get_object}) 293 | tempView = tempViewSet.as_view({'patch': 'partial_update'}) 294 | return tempView(request._request, *args, **kwargs) 295 | elif self.request.method == "DELETE": 296 | #For, deleting a relationship, through object will be used. For, that reason, the primary 297 | #key of the through object will be used as path parameter in the nested url. To get the primary 298 | #key of the through object filter can be used in with the url of list view (GET) 299 | parentObject = get_object_or_404(parentModel, pk=pk) #just to raise 404 300 | throughObject = get_object_or_404(throughModel, pk=childPk) 301 | def get_object(self): 302 | return throughObject 303 | throughObjects = throughModel.objects.all() 304 | tempViewSet = getTempViewSet(throughObjects, throughModel, throughSerializer, viewParams, {'get_object': get_object}) 305 | tempView = tempViewSet.as_view({'delete': 'destroy'}) 306 | return tempView(request._request, *args, **kwargs) 307 | # self.perform_destroy(throughObject) 308 | # return Response(status=status.HTTP_204_NO_CONTENT) 309 | else: 310 | return Response(status=status.HTTP_400_BAD_REQUEST) 311 | 312 | funcRelatedDetail.__name__ = constants.MANY_TO_MANY_DETAIL_ACTION + relatedName 313 | funcRelatedDetail = action(detail=True, methods=['get','put','patch','delete'], url_path=relatedName + "/(?P.+)", url_name=parentModelName.lower() + "-" + relatedName +"-detail")(funcRelatedDetail) 314 | 315 | return (funcRelatedList, funcRelatedDetail) 316 | 317 | def getObjectViewSetAttributes(model, modelSerializer, customViewParams): 318 | """ 319 | Method to create all the attributes for view set. 320 | 321 | Parameters: 322 | 323 | model (django.db.models.Model) : The model for which the Viewset needs to be created 324 | 325 | modelSerializer (rest_framework.serialzers.ModelSerializer) : Corresponding model serializer fo the model. 326 | 327 | customViewParams (dict) : Dictionary of custom view attributes. 328 | 329 | Returns: 330 | 331 | attributes (dict): dictionary of view attributes 332 | """ 333 | viewClassName = model.__name__ + "ViewSet" 334 | def list(self, request, *args, **kwargs): 335 | objects = self.filter_queryset(self.get_queryset()) 336 | if len(objects) == 0: 337 | headers = {} 338 | headers[constants.CONTENT_MESSAGE] = constants.NO_OBJECT_EXISTS.format(model.__name__) 339 | return Response(status=status.HTTP_204_NO_CONTENT, headers=headers) 340 | else: 341 | page = self.paginate_queryset(objects) 342 | if page is not None: 343 | serializer = self.get_serializer(page, many=True) 344 | return self.get_paginated_response(serializer.data) 345 | serializer = self.get_serializer(objects, many=True) 346 | return Response(serializer.data) 347 | 348 | attributes = {} 349 | #add defaults 350 | defaultFilterBackends = [DjangoFilterBackend, SearchFilter, OrderingFilter] 351 | attributes["queryset"] = model.objects.all() 352 | attributes["serializer_class"] = modelSerializer 353 | attributes["list"] = list 354 | #attributes[constants.FILTER_BACKENDS] = defaultFilterBackends if REST_FRAMEWORK_SETTINGS is None else REST_FRAMEWORK_SETTINGS.get(constants.DEFAULT_FILTER_BACKENDS, defaultFilterBackends) 355 | if REST_FRAMEWORK_SETTINGS is None or REST_FRAMEWORK_SETTINGS.get(constants.DEFAULT_FILTER_BACKENDS, None) is None: 356 | attributes[constants.FILTER_BACKENDS] = defaultFilterBackends 357 | if isDefaultSerializer(modelSerializer): 358 | attributes[constants.FILTERSET_FIELDS] = [x.name for x in model._meta.get_fields() if (not x.is_relation or x.many_to_one)] 359 | attributes[constants.SEARCH_FIELDS] = [x.name for x in model._meta.get_fields() if (not x.is_relation)] #no relational field by default as there could be large number of relational lookups possible 360 | attributes[constants.ORDERING_FIELDS] = [x.name for x in model._meta.get_fields() if (not x.is_relation or x.many_to_one)] 361 | #update with custom params 362 | if customViewParams is not None: 363 | attributes.update(customViewParams) 364 | if attributes.get(constants.FILTERSET_CLASS, False): 365 | if attributes.get(constants.FILTERSET_FIELDS, False): 366 | del attributes[constants.FILTERSET_FIELDS]#remove filterset_filds if filterset_class exists 367 | 368 | return attributes --------------------------------------------------------------------------------