├── .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 | [](https://badge.fury.io/py/django-to-rest) 
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 | { 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
--------------------------------------------------------------------------------