django-pfx 1.2.dev38__tar.gz → 1.2.dev42__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.
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/.gitignore +1 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/PKG-INFO +1 -1
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/django_pfx.egg-info/PKG-INFO +1 -1
- django-pfx-1.2.dev42/pfx/pfxcore/decorator/__init__.py +1 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/decorator/rest.py +16 -1
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/management/commands/makeapidoc.py +22 -1
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/test.py +32 -25
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/views/filters_views.py +11 -1
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/views/rest_views.py +95 -1
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/tests/tests/basic_api_test.py +73 -2
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/tests/tests/test_api_doc.py +8 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/tests/views.py +2 -1
- django-pfx-1.2.dev38/pfx/pfxcore/decorator/__init__.py +0 -1
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/.gitlab-ci.yml +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/.pre-commit-config.yaml +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/LICENSE +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/MANIFEST.in +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/README.md +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/django_pfx.egg-info/SOURCES.txt +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/django_pfx.egg-info/dependency_links.txt +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/django_pfx.egg-info/requires.txt +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/django_pfx.egg-info/top_level.txt +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/img/pfx.png +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/img/pfx.svg +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/__init__.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/__init__.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/apps.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/default_settings.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/exceptions.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/fields.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/http/__init__.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/http/json_response.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.po +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/management/__init__.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/management/commands/__init__.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/middleware/__init__.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/middleware/authentication.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/middleware/locale.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/models/__init__.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/models/cache_mixins.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/models/not_null_fields.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/models/pfx_models.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/models/user_filtered_queryset_mixin.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/serializers/__init__.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/serializers/json.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/settings.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/shortcuts.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/storage/__init__.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/storage/s3_storage.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/templates/registration/password_reset_email.txt +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/templates/registration/password_reset_subject.txt +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/templates/registration/welcome_email.txt +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/templates/registration/welcome_subject.txt +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/urls.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/views/__init__.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/views/authentication_views.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/views/fields.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/views/locale_views.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pyproject.toml +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/requirements.txt +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/runtest.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/setup.cfg +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/setup.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/tests/__init__.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/tests/models.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/tests/settings/__init__.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/tests/settings/ci.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/tests/settings/common.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/tests/settings/dev.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/tests/settings/dev_custom_example.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/tests/settings/dev_default.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/tests/tests/__init__.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/tests/tests/basic_api_errors.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/tests/tests/test_auth_api.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/tests/tests/test_cache.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/tests/tests/test_fields.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/tests/tests/test_filters.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/tests/tests/test_locale_api.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/tests/tests/test_perm_tests.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/tests/tests/test_perms_api.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/tests/tests/test_shortcuts.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/tests/tests/test_timezone_middleware.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/tests/tests/test_tools.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/tests/tests/test_user_queryset.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/tests/tests/test_view_decorators.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/tests/tests/test_view_fields.py +0 -0
- {django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/tests/urls.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .rest import rest_api, rest_doc, rest_property, rest_view
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from functools import wraps
|
|
2
3
|
|
|
3
4
|
from django.utils.translation import gettext_lazy as _
|
|
4
5
|
|
|
6
|
+
from apispec.utils import deepupdate
|
|
7
|
+
|
|
5
8
|
from pfx.pfxcore.exceptions import APIError
|
|
6
9
|
from pfx.pfxcore.http import JsonResponse
|
|
7
10
|
|
|
@@ -10,6 +13,7 @@ logger = logging.getLogger(__name__)
|
|
|
10
13
|
|
|
11
14
|
def rest_api(path, method='get', public=None, priority=0):
|
|
12
15
|
def decorator(func):
|
|
16
|
+
@wraps(func)
|
|
13
17
|
def wrapper(self, request, *args, **kwargs):
|
|
14
18
|
self.request = request
|
|
15
19
|
self.kwargs = kwargs
|
|
@@ -43,6 +47,17 @@ def rest_property(string=None, type="CharField"):
|
|
|
43
47
|
|
|
44
48
|
def rest_view(path):
|
|
45
49
|
def decorator(cls):
|
|
46
|
-
cls.rest_view_path = path
|
|
50
|
+
cls.rest_view_path[cls] = path
|
|
51
|
+
return cls
|
|
52
|
+
return decorator
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def rest_doc(path, **vals):
|
|
56
|
+
def decorator(cls):
|
|
57
|
+
if cls not in cls.rest_view_path:
|
|
58
|
+
raise Exception(
|
|
59
|
+
"@rest_doc must be used before a @rest_view decorator")
|
|
60
|
+
cls.rest_doc[f'{cls.rest_view_path[cls]}{path}'] = deepupdate(
|
|
61
|
+
cls.rest_doc.get(path, {}), vals)
|
|
47
62
|
return cls
|
|
48
63
|
return decorator
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+
import inspect
|
|
1
2
|
import json
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
|
|
4
5
|
from django.core.management.base import BaseCommand
|
|
5
6
|
|
|
6
7
|
from apispec import APISpec
|
|
8
|
+
from apispec.utils import deepupdate
|
|
9
|
+
from apispec.yaml_utils import load_operations_from_docstring
|
|
7
10
|
|
|
8
11
|
from pfx.pfxcore import __PFX_VIEWS__
|
|
9
12
|
from pfx.pfxcore.settings import PFXSettings
|
|
13
|
+
from pfx.pfxcore.views import ModelMixin
|
|
10
14
|
|
|
11
15
|
settings = PFXSettings()
|
|
12
16
|
DEFAULT_TEMPLATE = dict(
|
|
@@ -15,13 +19,30 @@ DEFAULT_TEMPLATE = dict(
|
|
|
15
19
|
openapi_version="3.0.2")
|
|
16
20
|
|
|
17
21
|
|
|
22
|
+
def get_operations(view, url):
|
|
23
|
+
for op, method_name in url['methods'].items():
|
|
24
|
+
doc = inspect.getdoc(getattr(view, method_name))
|
|
25
|
+
vars = {}
|
|
26
|
+
if issubclass(view, ModelMixin) and view.model:
|
|
27
|
+
vars.update(
|
|
28
|
+
model=view.model._meta.verbose_name.lower(),
|
|
29
|
+
models=view.model._meta.verbose_name_plural.lower())
|
|
30
|
+
if doc:
|
|
31
|
+
doc = doc.format(**vars)
|
|
32
|
+
spec = deepupdate(
|
|
33
|
+
load_operations_from_docstring(doc).get(op, {}),
|
|
34
|
+
view.rest_doc.get(url['path'], {}))
|
|
35
|
+
spec.setdefault('summary', url['path'])
|
|
36
|
+
yield op, spec
|
|
37
|
+
|
|
38
|
+
|
|
18
39
|
def get_spec():
|
|
19
40
|
spec = APISpec(**{**DEFAULT_TEMPLATE, **settings.PFX_OPENAPI_TEMPLATE})
|
|
20
41
|
for view in __PFX_VIEWS__:
|
|
21
42
|
for url in view.get_urls():
|
|
22
43
|
spec.path(
|
|
23
44
|
path=url['path'],
|
|
24
|
-
operations=
|
|
45
|
+
operations=dict(get_operations(view, url)))
|
|
25
46
|
return spec
|
|
26
47
|
|
|
27
48
|
|
|
@@ -159,57 +159,64 @@ class TestAssertMixin:
|
|
|
159
159
|
msg = '\n'.join([msg or '', response.formatted()])
|
|
160
160
|
return self.assertEqual(response.status_code, code, msg=msg)
|
|
161
161
|
|
|
162
|
-
def
|
|
162
|
+
def _format_source(self, src):
|
|
163
|
+
if isinstance(src, dict):
|
|
164
|
+
return json.dumps(src, indent=2)
|
|
165
|
+
return src.formatted()
|
|
166
|
+
|
|
167
|
+
def _get_dict(self, src):
|
|
168
|
+
return src if isinstance(src, dict) else src.json_content
|
|
169
|
+
|
|
170
|
+
def get_val(self, src, key):
|
|
163
171
|
def _p(k):
|
|
164
172
|
return int(k[1:]) if k[0] == '@' else k
|
|
173
|
+
|
|
165
174
|
try:
|
|
166
|
-
return reduce(
|
|
167
|
-
|
|
175
|
+
return reduce(
|
|
176
|
+
lambda c, k: c[_p(k)], key.split("."), self._get_dict(src))
|
|
168
177
|
except IndexError:
|
|
169
|
-
print(format_response(response, f'Index Error for key "{key}"'))
|
|
170
178
|
raise Exception(f'Index Error for key "{key}"')
|
|
171
179
|
except KeyError:
|
|
172
|
-
print(format_response(response, f'Key Error for key "{key}"'))
|
|
173
180
|
raise Exception(f'Key Error for key "{key}"')
|
|
174
181
|
|
|
175
182
|
# assert JSON content at key equals value
|
|
176
|
-
def assertJE(self,
|
|
177
|
-
msg = '\n'.join([msg or '',
|
|
178
|
-
return self.assertEqual(self.get_val(
|
|
183
|
+
def assertJE(self, src, key, value, msg=None):
|
|
184
|
+
msg = '\n'.join([msg or '', self._format_source(src)])
|
|
185
|
+
return self.assertEqual(self.get_val(src, key), value, msg=msg)
|
|
179
186
|
|
|
180
187
|
# assert JSON content at key not equals value
|
|
181
|
-
def assertNJE(self,
|
|
182
|
-
msg = '\n'.join([msg or '',
|
|
188
|
+
def assertNJE(self, src, key, value, msg=None):
|
|
189
|
+
msg = '\n'.join([msg or '', self._format_source(src)])
|
|
183
190
|
return self.assertNotEqual(
|
|
184
|
-
self.get_val(
|
|
191
|
+
self.get_val(src, key), value, msg=msg)
|
|
185
192
|
|
|
186
193
|
# assert JSON content at key exists
|
|
187
|
-
def assertJEExists(self,
|
|
188
|
-
msg = '\n'.join([msg or '',
|
|
194
|
+
def assertJEExists(self, src, key, msg=None):
|
|
195
|
+
msg = '\n'.join([msg or '', self._format_source(src)])
|
|
189
196
|
if '.' not in key:
|
|
190
|
-
return self.assertIn(key,
|
|
197
|
+
return self.assertIn(key, self._get_dict(src), msg=msg)
|
|
191
198
|
path, name = key.rsplit('.', 1)
|
|
192
|
-
return self.assertIn(name, self.get_val(
|
|
199
|
+
return self.assertIn(name, self.get_val(src, path), msg=msg)
|
|
193
200
|
|
|
194
201
|
# assert JSON content at key not exists
|
|
195
|
-
def assertJENotExists(self,
|
|
196
|
-
msg = '\n'.join([msg or '',
|
|
202
|
+
def assertJENotExists(self, src, key, msg=None):
|
|
203
|
+
msg = '\n'.join([msg or '', self._format_source(src)])
|
|
197
204
|
if '.' not in key:
|
|
198
|
-
return self.assertNotIn(key,
|
|
205
|
+
return self.assertNotIn(key, self._get_dict(src), msg=msg)
|
|
199
206
|
path, name = key.rsplit('.', 1)
|
|
200
|
-
return self.assertNotIn(name, self.get_val(
|
|
207
|
+
return self.assertNotIn(name, self.get_val(src, path), msg=msg)
|
|
201
208
|
|
|
202
209
|
# assert JSON array size
|
|
203
|
-
def assertSize(self,
|
|
204
|
-
msg = '\n'.join([msg or '',
|
|
210
|
+
def assertSize(self, src, key, size, msg=None):
|
|
211
|
+
msg = '\n'.join([msg or '', self._format_source(src)])
|
|
205
212
|
return self.assertEqual(
|
|
206
|
-
len(self.get_val(
|
|
213
|
+
len(self.get_val(src, key)), size, msg=msg)
|
|
207
214
|
|
|
208
215
|
# assert JSON array contains
|
|
209
|
-
def assertJIn(self,
|
|
210
|
-
msg = '\n'.join([msg or '',
|
|
216
|
+
def assertJIn(self, src, key, element, msg=None):
|
|
217
|
+
msg = '\n'.join([msg or '', self._format_source(src)])
|
|
211
218
|
return self.assertIn(
|
|
212
|
-
element, self.get_val(
|
|
219
|
+
element, self.get_val(src, key), msg=msg)
|
|
213
220
|
|
|
214
221
|
|
|
215
222
|
class TestPermsAssertMixin(TestAssertMixin):
|
|
@@ -87,12 +87,22 @@ class ModelFilter(Filter):
|
|
|
87
87
|
self.field = model._meta.get_field(name)
|
|
88
88
|
super().__init__(
|
|
89
89
|
name, label or self.field.verbose_name,
|
|
90
|
-
type or
|
|
90
|
+
type or self._type_from_model,
|
|
91
91
|
filter_func, filter_func_list, choices or self.field.choices,
|
|
92
92
|
related_model or (
|
|
93
93
|
self.field.remote_field and
|
|
94
94
|
self.field.remote_field.model.__name__), technical=technical)
|
|
95
95
|
|
|
96
|
+
@property
|
|
97
|
+
def _type_from_model(self):
|
|
98
|
+
model_type = FieldType.from_model_field(self.field.__class__)
|
|
99
|
+
if model_type == FieldType.ModelObjectList:
|
|
100
|
+
# For list related fields (OneToMany, ManyToMany, …) the field
|
|
101
|
+
# type is ModelObjectList, but the filter type should be
|
|
102
|
+
# ModelObject.
|
|
103
|
+
return FieldType.ModelObject
|
|
104
|
+
return model_type
|
|
105
|
+
|
|
96
106
|
@property
|
|
97
107
|
def meta(self):
|
|
98
108
|
res = dict(
|
|
@@ -180,6 +180,15 @@ class ModelResponseMixin(ModelMixin):
|
|
|
180
180
|
|
|
181
181
|
@rest_api("/meta", method="get")
|
|
182
182
|
def get_meta(self, *args, **kwargs):
|
|
183
|
+
"""Retrieve the model metadata.
|
|
184
|
+
---
|
|
185
|
+
get:
|
|
186
|
+
summary: Get {model} metadata
|
|
187
|
+
responses:
|
|
188
|
+
200:
|
|
189
|
+
content:
|
|
190
|
+
application/json
|
|
191
|
+
"""
|
|
183
192
|
return JsonResponse(self.object_meta())
|
|
184
193
|
|
|
185
194
|
|
|
@@ -266,6 +275,15 @@ class ListRestViewMixin(ModelResponseMixin):
|
|
|
266
275
|
|
|
267
276
|
@rest_api("/meta/list", method="get")
|
|
268
277
|
def get_meta_list(self, *args, **kwargs):
|
|
278
|
+
"""Retrieve the model list metadata.
|
|
279
|
+
---
|
|
280
|
+
get:
|
|
281
|
+
summary: Get {models} list metadata
|
|
282
|
+
responses:
|
|
283
|
+
200:
|
|
284
|
+
content:
|
|
285
|
+
application/json
|
|
286
|
+
"""
|
|
269
287
|
return JsonResponse(self.object_meta_list())
|
|
270
288
|
|
|
271
289
|
def apply_view_filter(self, qs):
|
|
@@ -337,6 +355,15 @@ class ListRestViewMixin(ModelResponseMixin):
|
|
|
337
355
|
|
|
338
356
|
@rest_api("", method="get")
|
|
339
357
|
def get_list(self, *args, **kwargs):
|
|
358
|
+
"""Retrieve a list of model.
|
|
359
|
+
---
|
|
360
|
+
get:
|
|
361
|
+
summary: Get {models} list
|
|
362
|
+
responses:
|
|
363
|
+
200:
|
|
364
|
+
content:
|
|
365
|
+
application/json
|
|
366
|
+
"""
|
|
340
367
|
res = {}
|
|
341
368
|
meta = {}
|
|
342
369
|
qs = self.get_list_queryset()
|
|
@@ -373,6 +400,15 @@ class ListRestViewMixin(ModelResponseMixin):
|
|
|
373
400
|
class DetailRestViewMixin(ModelResponseMixin):
|
|
374
401
|
@rest_api("/<int:id>", method="get")
|
|
375
402
|
def get(self, id, *args, **kwargs):
|
|
403
|
+
"""Retrieve the model by pk.
|
|
404
|
+
---
|
|
405
|
+
get:
|
|
406
|
+
summary: Get {model}
|
|
407
|
+
responses:
|
|
408
|
+
200:
|
|
409
|
+
content:
|
|
410
|
+
application/json
|
|
411
|
+
"""
|
|
376
412
|
obj = self.get_object(pk=id)
|
|
377
413
|
return self.response(obj)
|
|
378
414
|
|
|
@@ -382,6 +418,15 @@ class SlugDetailRestViewMixin(ModelResponseMixin):
|
|
|
382
418
|
|
|
383
419
|
@rest_api("/slug/<slug:slug>", method="get")
|
|
384
420
|
def get_by_slug(self, slug, *args, **kwargs):
|
|
421
|
+
"""Retrieve the model by slug.
|
|
422
|
+
---
|
|
423
|
+
get:
|
|
424
|
+
summary: Get {model} by slug
|
|
425
|
+
responses:
|
|
426
|
+
200:
|
|
427
|
+
content:
|
|
428
|
+
application/json
|
|
429
|
+
"""
|
|
385
430
|
obj = self.get_object(**{self.SLUG_FIELD: slug})
|
|
386
431
|
return self.response(obj)
|
|
387
432
|
|
|
@@ -416,6 +461,15 @@ class CreateRestViewMixin(ModelBodyMixin, ModelResponseMixin):
|
|
|
416
461
|
|
|
417
462
|
@rest_api("", method="post")
|
|
418
463
|
def post(self, *args, **kwargs):
|
|
464
|
+
"""Create the model.
|
|
465
|
+
---
|
|
466
|
+
post:
|
|
467
|
+
summary: Create {model}
|
|
468
|
+
responses:
|
|
469
|
+
200:
|
|
470
|
+
content:
|
|
471
|
+
application/json
|
|
472
|
+
"""
|
|
419
473
|
return self._post(*args, **kwargs)
|
|
420
474
|
|
|
421
475
|
|
|
@@ -441,6 +495,15 @@ class UpdateRestViewMixin(ModelBodyMixin, ModelResponseMixin):
|
|
|
441
495
|
|
|
442
496
|
@rest_api("/<int:id>", method="put")
|
|
443
497
|
def put(self, id, *args, **kwargs):
|
|
498
|
+
"""Update the model by pk.
|
|
499
|
+
---
|
|
500
|
+
put:
|
|
501
|
+
summary: Update {model}
|
|
502
|
+
responses:
|
|
503
|
+
200:
|
|
504
|
+
content:
|
|
505
|
+
application/json
|
|
506
|
+
"""
|
|
444
507
|
return self._put(id, *args, **kwargs)
|
|
445
508
|
|
|
446
509
|
|
|
@@ -458,6 +521,15 @@ class DeleteRestViewMixin(ModelMixin):
|
|
|
458
521
|
|
|
459
522
|
@rest_api("/<int:id>", method="delete")
|
|
460
523
|
def delete(self, id, *args, **kwargs):
|
|
524
|
+
"""Delete the model by pk.
|
|
525
|
+
---
|
|
526
|
+
delete:
|
|
527
|
+
summary: Delete {model}
|
|
528
|
+
responses:
|
|
529
|
+
200:
|
|
530
|
+
content:
|
|
531
|
+
application/json
|
|
532
|
+
"""
|
|
461
533
|
return self._delete(id, *args, **kwargs)
|
|
462
534
|
|
|
463
535
|
|
|
@@ -473,6 +545,16 @@ class MediaRestViewMixin(ModelMixin):
|
|
|
473
545
|
|
|
474
546
|
@rest_api("/<int:pk>/<str:field>/upload-url/<str:filename>", method="get")
|
|
475
547
|
def field_media_upload_url(self, pk, field, filename, *args, **kwargs):
|
|
548
|
+
"""Get upload URL for a media field.
|
|
549
|
+
---
|
|
550
|
+
get:
|
|
551
|
+
summary: Get upload URL
|
|
552
|
+
description: Get upload URL for a file field.
|
|
553
|
+
responses:
|
|
554
|
+
200:
|
|
555
|
+
content:
|
|
556
|
+
application/json
|
|
557
|
+
"""
|
|
476
558
|
obj = self.get_object(pk=pk)
|
|
477
559
|
try:
|
|
478
560
|
res = self._get_model_field(field).get_upload_url(
|
|
@@ -484,6 +566,16 @@ class MediaRestViewMixin(ModelMixin):
|
|
|
484
566
|
|
|
485
567
|
@rest_api("/<int:pk>/<str:field>", method="get")
|
|
486
568
|
def field_media_get(self, pk, field, *args, **kwargs):
|
|
569
|
+
"""Get a redirect for a media file file.
|
|
570
|
+
---
|
|
571
|
+
get:
|
|
572
|
+
summary: Get file
|
|
573
|
+
description: Get a redirect for a media field file.
|
|
574
|
+
responses:
|
|
575
|
+
200:
|
|
576
|
+
content:
|
|
577
|
+
application/json
|
|
578
|
+
"""
|
|
487
579
|
obj = self.get_object(pk=pk)
|
|
488
580
|
try:
|
|
489
581
|
url = self._get_model_field(field).get_url(self.request, obj)
|
|
@@ -523,6 +615,8 @@ class SecuredRestViewMixin(View):
|
|
|
523
615
|
|
|
524
616
|
class BaseRestView(SecuredRestViewMixin, View):
|
|
525
617
|
pfx_methods = None
|
|
618
|
+
rest_view_path = {}
|
|
619
|
+
rest_doc = {}
|
|
526
620
|
|
|
527
621
|
def dispatch(self, request, *args, **kwargs):
|
|
528
622
|
# Try to dispatch to the right method; if a method doesn't exist,
|
|
@@ -554,7 +648,7 @@ class BaseRestView(SecuredRestViewMixin, View):
|
|
|
554
648
|
@classmethod
|
|
555
649
|
def get_urls(cls, as_pattern=False):
|
|
556
650
|
def fullpath(p2):
|
|
557
|
-
res = f'{cls.rest_view_path}{p2}'.lstrip('/')
|
|
651
|
+
res = f'{cls.rest_view_path[cls]}{p2}'.lstrip('/')
|
|
558
652
|
return res if as_pattern else f'/{res}'
|
|
559
653
|
|
|
560
654
|
paths = {}
|
|
@@ -156,8 +156,79 @@ class BasicAPITest(TestAssertMixin, TestCase):
|
|
|
156
156
|
response = self.client.get('/api/authors/meta/list')
|
|
157
157
|
self.assertRC(response, 200)
|
|
158
158
|
self.assertJE(response, 'filters.@0.name', 'book_type')
|
|
159
|
-
self.assertJE(response, 'filters.@0.items
|
|
160
|
-
|
|
159
|
+
self.assertJE(response, 'filters.@0.items', [
|
|
160
|
+
{
|
|
161
|
+
"is_group": False,
|
|
162
|
+
"label": "Science Fiction",
|
|
163
|
+
"name": "science_fiction",
|
|
164
|
+
"technical": True,
|
|
165
|
+
"type": "BooleanField"
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"label": "Heroic Fantasy",
|
|
169
|
+
"name": "heroic_fantasy",
|
|
170
|
+
"technical": False,
|
|
171
|
+
"type": "BooleanField"
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"is_group": False,
|
|
175
|
+
"label": "Types",
|
|
176
|
+
"name": "types",
|
|
177
|
+
"related_model": "BookType",
|
|
178
|
+
"technical": False,
|
|
179
|
+
"type": "ModelObject"
|
|
180
|
+
}
|
|
181
|
+
])
|
|
182
|
+
self.assertJE(response, 'filters.@1.name', 'custom')
|
|
183
|
+
self.assertJE(response, 'filters.@1.items', [
|
|
184
|
+
{
|
|
185
|
+
"is_group": False,
|
|
186
|
+
"label": "Last Name",
|
|
187
|
+
"name": "last_name",
|
|
188
|
+
"technical": False,
|
|
189
|
+
"type": "BooleanField"
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"is_group": False,
|
|
193
|
+
"label": "First Name",
|
|
194
|
+
"name": "first_name",
|
|
195
|
+
"technical": False,
|
|
196
|
+
"type": "CharField"
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
"choices": [
|
|
200
|
+
{
|
|
201
|
+
"label": "Male",
|
|
202
|
+
"value": "male"
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"label": "Female",
|
|
206
|
+
"value": "female"
|
|
207
|
+
}
|
|
208
|
+
],
|
|
209
|
+
"is_group": False,
|
|
210
|
+
"label": "Gender",
|
|
211
|
+
"name": "gender",
|
|
212
|
+
"technical": False,
|
|
213
|
+
"type": "CharField"
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"choices": [
|
|
217
|
+
{
|
|
218
|
+
"label": "Tolkien",
|
|
219
|
+
"value": "Tolkien"
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
"label": "Asimov",
|
|
223
|
+
"value": "Asimov"
|
|
224
|
+
}
|
|
225
|
+
],
|
|
226
|
+
"label": "Tolkien or Asimov",
|
|
227
|
+
"name": "last_name_choices",
|
|
228
|
+
"technical": False,
|
|
229
|
+
"type": "CharField"
|
|
230
|
+
}
|
|
231
|
+
])
|
|
161
232
|
|
|
162
233
|
response = self.client.get('/api/books/meta/list')
|
|
163
234
|
self.assertRC(response, 200)
|
|
@@ -35,6 +35,14 @@ class ApiDocTest(TestAssertMixin, TestCase):
|
|
|
35
35
|
assertMethods(paths, '/authors/slug/<slug:slug>', {'get'})
|
|
36
36
|
assertMethods(paths, '/authors/cache/<int:id>', {'get'})
|
|
37
37
|
|
|
38
|
+
# Check a inherited get with default description
|
|
39
|
+
self.assertJE(
|
|
40
|
+
paths, '/authors/<int:id>.get.summary', "Get author")
|
|
41
|
+
# Check a inherited get with custom description
|
|
42
|
+
self.assertJE(
|
|
43
|
+
paths, '/authors-annotate/<int:id>.get.summary',
|
|
44
|
+
"Get custom author")
|
|
45
|
+
|
|
38
46
|
def test_view_get_urls(self):
|
|
39
47
|
def assertMethods(urls, p, methods):
|
|
40
48
|
self.assertEqual(next(filter(
|
|
@@ -4,7 +4,7 @@ from django.core.exceptions import ImproperlyConfigured
|
|
|
4
4
|
from django.db.models import Count, Q
|
|
5
5
|
from django.utils.translation import gettext as _
|
|
6
6
|
|
|
7
|
-
from pfx.pfxcore.decorator import rest_api, rest_view
|
|
7
|
+
from pfx.pfxcore.decorator import rest_api, rest_doc, rest_view
|
|
8
8
|
from pfx.pfxcore.http import JsonResponse
|
|
9
9
|
from pfx.pfxcore.views import (
|
|
10
10
|
VF,
|
|
@@ -125,6 +125,7 @@ class AuthorRestView(AuthorRestViewMixin, SlugDetailRestViewMixin, RestView):
|
|
|
125
125
|
return JsonResponse(dict(value='static:more'))
|
|
126
126
|
|
|
127
127
|
|
|
128
|
+
@rest_doc('/<int:id>', summary="Get custom author")
|
|
128
129
|
@rest_view("/authors-annotate")
|
|
129
130
|
class AuthorAnnotateRestView(AuthorRestView):
|
|
130
131
|
fields = [
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
from .rest import rest_api, rest_property, rest_view
|
|
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
|
{django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/models/user_filtered_queryset_mixin.py
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
|
{django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/templates/registration/welcome_email.txt
RENAMED
|
File without changes
|
{django-pfx-1.2.dev38 → django-pfx-1.2.dev42}/pfx/pfxcore/templates/registration/welcome_subject.txt
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
|