django-pfx 1.2.dev40__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.
Files changed (88) hide show
  1. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/.gitignore +1 -0
  2. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/PKG-INFO +1 -1
  3. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/django_pfx.egg-info/PKG-INFO +1 -1
  4. django-pfx-1.2.dev42/pfx/pfxcore/decorator/__init__.py +1 -0
  5. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/decorator/rest.py +16 -1
  6. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/management/commands/makeapidoc.py +22 -1
  7. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/test.py +32 -25
  8. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/views/rest_views.py +95 -1
  9. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/tests/tests/test_api_doc.py +8 -0
  10. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/tests/views.py +2 -1
  11. django-pfx-1.2.dev40/pfx/pfxcore/decorator/__init__.py +0 -1
  12. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/.gitlab-ci.yml +0 -0
  13. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/.pre-commit-config.yaml +0 -0
  14. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/LICENSE +0 -0
  15. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/MANIFEST.in +0 -0
  16. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/README.md +0 -0
  17. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/django_pfx.egg-info/SOURCES.txt +0 -0
  18. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/django_pfx.egg-info/dependency_links.txt +0 -0
  19. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/django_pfx.egg-info/requires.txt +0 -0
  20. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/django_pfx.egg-info/top_level.txt +0 -0
  21. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/img/pfx.png +0 -0
  22. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/img/pfx.svg +0 -0
  23. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/__init__.py +0 -0
  24. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/__init__.py +0 -0
  25. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/apps.py +0 -0
  26. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/default_settings.py +0 -0
  27. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/exceptions.py +0 -0
  28. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/fields.py +0 -0
  29. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/http/__init__.py +0 -0
  30. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/http/json_response.py +0 -0
  31. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
  32. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.po +0 -0
  33. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/management/__init__.py +0 -0
  34. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/management/commands/__init__.py +0 -0
  35. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/middleware/__init__.py +0 -0
  36. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/middleware/authentication.py +0 -0
  37. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/middleware/locale.py +0 -0
  38. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/models/__init__.py +0 -0
  39. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/models/cache_mixins.py +0 -0
  40. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/models/not_null_fields.py +0 -0
  41. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/models/pfx_models.py +0 -0
  42. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/models/user_filtered_queryset_mixin.py +0 -0
  43. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/serializers/__init__.py +0 -0
  44. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/serializers/json.py +0 -0
  45. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/settings.py +0 -0
  46. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/shortcuts.py +0 -0
  47. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/storage/__init__.py +0 -0
  48. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/storage/s3_storage.py +0 -0
  49. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/templates/registration/password_reset_email.txt +0 -0
  50. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/templates/registration/password_reset_subject.txt +0 -0
  51. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/templates/registration/welcome_email.txt +0 -0
  52. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/templates/registration/welcome_subject.txt +0 -0
  53. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/urls.py +0 -0
  54. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/views/__init__.py +0 -0
  55. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/views/authentication_views.py +0 -0
  56. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/views/fields.py +0 -0
  57. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/views/filters_views.py +0 -0
  58. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pfx/pfxcore/views/locale_views.py +0 -0
  59. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/pyproject.toml +0 -0
  60. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/requirements.txt +0 -0
  61. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/runtest.py +0 -0
  62. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/setup.cfg +0 -0
  63. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/setup.py +0 -0
  64. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/tests/__init__.py +0 -0
  65. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/tests/models.py +0 -0
  66. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/tests/settings/__init__.py +0 -0
  67. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/tests/settings/ci.py +0 -0
  68. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/tests/settings/common.py +0 -0
  69. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/tests/settings/dev.py +0 -0
  70. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/tests/settings/dev_custom_example.py +0 -0
  71. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/tests/settings/dev_default.py +0 -0
  72. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/tests/tests/__init__.py +0 -0
  73. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/tests/tests/basic_api_errors.py +0 -0
  74. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/tests/tests/basic_api_test.py +0 -0
  75. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/tests/tests/test_auth_api.py +0 -0
  76. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/tests/tests/test_cache.py +0 -0
  77. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/tests/tests/test_fields.py +0 -0
  78. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/tests/tests/test_filters.py +0 -0
  79. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/tests/tests/test_locale_api.py +0 -0
  80. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/tests/tests/test_perm_tests.py +0 -0
  81. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/tests/tests/test_perms_api.py +0 -0
  82. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/tests/tests/test_shortcuts.py +0 -0
  83. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/tests/tests/test_timezone_middleware.py +0 -0
  84. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/tests/tests/test_tools.py +0 -0
  85. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/tests/tests/test_user_queryset.py +0 -0
  86. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/tests/tests/test_view_decorators.py +0 -0
  87. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/tests/tests/test_view_fields.py +0 -0
  88. {django-pfx-1.2.dev40 → django-pfx-1.2.dev42}/tests/urls.py +0 -0
@@ -26,3 +26,4 @@ dev_custom.py
26
26
 
27
27
  # Other
28
28
  /local
29
+ /doc
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-pfx
3
- Version: 1.2.dev40
3
+ Version: 1.2.dev42
4
4
  Summary: Django PFX is a toolkit to build web APIs dedicated to be used by React progressive web app.
5
5
  Author: Hervé Martinet
6
6
  Author-email: herve.martinet@gmail.com
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-pfx
3
- Version: 1.2.dev40
3
+ Version: 1.2.dev42
4
4
  Summary: Django PFX is a toolkit to build web APIs dedicated to be used by React progressive web app.
5
5
  Author: Hervé Martinet
6
6
  Author-email: herve.martinet@gmail.com
@@ -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={k: {} for k, v in url['methods'].items()})
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 get_val(self, response, key):
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(lambda c, k: c[_p(k)], key.split("."),
167
- response.json_content)
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, response, key, value, msg=None):
177
- msg = '\n'.join([msg or '', response.formatted()])
178
- return self.assertEqual(self.get_val(response, key), value, msg=msg)
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, response, key, value, msg=None):
182
- msg = '\n'.join([msg or '', response.formatted()])
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(response, key), value, msg=msg)
191
+ self.get_val(src, key), value, msg=msg)
185
192
 
186
193
  # assert JSON content at key exists
187
- def assertJEExists(self, response, key, msg=None):
188
- msg = '\n'.join([msg or '', response.formatted()])
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, response.json_content, msg=msg)
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(response, path), msg=msg)
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, response, key, msg=None):
196
- msg = '\n'.join([msg or '', response.formatted()])
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, response.json_content, msg=msg)
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(response, path), msg=msg)
207
+ return self.assertNotIn(name, self.get_val(src, path), msg=msg)
201
208
 
202
209
  # assert JSON array size
203
- def assertSize(self, response, key, size, msg=None):
204
- msg = '\n'.join([msg or '', response.formatted()])
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(response, key)), size, msg=msg)
213
+ len(self.get_val(src, key)), size, msg=msg)
207
214
 
208
215
  # assert JSON array contains
209
- def assertJIn(self, response, key, element, msg=None):
210
- msg = '\n'.join([msg or '', response.formatted()])
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(response, key), msg=msg)
219
+ element, self.get_val(src, key), msg=msg)
213
220
 
214
221
 
215
222
  class TestPermsAssertMixin(TestAssertMixin):
@@ -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 = {}
@@ -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