Skip to content

Commit

Permalink
Release 5.10.2
Browse files Browse the repository at this point in the history
### Changelog:
* Feature(backend): Add support for ``x-edit-only`` for serializers.
* Fix(docs): Some ru translations and indents.

Closes vst/vst-utils#655

See merge request vst/vst-utils!664
  • Loading branch information
onegreyonewhite committed Aug 19, 2024
2 parents 5a88dbc + 8887c8c commit 0739894
Show file tree
Hide file tree
Showing 12 changed files with 185 additions and 212 deletions.
1 change: 1 addition & 0 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@
intersphinx_mapping = {
'django': (django_version_docs, django_version_docs + '_objects/'),
'python': ('https://docs.python.org/3.10', None),
'celery': ('https://docs.celeryq.dev/en/stable/', None),
}
extlinks = {
'wiki': ('https://en.wikipedia.org/wiki/%s', None),
Expand Down
3 changes: 3 additions & 0 deletions doc/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,7 @@ Ensure that Traefik is installed on your server. You can download the binary fro
Create a Traefik configuration file ``/path/to/traefik.toml``. Here's a basic example:

.. sourcecode:: toml

[experimental]
http3 = true

Expand Down Expand Up @@ -776,6 +777,7 @@ Make sure to replace ``your_domain.com`` with your actual domain.
Ensure that your vstutils settings have the correct configurations for HTTPS. In your ``/etc/vstutils/settings.ini`` (or project ``settings.ini``):

.. sourcecode:: ini

[web]
secure_proxy_ssl_header_name = HTTP_X_FORWARDED_PROTO
secure_proxy_ssl_header_value = https
Expand Down Expand Up @@ -828,6 +830,7 @@ Replace ``your_domain.com`` with your actual domain and update the paths for SSL
Ensure that your vstutils settings have the correct configurations for HTTPS. In your ``/etc/vstutils/settings.ini`` (or project ``settings.ini``):

.. sourcecode:: ini

[web]
secure_proxy_ssl_header_name = HTTP_X_FORWARDED_PROTO
secure_proxy_ssl_header_value = https
Expand Down
296 changes: 104 additions & 192 deletions doc/locale/ru/LC_MESSAGES/backend.po

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions requirements-doc.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Packages needed for compile documentation.
sphinx~=5.3.0
sphinx-autobuild~=2021.3.14
sphinx~=7.4.7
sphinx-autobuild~=2024.4.16
sphinxcontrib-httpdomain~=1.8.1
sphinxcontrib-websupport~=1.2.4
sphinxcontrib-mermaid~=0.7.1
sphinx-autodoc-typehints~=1.23.0
sphinxcontrib-websupport~=2.0.0
sphinxcontrib-mermaid~=0.9.2
sphinx-autodoc-typehints~=2.2.3
sphinx-rtd-theme~=2.0.0
6 changes: 3 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ ormsgpack~=1.5.0
pyyaml~=6.0.2

# web server
uvicorn~=0.30.5
pyuwsgi==2.0.23.post0
uvicorn~=0.30.6
pyuwsgi~=2.0.26
# Restore it if some problems with pyuwsgi
# uwsgi==2.0.23
fastapi-slim~=0.112.0
fastapi-slim~=0.112.1
aiofiles~=24.1.0
asgiref>=3.8.1

Expand Down
2 changes: 1 addition & 1 deletion test_src/test_proj/models/fields_testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class PropertyAuthorSerializer(BaseSerializer):
}


@actions.SimpleAction(serializer_class=PropertyAuthorSerializer, atomic=True, require_confirmation=True)
@actions.SimpleAction(serializer_class=PropertyAuthorSerializer, atomic=True, require_confirmation=True, edit_only=True)
def simple_property_action(self, request, *args, **kwargs):
"""
Simple property description
Expand Down
5 changes: 4 additions & 1 deletion test_src/test_proj/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -2260,11 +2260,12 @@ def has_deep_parent_filter(params):
self.assertFalse(api['paths'][path]['put']['x-multiaction'])

path = '/author/{id}/simple_property_action/'
self.assertCount(api['paths'][path], 5)
self.assertCount(api['paths'][path], 6)
self.assertIn('get', api['paths'][path])
self.assertIn('put', api['paths'][path])
self.assertIn('patch', api['paths'][path])
self.assertIn('delete', api['paths'][path])
self.assertIn('x-edit-only', api['paths'][path])
self.assertIn('parameters', api['paths'][path])
self.assertEqual(api['paths'][path]['get']['responses']['200']['schema']['$ref'], '#/definitions/PropertyAuthor')
self.assertEqual(api['paths'][path]['put']['responses']['200']['schema']['$ref'], '#/definitions/PropertyAuthor')
Expand All @@ -2275,6 +2276,8 @@ def has_deep_parent_filter(params):
self.assertNotIn('x-title', api['paths'][path]['get'])
self.assertEqual(len(api['paths'][path]['get']['parameters']), 0)
self.assertEqual(api['paths'][path]['patch']['x-require-confirmation'], True)
# Test edit only view in schema
self.assertTrue(api['paths'][path]['x-edit-only'])

path = '/author/{id}/simple_property_action_with_query/'
self.assertCount(api['paths'][path], 2)
Expand Down
2 changes: 1 addition & 1 deletion vstutils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# pylint: disable=django-not-available
__version__: str = '5.10.1'
__version__: str = '5.10.2'
13 changes: 13 additions & 0 deletions vstutils/api/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ def profile(self, request, *args, **kwargs):
:param icons: List of icons for UI button.
:param is_list: Flag indicating whether the action type is a list or a single entity.
Typically used with GET actions.
:param edit_only: Flag indicating whether the action will only use edit mode, without a view page.
This is used for actions where there is a GET method and
any other modifying methods (POST, PUT, PATCH).
:param require_confirmation: If true user will be asked to confirm action execution on frontend.
:param kwargs: Set of named arguments for :func:`rest_framework.decorators.action`.
Expand All @@ -96,6 +99,7 @@ def profile(self, request, *args, **kwargs):
'title',
'icons',
'is_list',
'edit_only',
'hidden',
'require_confirmation',
)
Expand All @@ -121,6 +125,7 @@ def __init__( # noqa: CFQ002
is_list=False,
hidden=False,
require_confirmation=False,
edit_only=False,
**kwargs,
):
# pylint: disable=too-many-arguments
Expand All @@ -133,6 +138,7 @@ def __init__( # noqa: CFQ002
self.title = title
self.icons = icons
self.is_list = is_list
self.edit_only = edit_only
self.hidden = hidden
self.require_confirmation = require_confirmation
self.action_kwargs = kwargs
Expand All @@ -144,6 +150,13 @@ def __init__( # noqa: CFQ002
def is_page(self):
return 'GET' in self.methods

def get_extra_path_data(self, method_name):
extra_path_data = {}
if method_name.upper() == 'GET':
if not self.is_list and self.edit_only and self.is_page and len(set(self.methods) - {'DELETE', 'GET'}):
extra_path_data['x-edit-only'] = True
return extra_path_data

def wrap_function(self, func):
res = action(
detail=self.detail,
Expand Down
3 changes: 3 additions & 0 deletions vstutils/api/actions.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class Action:
title: _t.Optional[_t.Text]
icons: _t.Optional[_t.Union[_t.Text, _t.Iterable]]
is_list: bool
edit_only: bool
hidden: bool
action_kwargs: _t.Dict[_t.Text, _t.Any]
def __init__(
Expand All @@ -51,11 +52,13 @@ class Action:
title: _t.Optional[_t.Text] = ...,
icons: _t.Optional[_t.Union[_t.Text, _t.Iterable]] = ...,
is_list: bool = ...,
edit_only: bool = ...,
hidden: bool = ...,
**kwargs
) -> None: ...
@property
def is_page(self) -> bool: ...
def get_extra_path_data(self, method_name: str) -> dict[str, _t.Any]: ...
def wrap_function(self, func: _t.Callable) -> ViewSetAction: ...
def __call__(self, method: _t.Callable) -> ViewSetAction:
def action_method(
Expand Down
40 changes: 39 additions & 1 deletion vstutils/api/schema/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from rest_framework import request as drf_request
from django.conf import settings
from django.utils.module_loading import import_string
from drf_yasg import generators
from drf_yasg import generators, openapi
from drf_yasg.inspectors import field as field_insp
from vstutils.utils import raise_context_decorator_with_default

Expand Down Expand Up @@ -141,6 +141,44 @@ def get_operation(self, *args, **kwargs):
self.required_security_definitions.add(tuple(secDef.keys())[0])
return operation

def get_paths(self, endpoints, components, request, public):
# pylint: disable=too-many-locals
if not endpoints:
return openapi.Paths(paths={}), ''

prefix = self.determine_path_prefix(list(endpoints.keys())) or ''
assert '{' not in prefix, "base path cannot be templated in swagger 2.0"

paths = {}
for path, (view_cls, methods) in sorted(endpoints.items()):
operations = {}
extra_path_data = {}
for method, view in methods:
if not self.should_include_endpoint(path, method, view, public):
continue

action_name = getattr(view, 'action', None)
if action_name and (action_object := getattr(getattr(view, action_name, None), 'action', None)):
extra_path_data.update(action_object.get_extra_path_data(method))
operation = self.get_operation(view, path, prefix, method, components, request)
if operation is not None:
operations[method.lower()] = operation

if operations:
# since the common prefix is used as the API basePath, it must be stripped
# from individual paths when writing them into the swagger document
path_suffix = path[len(prefix):]
if not path_suffix.startswith('/'):
# copied from original method
# may be unnecessary
path_suffix = '/' + path_suffix # nocv
path_item = self.get_path_item(path, view_cls, operations)
if extra_path_data:
path_item.update(extra_path_data)
paths[path_suffix] = path_item

return self.get_paths_object(paths), prefix

def get_operation_keys(self, subpath, method, view):
keys = super().get_operation_keys(subpath, method, view)
subpath_keys = list(filter(bool, subpath.split('/')))
Expand Down
16 changes: 8 additions & 8 deletions vstutils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ def create_view(model, **meta_options):
.. warning::
This function is oldstyle and will be deprecated in future versions.
Use native call of method :method:`vstutils.models.BModel.get_view_class`.
Use native call of method :meth:`vstutils.models.BModel.get_view_class`.
:type model: Type[vstutils.models.BaseModel]
:param model: Model class with `.get_view_class` method. This method also has :class:`vstutils.models.BModel`.
Expand Down Expand Up @@ -1438,31 +1438,31 @@ class VstEnum(Enum, metaclass=VstEnumMeta):

class BaseEnum(str, VstEnum):
"""
BaseEnum extends `Enum` class and used to create enum-like objects that can be used in django serializers or
django models.
BaseEnum extends :class:`enum.Enum` class and used to create enum-like objects that can be used in
django serializers or django models.
Example:
.. sourcecode:: python
from vstutils.models import BModel
class ItemCLasses(BaseEnum):
class ItemClasses(BaseEnum):
FIRST = BaseEnum.SAME
SECOND = BaseEnum.SAME
THIRD = BaseEnum.SAME
class MyDjangoModel(BModel):
item_class = models.CharField(max_length=ItemCLasses.max_len, choices=ItemCLasses.to_choices())
item_class = models.CharField(max_length=ItemClasses.max_len, choices=ItemClasses.to_choices())
@property
def is_second(self):
# Function check is item has second class of instance
return ItemCLasses.SECOND.is_equal(self.item_class)
return ItemClasses.SECOND.is_equal(self.item_class)
.. note::
For special cases, when value must be in lower or upper case, you can setup value as ``BaseEnum.LOWER` or
For special cases, when value must be in lower or upper case, you can setup value as ``BaseEnum.LOWER`` or
``BaseEnum.UPPER``. But in default cases we recommend use ``BaseEnum.SAME`` for memory optimization.
"""

Expand Down

0 comments on commit 0739894

Please sign in to comment.