drf-query-method 0.1.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/.github/dependabot.yml +5 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/.github/workflows/ci.yml +3 -3
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/.github/workflows/codeql.yml +2 -2
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/.github/workflows/publish.yml +2 -2
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/.github/workflows/zizmor.yml +1 -1
- drf_query_method-0.3.0/CHANGELOG/0.2.0.md +10 -0
- drf_query_method-0.3.0/CHANGELOG/0.3.0.md +11 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/PKG-INFO +18 -6
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/README.md +17 -5
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/docs/usage.md +38 -6
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/pyproject.toml +3 -2
- drf_query_method-0.3.0/src/drf_query_method/methods.py +47 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/src/drf_query_method/params.py +9 -1
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/src/drf_query_method/patching.py +26 -3
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/tests/settings.py +7 -0
- drf_query_method-0.3.0/tests/test_csrf.py +39 -0
- drf_query_method-0.3.0/tests/test_datatables.py +94 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/tests/urls.py +2 -1
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/tests/views.py +13 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/uv.lock +17 -1
- drf_query_method-0.1.0/src/drf_query_method/methods.py +0 -16
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/.editorconfig +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/.github/pull_request_template.md +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/.github/workflows/docs.yml +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/.gitignore +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/CHANGELOG/0.1.0.md +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/CHANGELOG/unreleased.md +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/CODE_OF_CONDUCT.md +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/CONTRIBUTING.md +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/LICENSE +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/SECURITY.md +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/docs/api.md +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/docs/index.md +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/docs/installation.md +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/justfile +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/scripts/release.py +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/src/drf_query_method/__init__.py +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/src/drf_query_method/django_filters.py +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/src/drf_query_method/filters.py +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/src/drf_query_method/permissions.py +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/src/drf_query_method/py.typed +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/src/drf_query_method/routers.py +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/src/drf_query_method/test.py +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/src/drf_query_method/viewsets.py +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/tests/__init__.py +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/tests/conftest.py +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/tests/test_django_filters.py +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/tests/test_drf_query_method.py +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/tests/test_filters.py +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/tests/test_methods.py +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/tests/test_params.py +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/tests/test_patching.py +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/tests/test_permissions.py +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/tests/test_routers.py +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/tests/test_test.py +0 -0
- {drf_query_method-0.1.0 → drf_query_method-0.3.0}/zensical.toml +0 -0
|
@@ -96,7 +96,7 @@ jobs:
|
|
|
96
96
|
drf_req="djangorestframework~=$DRF.0"
|
|
97
97
|
fi
|
|
98
98
|
uv venv --python "$PYTHON"
|
|
99
|
-
uv pip install -e . "$django_req" "$drf_req" django-filter coverage pytest pytest-django
|
|
99
|
+
uv pip install -e . "$django_req" "$drf_req" django-filter djangorestframework-datatables coverage pytest pytest-django
|
|
100
100
|
|
|
101
101
|
- name: Run tests with coverage
|
|
102
102
|
run: .venv/bin/coverage run -m pytest
|
|
@@ -122,7 +122,7 @@ jobs:
|
|
|
122
122
|
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
123
123
|
|
|
124
124
|
- name: Download coverage data
|
|
125
|
-
uses: actions/download-artifact@
|
|
125
|
+
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
126
126
|
with:
|
|
127
127
|
pattern: coverage-*
|
|
128
128
|
merge-multiple: true
|
|
@@ -144,6 +144,6 @@ jobs:
|
|
|
144
144
|
runs-on: ubuntu-latest
|
|
145
145
|
steps:
|
|
146
146
|
- name: Check job results
|
|
147
|
-
uses: re-actors/alls-green@
|
|
147
|
+
uses: re-actors/alls-green@b5b5b37504aa4183270bd3d855c52a67f212be35 # v1.3.0
|
|
148
148
|
with:
|
|
149
149
|
jobs: ${{ toJSON(needs) }}
|
|
@@ -25,11 +25,11 @@ jobs:
|
|
|
25
25
|
persist-credentials: false
|
|
26
26
|
|
|
27
27
|
- name: Initialize CodeQL
|
|
28
|
-
uses: github/codeql-action/init@
|
|
28
|
+
uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
|
29
29
|
with:
|
|
30
30
|
languages: python # Add c-cpp, go, java-kotlin, or swift if needed
|
|
31
31
|
build-mode: none # Switch to autobuild for compiled languages
|
|
32
32
|
queries: security-extended
|
|
33
33
|
|
|
34
34
|
- name: Perform CodeQL Analysis
|
|
35
|
-
uses: github/codeql-action/analyze@
|
|
35
|
+
uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
|
@@ -50,13 +50,13 @@ jobs:
|
|
|
50
50
|
url: https://pypi.org/p/drf-query-method
|
|
51
51
|
steps:
|
|
52
52
|
- name: Download distribution
|
|
53
|
-
uses: actions/download-artifact@
|
|
53
|
+
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
54
54
|
with:
|
|
55
55
|
name: dist
|
|
56
56
|
path: dist/
|
|
57
57
|
|
|
58
58
|
- name: Attest build provenance
|
|
59
|
-
uses: actions/attest-build-provenance@
|
|
59
|
+
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
|
|
60
60
|
with:
|
|
61
61
|
subject-path: "dist/*"
|
|
62
62
|
|
|
@@ -31,7 +31,7 @@ jobs:
|
|
|
31
31
|
persist-credentials: false
|
|
32
32
|
|
|
33
33
|
- name: Run zizmor
|
|
34
|
-
uses: zizmorcore/zizmor-action@
|
|
34
|
+
uses: zizmorcore/zizmor-action@53290badfa369cb0bf7ad16342cb62c2fa47ac7b # v0.8.0
|
|
35
35
|
with:
|
|
36
36
|
advanced-security: ${{ github.event.repository.private == false || vars.CODE_SECURITY_ENABLED == 'true' }}
|
|
37
37
|
inputs: ./.github/
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# drf-query-method 0.2.0
|
|
2
|
+
|
|
3
|
+
## Added
|
|
4
|
+
|
|
5
|
+
- `patch()` makes [django-rest-framework-datatables](https://github.com/izimobil/django-rest-framework-datatables) read
|
|
6
|
+
`QUERY` request bodies, so server-side DataTables can send their parameters as a `QUERY` body.
|
|
7
|
+
|
|
8
|
+
## Changed
|
|
9
|
+
|
|
10
|
+
- `filter_params()` caches its result on the request.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# drf-query-method 0.3.0
|
|
2
|
+
|
|
3
|
+
## Changed
|
|
4
|
+
|
|
5
|
+
- On Django 4.2 to 6.1, `add_query_method()` (and so `patch()`) now treats `QUERY` as a safe method in Django's
|
|
6
|
+
`CsrfViewMiddleware`, matching Django 6.2. `QUERY` requests no longer need a CSRF token on any supported version,
|
|
7
|
+
including session-authenticated requests through DRF's `SessionAuthentication`.
|
|
8
|
+
|
|
9
|
+
## Documentation
|
|
10
|
+
|
|
11
|
+
- README and usage docs explain CSRF for `QUERY`, and the README no longer says the package is unreleased.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: drf-query-method
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: HTTP QUERY method (RFC 10008) support for Django REST framework.
|
|
5
5
|
Project-URL: bugs, https://github.com/BenA-SA/drf-query-method/issues
|
|
6
6
|
Project-URL: changelog, https://github.com/BenA-SA/drf-query-method/releases
|
|
@@ -73,19 +73,31 @@ class NoteViewSet(ModelViewSet):
|
|
|
73
73
|
return self.list(request, *args, **kwargs)
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
Routers then send `QUERY /notes/` to `query()`, and `SearchFilter`, `OrderingFilter
|
|
77
|
-
`DjangoFilterBackend` read their parameters from the request body. If you'd rather not patch DRF, import drop-in
|
|
76
|
+
Routers then send `QUERY /notes/` to `query()`, and `SearchFilter`, `OrderingFilter`, django-filter's
|
|
77
|
+
`DjangoFilterBackend` and django-rest-framework-datatables read their parameters from the request body. If you'd rather not patch DRF, import drop-in
|
|
78
78
|
classes with the same names from `drf_query_method` instead. See the [documentation](https://BenA-SA.github.io/drf-query-method/usage/)
|
|
79
79
|
for both approaches, and for sending `QUERY` requests from JavaScript and other clients.
|
|
80
80
|
|
|
81
|
-
The package is in early development
|
|
81
|
+
The package is in early development. Releases are on [PyPI](https://pypi.org/project/drf-query-method/).
|
|
82
|
+
|
|
83
|
+
## CSRF
|
|
84
|
+
|
|
85
|
+
`QUERY` is a safe method, so it never needs a CSRF token, on any supported Django version. Django 6.2 exempts it from
|
|
86
|
+
CSRF checks itself. On Django 4.2 to 6.1, where this package adds `QUERY`, it also teaches Django's
|
|
87
|
+
`CsrfViewMiddleware` to treat `QUERY` as safe, so the behaviour matches 6.2. DRF's `SessionAuthentication` uses the same
|
|
88
|
+
check, so session-authenticated `QUERY` requests work without a token too.
|
|
89
|
+
|
|
90
|
+
The trade-off is the one `GET` has always had: a `query()` handler must never change data. Cross-site requests are
|
|
91
|
+
still guarded by CORS, because browsers can only send `QUERY` from JavaScript, and `QUERY` isn't a CORS-safelisted
|
|
92
|
+
method, so a cross-origin request needs your server's permission first.
|
|
82
93
|
|
|
83
94
|
## Compatibility
|
|
84
95
|
|
|
85
96
|
* Python 3.10+
|
|
86
|
-
* Django 4.2, 5.0, 5.1, 5.2, 6.0 and 6.1. Django 6.2 adds `QUERY` natively; on earlier versions this package adds it
|
|
97
|
+
* Django 4.2, 5.0, 5.1, 5.2, 6.0 and 6.1. Django 6.2 adds `QUERY` natively; on earlier versions this package adds it,
|
|
98
|
+
including Django 6.2's CSRF behaviour.
|
|
87
99
|
* Django REST framework 3.16+
|
|
88
|
-
* django-filter, optionally
|
|
100
|
+
* django-filter and django-rest-framework-datatables, optionally
|
|
89
101
|
|
|
90
102
|
## Installation
|
|
91
103
|
|
|
@@ -35,19 +35,31 @@ class NoteViewSet(ModelViewSet):
|
|
|
35
35
|
return self.list(request, *args, **kwargs)
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
Routers then send `QUERY /notes/` to `query()`, and `SearchFilter`, `OrderingFilter
|
|
39
|
-
`DjangoFilterBackend` read their parameters from the request body. If you'd rather not patch DRF, import drop-in
|
|
38
|
+
Routers then send `QUERY /notes/` to `query()`, and `SearchFilter`, `OrderingFilter`, django-filter's
|
|
39
|
+
`DjangoFilterBackend` and django-rest-framework-datatables read their parameters from the request body. If you'd rather not patch DRF, import drop-in
|
|
40
40
|
classes with the same names from `drf_query_method` instead. See the [documentation](https://BenA-SA.github.io/drf-query-method/usage/)
|
|
41
41
|
for both approaches, and for sending `QUERY` requests from JavaScript and other clients.
|
|
42
42
|
|
|
43
|
-
The package is in early development
|
|
43
|
+
The package is in early development. Releases are on [PyPI](https://pypi.org/project/drf-query-method/).
|
|
44
|
+
|
|
45
|
+
## CSRF
|
|
46
|
+
|
|
47
|
+
`QUERY` is a safe method, so it never needs a CSRF token, on any supported Django version. Django 6.2 exempts it from
|
|
48
|
+
CSRF checks itself. On Django 4.2 to 6.1, where this package adds `QUERY`, it also teaches Django's
|
|
49
|
+
`CsrfViewMiddleware` to treat `QUERY` as safe, so the behaviour matches 6.2. DRF's `SessionAuthentication` uses the same
|
|
50
|
+
check, so session-authenticated `QUERY` requests work without a token too.
|
|
51
|
+
|
|
52
|
+
The trade-off is the one `GET` has always had: a `query()` handler must never change data. Cross-site requests are
|
|
53
|
+
still guarded by CORS, because browsers can only send `QUERY` from JavaScript, and `QUERY` isn't a CORS-safelisted
|
|
54
|
+
method, so a cross-origin request needs your server's permission first.
|
|
44
55
|
|
|
45
56
|
## Compatibility
|
|
46
57
|
|
|
47
58
|
* Python 3.10+
|
|
48
|
-
* Django 4.2, 5.0, 5.1, 5.2, 6.0 and 6.1. Django 6.2 adds `QUERY` natively; on earlier versions this package adds it
|
|
59
|
+
* Django 4.2, 5.0, 5.1, 5.2, 6.0 and 6.1. Django 6.2 adds `QUERY` natively; on earlier versions this package adds it,
|
|
60
|
+
including Django 6.2's CSRF behaviour.
|
|
49
61
|
* Django REST framework 3.16+
|
|
50
|
-
* django-filter, optionally
|
|
62
|
+
* django-filter and django-rest-framework-datatables, optionally
|
|
51
63
|
|
|
52
64
|
## Installation
|
|
53
65
|
|
|
@@ -57,6 +57,8 @@ were merged:
|
|
|
57
57
|
* DRF's deprecated OpenAPI generator leaves `QUERY` out, as OpenAPI 3.0 has no query operation.
|
|
58
58
|
* If [django-filter](https://github.com/carltongibson/django-filter) is installed, `DjangoFilterBackend` reads the
|
|
59
59
|
request body too.
|
|
60
|
+
* If [django-rest-framework-datatables](https://github.com/izimobil/django-rest-framework-datatables) is installed, its
|
|
61
|
+
filter backends, pagination and renderer read the request body too. See [DataTables](#datatables) below.
|
|
60
62
|
|
|
61
63
|
`patch()` is safe to call more than once, and leaves DRF alone once DRF supports `QUERY` itself.
|
|
62
64
|
|
|
@@ -73,6 +75,8 @@ Every class has the same name as DRF's (or django-filter's), so you change the i
|
|
|
73
75
|
| `rest_framework.test.APIRequestFactory`, `APIClient`, `APITestCase` and friends | `drf_query_method.test` |
|
|
74
76
|
| `django_filters.rest_framework.DjangoFilterBackend` | `drf_query_method.django_filters` |
|
|
75
77
|
|
|
78
|
+
django-rest-framework-datatables is supported through `patch()` only.
|
|
79
|
+
|
|
76
80
|
Use the router and viewset classes together: the viewsets stop a `query()` method answering `QUERY` on detail routes.
|
|
77
81
|
|
|
78
82
|
On Django versions before 6.2, also call `drf_query_method.add_query_method()` at startup (`patch()` does this for you).
|
|
@@ -131,19 +135,47 @@ A few things to check before relying on `QUERY` in production:
|
|
|
131
135
|
CORS_ALLOW_METHODS = (*default_methods, "QUERY")
|
|
132
136
|
```
|
|
133
137
|
|
|
134
|
-
* **CSRF.**
|
|
135
|
-
|
|
138
|
+
* **CSRF.** None needed: `QUERY` is a safe method, so it never needs a CSRF token on any supported Django version. See
|
|
139
|
+
[Django versions before 6.2](#django-versions-before-62) for how older versions get the same behaviour.
|
|
136
140
|
* **Proxies, CDNs and firewalls.** Some reject request methods they don't recognise. Check that `QUERY` reaches your
|
|
137
141
|
application through everything in front of it.
|
|
138
142
|
* **Caching.** `QUERY` responses are cacheable in principle, but caches generally don't key on request bodies, and
|
|
139
143
|
Django's `cache_page` ignores `QUERY` from 6.2. Don't expect responses to be cached.
|
|
140
|
-
|
|
141
|
-
|
|
144
|
+
## DataTables
|
|
145
|
+
|
|
146
|
+
With `patch()` and [django-rest-framework-datatables](https://github.com/izimobil/django-rest-framework-datatables),
|
|
147
|
+
a server-side DataTable can send its parameters as a `QUERY` body instead of a long query string. Keep `format=datatables`
|
|
148
|
+
in the URL, and set the Ajax type:
|
|
149
|
+
|
|
150
|
+
```javascript
|
|
151
|
+
new DataTable("#notes", {
|
|
152
|
+
serverSide: true,
|
|
153
|
+
ajax: {url: "/api/notes/?format=datatables", type: "QUERY"},
|
|
154
|
+
columns: [{data: "title"}, {data: "created"}],
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
jQuery sends DataTables' parameters as a form-encoded body for any method other than `GET` and `HEAD`, which is the
|
|
159
|
+
form django-rest-framework-datatables expects. The view needs a `query()` handler, as above. Its `?keep=` parameter is
|
|
160
|
+
still read from the URL.
|
|
142
161
|
|
|
143
162
|
## Django versions before 6.2
|
|
144
163
|
|
|
145
|
-
Django 6.2 supports `QUERY` natively. On
|
|
146
|
-
|
|
164
|
+
Django 6.2 supports `QUERY` natively. On Django 4.2 to 6.1, `add_query_method()` (which `patch()` calls) brings the same
|
|
165
|
+
behaviour to older versions in two ways:
|
|
166
|
+
|
|
167
|
+
* It adds `query` to Django's allowed view methods, so views can define a `query()` handler.
|
|
168
|
+
* It treats `QUERY` as a safe method in `CsrfViewMiddleware`, as Django 6.2 does. Before 6.2 the middleware only
|
|
169
|
+
exempts `GET`, `HEAD`, `OPTIONS` and `TRACE`, so without this, session-authenticated `QUERY` requests would need a
|
|
170
|
+
CSRF token. DRF's `SessionAuthentication` checks CSRF through the same middleware, so it follows suit.
|
|
171
|
+
|
|
172
|
+
The result is that `QUERY` never needs a CSRF token, whichever supported Django version you're on. If you call
|
|
173
|
+
`add_query_method()` yourself rather than `patch()`, call it once at startup, before requests are served.
|
|
174
|
+
|
|
175
|
+
This is safe for the same reason it is in Django 6.2. CSRF protects against another site making a logged-in user's
|
|
176
|
+
browser send a request. HTML forms can only send `GET` and `POST`, so `QUERY` can only come from JavaScript, and
|
|
177
|
+
because `QUERY` is not a CORS-safelisted method, the browser asks your server's permission with a preflight request
|
|
178
|
+
before sending a cross-origin one. As with `GET`, a `query()` handler must never change data.
|
|
147
179
|
|
|
148
180
|
## Moving to DRF's own support
|
|
149
181
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "drf-query-method"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "HTTP QUERY method (RFC 10008) support for Django REST framework."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
@@ -54,6 +54,7 @@ lint = [
|
|
|
54
54
|
test = [
|
|
55
55
|
"coverage",
|
|
56
56
|
"django-filter",
|
|
57
|
+
"djangorestframework-datatables",
|
|
57
58
|
"pytest",
|
|
58
59
|
"pytest-django",
|
|
59
60
|
]
|
|
@@ -81,7 +82,7 @@ homepage = "https://github.com/BenA-SA/drf-query-method"
|
|
|
81
82
|
|
|
82
83
|
[[tool.ty.overrides]]
|
|
83
84
|
# These tests use attributes that patch() adds to DRF at runtime.
|
|
84
|
-
include = ["tests/test_patching.py"]
|
|
85
|
+
include = ["tests/test_patching.py", "tests/test_datatables.py"]
|
|
85
86
|
rules = { unresolved-attribute = "ignore" }
|
|
86
87
|
|
|
87
88
|
[tool.ruff]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Make Django accept the QUERY method on versions before 6.2."""
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
|
|
5
|
+
from django.middleware.csrf import CsrfViewMiddleware
|
|
6
|
+
from django.views.generic import View
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def add_query_method() -> None:
|
|
10
|
+
"""Make Django before 6.2 handle ``QUERY`` as Django 6.2 does.
|
|
11
|
+
|
|
12
|
+
Django 6.2 supports ``QUERY`` natively, so this does nothing there. On
|
|
13
|
+
earlier versions it adds ``query`` to ``View.http_method_names``, so any
|
|
14
|
+
view, including DRF's, can dispatch ``QUERY`` requests to a ``query()``
|
|
15
|
+
handler, and it treats ``QUERY`` as a safe method in
|
|
16
|
+
``CsrfViewMiddleware``, so ``QUERY`` requests need no CSRF token. See
|
|
17
|
+
:func:`skip_csrf_for_query`.
|
|
18
|
+
"""
|
|
19
|
+
if "query" in View.http_method_names:
|
|
20
|
+
return
|
|
21
|
+
View.http_method_names = [*View.http_method_names, "query"]
|
|
22
|
+
CsrfViewMiddleware.process_view = skip_csrf_for_query(CsrfViewMiddleware.process_view)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def skip_csrf_for_query(process_view):
|
|
26
|
+
"""Wrap ``CsrfViewMiddleware.process_view`` to accept ``QUERY`` like ``GET``.
|
|
27
|
+
|
|
28
|
+
Django 6.2 adds ``QUERY`` to the methods the middleware exempts as safe
|
|
29
|
+
under RFC 9110. Earlier versions hard-code ``GET``, ``HEAD``, ``OPTIONS``
|
|
30
|
+
and ``TRACE``, so the wrapper sends ``QUERY`` down the same path: a request
|
|
31
|
+
already processed, or a ``csrf_exempt`` view, is left alone, and anything
|
|
32
|
+
else is accepted without a token. Every other method falls through to
|
|
33
|
+
Django unchanged.
|
|
34
|
+
|
|
35
|
+
DRF's ``SessionAuthentication`` checks CSRF through ``CSRFCheck``, a
|
|
36
|
+
``CsrfViewMiddleware`` subclass, so it follows the same rule.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
@functools.wraps(process_view)
|
|
40
|
+
def wrapper(self, request, callback, callback_args, callback_kwargs):
|
|
41
|
+
if request.method != "QUERY":
|
|
42
|
+
return process_view(self, request, callback, callback_args, callback_kwargs)
|
|
43
|
+
if getattr(request, "csrf_processing_done", False) or getattr(callback, "csrf_exempt", False):
|
|
44
|
+
return None
|
|
45
|
+
return self._accept(request)
|
|
46
|
+
|
|
47
|
+
return wrapper
|
|
@@ -6,6 +6,8 @@ from django.http import QueryDict
|
|
|
6
6
|
from django.utils.datastructures import MultiValueDict
|
|
7
7
|
from rest_framework.exceptions import ParseError
|
|
8
8
|
|
|
9
|
+
_CACHE_ATTRIBUTE = "_drf_query_method_filter_params"
|
|
10
|
+
|
|
9
11
|
|
|
10
12
|
def merge_filter_params(request) -> QueryDict:
|
|
11
13
|
"""Return ``request.query_params`` with a QUERY request's body merged over it.
|
|
@@ -17,11 +19,16 @@ def merge_filter_params(request) -> QueryDict:
|
|
|
17
19
|
values, as repeated query-string parameters do.
|
|
18
20
|
|
|
19
21
|
Raises ``ParseError`` (400) when the body is not an object of parameters,
|
|
20
|
-
such as a JSON array.
|
|
22
|
+
such as a JSON array. The result is cached on the request, as packages
|
|
23
|
+
such as django-rest-framework-datatables read many parameters each.
|
|
21
24
|
"""
|
|
22
25
|
if request.method != "QUERY":
|
|
23
26
|
return request.query_params
|
|
24
27
|
|
|
28
|
+
cached = request.__dict__.get(_CACHE_ATTRIBUTE)
|
|
29
|
+
if cached is not None:
|
|
30
|
+
return cached
|
|
31
|
+
|
|
25
32
|
body = request.data
|
|
26
33
|
if not isinstance(body, Mapping):
|
|
27
34
|
raise ParseError(f"QUERY request body must be an object of parameters, not {type(body).__name__}.")
|
|
@@ -32,6 +39,7 @@ def merge_filter_params(request) -> QueryDict:
|
|
|
32
39
|
params = request.query_params.copy()
|
|
33
40
|
for key, values in body.lists():
|
|
34
41
|
params.setlist(key, values)
|
|
42
|
+
request.__dict__[_CACHE_ATTRIBUTE] = params
|
|
35
43
|
return params
|
|
36
44
|
|
|
37
45
|
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
:func:`patch` makes DRF behave as if encode/django-rest-framework#10091 were
|
|
4
4
|
merged, so existing views, routers and settings pick up ``QUERY`` without
|
|
5
|
-
changing imports.
|
|
6
|
-
|
|
5
|
+
changing imports. When django-filter or django-rest-framework-datatables
|
|
6
|
+
is installed, they read QUERY request bodies too. Call it once at startup,
|
|
7
|
+
for example from an ``AppConfig.ready()``. It does nothing to DRF once DRF supports ``QUERY``
|
|
7
8
|
itself.
|
|
8
9
|
"""
|
|
9
10
|
|
|
@@ -78,8 +79,28 @@ def _patch_django_filter() -> None:
|
|
|
78
79
|
)
|
|
79
80
|
|
|
80
81
|
|
|
82
|
+
def _patch_drf_datatables() -> None:
|
|
83
|
+
"""Make django-rest-framework-datatables read QUERY request bodies.
|
|
84
|
+
|
|
85
|
+
Its modules import ``get_param`` by name, so each binding is replaced.
|
|
86
|
+
"""
|
|
87
|
+
from rest_framework_datatables import filters, pagination, renderers, utils
|
|
88
|
+
|
|
89
|
+
from drf_query_method.params import filter_params
|
|
90
|
+
|
|
91
|
+
original = utils.get_param
|
|
92
|
+
|
|
93
|
+
def get_param(request, param, default=None):
|
|
94
|
+
if request.method == "QUERY":
|
|
95
|
+
return filter_params(request).get(param, default)
|
|
96
|
+
return original(request, param, default)
|
|
97
|
+
|
|
98
|
+
for module in (utils, filters, pagination, renderers):
|
|
99
|
+
_set(module, "get_param", get_param)
|
|
100
|
+
|
|
101
|
+
|
|
81
102
|
def patch() -> None:
|
|
82
|
-
"""Add QUERY support to Django, DRF and django-
|
|
103
|
+
"""Add QUERY support to Django, DRF, django-filter and django-rest-framework-datatables in place.
|
|
83
104
|
|
|
84
105
|
Safe to call more than once. On Django versions before 6.2 this also
|
|
85
106
|
calls :func:`~drf_query_method.add_query_method`, which :func:`unpatch`
|
|
@@ -95,6 +116,8 @@ def patch() -> None:
|
|
|
95
116
|
_patch_drf()
|
|
96
117
|
if importlib.util.find_spec("django_filters") is not None:
|
|
97
118
|
_patch_django_filter()
|
|
119
|
+
if importlib.util.find_spec("rest_framework_datatables") is not None:
|
|
120
|
+
_patch_drf_datatables()
|
|
98
121
|
|
|
99
122
|
|
|
100
123
|
def unpatch() -> None:
|
|
@@ -5,12 +5,19 @@ SECRET_KEY = "drf-query-method-tests"
|
|
|
5
5
|
INSTALLED_APPS = [
|
|
6
6
|
"django.contrib.auth",
|
|
7
7
|
"django.contrib.contenttypes",
|
|
8
|
+
"django.contrib.sessions",
|
|
8
9
|
"django_filters",
|
|
9
10
|
"rest_framework",
|
|
10
11
|
]
|
|
11
12
|
|
|
12
13
|
ROOT_URLCONF = "tests.urls"
|
|
13
14
|
|
|
15
|
+
MIDDLEWARE = [
|
|
16
|
+
"django.contrib.sessions.middleware.SessionMiddleware",
|
|
17
|
+
"django.middleware.csrf.CsrfViewMiddleware",
|
|
18
|
+
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
|
19
|
+
]
|
|
20
|
+
|
|
14
21
|
DATABASES = {
|
|
15
22
|
"default": {
|
|
16
23
|
"ENGINE": "django.db.backends.sqlite3",
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from django.contrib.auth.models import User
|
|
3
|
+
from django.http import HttpResponse
|
|
4
|
+
from django.middleware.csrf import CsrfViewMiddleware
|
|
5
|
+
from django.test import RequestFactory
|
|
6
|
+
|
|
7
|
+
from drf_query_method import add_query_method
|
|
8
|
+
from drf_query_method.test import APIClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def client(db):
|
|
13
|
+
client = APIClient(enforce_csrf_checks=True)
|
|
14
|
+
client.force_login(User.objects.create(username="alice"))
|
|
15
|
+
return client
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_session_authenticated_query_needs_no_csrf_token(client):
|
|
19
|
+
response = client.query("/session/", {"search": "a"}, format="json")
|
|
20
|
+
assert response.status_code == 200
|
|
21
|
+
assert response.data == {"method": "QUERY"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_session_authenticated_post_still_needs_a_csrf_token(client):
|
|
25
|
+
assert client.post("/session/", {}, format="json").status_code == 403
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_csrf_middleware_accepts_query_as_a_safe_method():
|
|
29
|
+
middleware = CsrfViewMiddleware(lambda request: HttpResponse())
|
|
30
|
+
request = RequestFactory().generic("QUERY", "/")
|
|
31
|
+
assert middleware.process_view(request, lambda request: HttpResponse(), (), {}) is None
|
|
32
|
+
assert request.csrf_processing_done
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_add_query_method_wraps_the_middleware_once():
|
|
36
|
+
add_query_method()
|
|
37
|
+
add_query_method()
|
|
38
|
+
process_view = CsrfViewMiddleware.process_view
|
|
39
|
+
assert not hasattr(getattr(process_view, "__wrapped__", None), "__wrapped__")
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from urllib.parse import urlencode
|
|
2
|
+
|
|
3
|
+
import django_filters
|
|
4
|
+
import pytest
|
|
5
|
+
from django.contrib.auth.models import User
|
|
6
|
+
from rest_framework import generics
|
|
7
|
+
from rest_framework.renderers import JSONRenderer
|
|
8
|
+
from rest_framework.test import APIRequestFactory
|
|
9
|
+
from rest_framework_datatables import filters, pagination, renderers
|
|
10
|
+
from rest_framework_datatables.django_filters.backends import DatatablesFilterBackend
|
|
11
|
+
from rest_framework_datatables.django_filters.filters import GlobalFilter
|
|
12
|
+
from rest_framework_datatables.django_filters.filterset import DatatablesFilterSet
|
|
13
|
+
|
|
14
|
+
from tests.views import UserSerializer
|
|
15
|
+
|
|
16
|
+
DATATABLES_PARAMS = {
|
|
17
|
+
"draw": "3",
|
|
18
|
+
"start": "1",
|
|
19
|
+
"length": "1",
|
|
20
|
+
"columns[0][data]": "username",
|
|
21
|
+
"columns[0][name]": "username",
|
|
22
|
+
"columns[0][searchable]": "true",
|
|
23
|
+
"columns[0][orderable]": "true",
|
|
24
|
+
"columns[0][search][value]": "",
|
|
25
|
+
"columns[0][search][regex]": "false",
|
|
26
|
+
"order[0][column]": "0",
|
|
27
|
+
"order[0][dir]": "desc",
|
|
28
|
+
"search[value]": "o",
|
|
29
|
+
"search[regex]": "false",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class GlobalCharFilter(GlobalFilter, django_filters.CharFilter):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class UserGlobalFilterSet(DatatablesFilterSet):
|
|
38
|
+
username = GlobalCharFilter(lookup_expr="icontains")
|
|
39
|
+
|
|
40
|
+
class Meta:
|
|
41
|
+
model = User
|
|
42
|
+
fields = ["username"]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def build_view(filter_backend):
|
|
46
|
+
class View(generics.ListAPIView):
|
|
47
|
+
queryset = User.objects.order_by("pk")
|
|
48
|
+
serializer_class = UserSerializer
|
|
49
|
+
permission_classes = []
|
|
50
|
+
filterset_class = UserGlobalFilterSet
|
|
51
|
+
renderer_classes = [JSONRenderer, renderers.DatatablesRenderer]
|
|
52
|
+
filter_backends = [filter_backend]
|
|
53
|
+
pagination_class = pagination.DatatablesPageNumberPagination
|
|
54
|
+
|
|
55
|
+
def query(self, request, *args, **kwargs):
|
|
56
|
+
return self.list(request, *args, **kwargs)
|
|
57
|
+
|
|
58
|
+
return View.as_view()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.fixture
|
|
62
|
+
def users(db):
|
|
63
|
+
for username in ["alice", "bob", "carol"]:
|
|
64
|
+
User.objects.create(username=username)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@pytest.mark.parametrize("filter_backend", [filters.DatatablesFilterBackend, DatatablesFilterBackend])
|
|
68
|
+
def test_datatables_reads_query_body(patched, users, filter_backend):
|
|
69
|
+
request = APIRequestFactory().query(
|
|
70
|
+
"/?format=datatables", urlencode(DATATABLES_PARAMS), content_type="application/x-www-form-urlencoded"
|
|
71
|
+
)
|
|
72
|
+
response = build_view(filter_backend)(request)
|
|
73
|
+
response.render()
|
|
74
|
+
assert response.data["draw"] == 3
|
|
75
|
+
assert response.data["recordsTotal"] == 3
|
|
76
|
+
assert response.data["recordsFiltered"] == 2
|
|
77
|
+
assert [user["username"] for user in response.data["data"]] == ["bob"]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_datatables_get_is_unchanged(patched, users):
|
|
81
|
+
request = APIRequestFactory().get("/", {**DATATABLES_PARAMS, "format": "datatables"})
|
|
82
|
+
response = build_view(filters.DatatablesFilterBackend)(request)
|
|
83
|
+
assert [user["username"] for user in response.data["data"]] == ["bob"]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_merged_filter_params_are_cached_per_request():
|
|
87
|
+
from rest_framework.parsers import JSONParser
|
|
88
|
+
from rest_framework.request import Request
|
|
89
|
+
|
|
90
|
+
from drf_query_method.params import merge_filter_params
|
|
91
|
+
from drf_query_method.test import APIRequestFactory as QueryRequestFactory
|
|
92
|
+
|
|
93
|
+
request = Request(QueryRequestFactory().query("/", {"a": "1"}, format="json"), parsers=[JSONParser()])
|
|
94
|
+
assert merge_filter_params(request) is merge_filter_params(request)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from django.urls import path
|
|
2
2
|
|
|
3
|
-
from tests.views import QueryEchoView, redirect_view
|
|
3
|
+
from tests.views import QueryEchoView, SessionQueryView, redirect_view
|
|
4
4
|
|
|
5
5
|
urlpatterns = [
|
|
6
6
|
path("query/", QueryEchoView.as_view()),
|
|
7
7
|
path("redirect/", redirect_view),
|
|
8
|
+
path("session/", SessionQueryView.as_view()),
|
|
8
9
|
]
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
from django.contrib.auth.models import User
|
|
4
4
|
from django.http.response import HttpResponseRedirectBase
|
|
5
5
|
from rest_framework import generics, serializers
|
|
6
|
+
from rest_framework.authentication import SessionAuthentication
|
|
6
7
|
from rest_framework.decorators import action, api_view
|
|
8
|
+
from rest_framework.permissions import IsAuthenticated
|
|
7
9
|
from rest_framework.response import Response
|
|
8
10
|
from rest_framework.views import APIView
|
|
9
11
|
|
|
@@ -72,3 +74,14 @@ class QueryActionViewSet(viewsets.GenericViewSet):
|
|
|
72
74
|
@action(detail=False, methods=["post"])
|
|
73
75
|
def query(self, request):
|
|
74
76
|
return Response("action")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class SessionQueryView(APIView):
|
|
80
|
+
authentication_classes = [SessionAuthentication]
|
|
81
|
+
permission_classes = [IsAuthenticated]
|
|
82
|
+
|
|
83
|
+
def query(self, request):
|
|
84
|
+
return Response({"method": request.method})
|
|
85
|
+
|
|
86
|
+
def post(self, request):
|
|
87
|
+
return Response({"method": request.method})
|
|
@@ -408,9 +408,21 @@ wheels = [
|
|
|
408
408
|
{ url = "https://files.pythonhosted.org/packages/c4/dd/de5f4543765bec94e036a5e9593a42f8fa7efec8de78eb56b9ca834385f3/djangorestframework-3.18.3-py3-none-any.whl", hash = "sha256:8544bb674846731b1e3c9b309236ee1dc412905a0aa725be2ec193ca950a7d12", size = 903986, upload-time = "2026-10-06T21:33:49.917Z" },
|
|
409
409
|
]
|
|
410
410
|
|
|
411
|
+
[[package]]
|
|
412
|
+
name = "djangorestframework-datatables"
|
|
413
|
+
version = "0.7.2"
|
|
414
|
+
source = { registry = "https://pypi.org/simple" }
|
|
415
|
+
dependencies = [
|
|
416
|
+
{ name = "djangorestframework" },
|
|
417
|
+
]
|
|
418
|
+
sdist = { url = "https://files.pythonhosted.org/packages/c7/a0/7481fee713387fd5c632b05c1d95c32f5b2ea6e1e8f15c9cf1894e1895a8/djangorestframework_datatables-0.7.2.tar.gz", hash = "sha256:d7953968088ceb14968621a0c0a83d2f39ec1cda24b5ebe1240f3c9619bc7129", size = 19647, upload-time = "2024-06-14T17:11:41.742Z" }
|
|
419
|
+
wheels = [
|
|
420
|
+
{ url = "https://files.pythonhosted.org/packages/f3/25/17cd01982c7299cbca9e20247e73c83bfa53a0ed9e8b7735c915c1bb6675/djangorestframework_datatables-0.7.2-py2.py3-none-any.whl", hash = "sha256:fd5747b1e3d0bfb84713a975394dd4d879b57ee8bc66cc0f97566fa67f4406f4", size = 15303, upload-time = "2024-06-14T17:11:39.658Z" },
|
|
421
|
+
]
|
|
422
|
+
|
|
411
423
|
[[package]]
|
|
412
424
|
name = "drf-query-method"
|
|
413
|
-
version = "0.
|
|
425
|
+
version = "0.3.0"
|
|
414
426
|
source = { editable = "." }
|
|
415
427
|
dependencies = [
|
|
416
428
|
{ name = "django", version = "5.2.18", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
|
|
@@ -430,6 +442,7 @@ dev = [
|
|
|
430
442
|
{ name = "coverage", version = "7.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
|
431
443
|
{ name = "django-filter", version = "26.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
|
432
444
|
{ name = "django-filter", version = "26.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
|
445
|
+
{ name = "djangorestframework-datatables" },
|
|
433
446
|
{ name = "pytest" },
|
|
434
447
|
{ name = "pytest-django" },
|
|
435
448
|
{ name = "ruff" },
|
|
@@ -449,6 +462,7 @@ test = [
|
|
|
449
462
|
{ name = "coverage", version = "7.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
|
450
463
|
{ name = "django-filter", version = "26.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
|
451
464
|
{ name = "django-filter", version = "26.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
|
465
|
+
{ name = "djangorestframework-datatables" },
|
|
452
466
|
{ name = "pytest" },
|
|
453
467
|
{ name = "pytest-django" },
|
|
454
468
|
]
|
|
@@ -468,6 +482,7 @@ provides-extras = ["django-filter"]
|
|
|
468
482
|
dev = [
|
|
469
483
|
{ name = "coverage" },
|
|
470
484
|
{ name = "django-filter" },
|
|
485
|
+
{ name = "djangorestframework-datatables" },
|
|
471
486
|
{ name = "pytest" },
|
|
472
487
|
{ name = "pytest-django" },
|
|
473
488
|
{ name = "ruff" },
|
|
@@ -481,6 +496,7 @@ lint = [{ name = "ruff" }]
|
|
|
481
496
|
test = [
|
|
482
497
|
{ name = "coverage" },
|
|
483
498
|
{ name = "django-filter" },
|
|
499
|
+
{ name = "djangorestframework-datatables" },
|
|
484
500
|
{ name = "pytest" },
|
|
485
501
|
{ name = "pytest-django" },
|
|
486
502
|
]
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
"""Make Django accept the QUERY method on versions before 6.2."""
|
|
2
|
-
|
|
3
|
-
from django.views.generic import View
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def add_query_method() -> None:
|
|
7
|
-
"""Add ``query`` to Django's ``View.http_method_names`` if it is missing.
|
|
8
|
-
|
|
9
|
-
Django 6.2 lists ``query`` itself, so this does nothing there. On earlier
|
|
10
|
-
versions it lets any view, including DRF's, dispatch ``QUERY`` requests to
|
|
11
|
-
a ``query()`` handler. Earlier versions of Django's CSRF middleware treat
|
|
12
|
-
``QUERY`` as unsafe, so session-authenticated ``QUERY`` requests need a
|
|
13
|
-
CSRF token until Django 6.2.
|
|
14
|
-
"""
|
|
15
|
-
if "query" not in View.http_method_names:
|
|
16
|
-
View.http_method_names = [*View.http_method_names, "query"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{drf_query_method-0.1.0 → drf_query_method-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|