viur-core 3.9.0.dev4__tar.gz → 3.9.0.dev5__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.
- {viur_core-3.9.0.dev4/src/viur_core.egg-info → viur_core-3.9.0.dev5}/PKG-INFO +2 -1
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/pyproject.toml +1 -0
- viur_core-3.9.0.dev5/src/viur/core/bones/captcha.py +177 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/bones/randomslice.py +2 -2
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/bones/relational.py +16 -9
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/bones/spatial.py +12 -6
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/bones/text.py +1 -1
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/config.py +1 -7
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/db/__init__.py +2 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/db/query.py +38 -25
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/db/transport.py +4 -6
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/db/types.py +7 -1
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/modules/file.py +2 -1
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/modules/script.py +7 -2
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/modules/translation.py +2 -2
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/modules/user.py +3 -3
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/prototypes/skelmodule.py +8 -10
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/securityheaders.py +14 -3
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/skeleton/__init__.py +2 -1
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/skeleton/instance.py +59 -7
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/skeleton/meta.py +36 -1
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/skeleton/skeleton.py +14 -5
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/skeleton/utils.py +20 -10
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/tasks.py +1 -1
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/version.py +1 -1
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5/src/viur_core.egg-info}/PKG-INFO +2 -1
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur_core.egg-info/requires.txt +1 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/tests/test_config.py +0 -1
- viur_core-3.9.0.dev5/tests/test_db.py +81 -0
- viur_core-3.9.0.dev4/src/viur/core/bones/captcha.py +0 -130
- viur_core-3.9.0.dev4/tests/test_db.py +0 -29
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/LICENSE +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/README.md +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/setup.cfg +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/__init__.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/bones/__init__.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/bones/base.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/bones/boolean.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/bones/code.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/bones/color.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/bones/credential.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/bones/date.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/bones/email.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/bones/file.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/bones/image.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/bones/json.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/bones/key.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/bones/numeric.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/bones/password.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/bones/phone.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/bones/raw.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/bones/record.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/bones/select.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/bones/selectcountry.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/bones/sortindex.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/bones/spam.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/bones/string.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/bones/treeleaf.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/bones/treenode.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/bones/uid.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/bones/uri.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/bones/user.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/cache.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/contrib/__init__.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/contrib/loginkey.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/contrib/ratelimit.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/current.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/db/cache.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/db/config.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/db/overrides.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/db/utils.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/decorators.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/email.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/errors.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/i18n.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/languages/__init__.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/languages/de.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/languages/en.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/logging.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/module.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/modules/__init__.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/modules/email.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/modules/formmailer.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/modules/history.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/modules/moduleconf.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/modules/page.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/modules/site.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/pagination.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/prototypes/__init__.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/prototypes/instanced_module.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/prototypes/list.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/prototypes/singleton.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/prototypes/tree.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/ratelimit.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/render/__init__.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/render/abstract.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/render/html/__init__.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/render/html/default.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/render/html/env/__init__.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/render/html/env/date.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/render/html/env/debug.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/render/html/env/regex.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/render/html/env/session.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/render/html/env/strings.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/render/html/env/tests.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/render/html/env/viur.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/render/html/utils.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/render/json/__init__.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/render/json/default.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/render/vi/__init__.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/request.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/scripts/viur_migrate.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/secret.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/securitykey.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/session.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/skeleton/adapter.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/skeleton/relskel.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/skeleton/tasks.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/template/error.html +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/template/vi_user_google_login.html +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/utils/__init__.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/utils/json.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/utils/parse.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur/core/utils/string.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur_core.egg-info/SOURCES.txt +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur_core.egg-info/dependency_links.txt +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur_core.egg-info/entry_points.txt +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/src/viur_core.egg-info/top_level.txt +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/tests/test_decorators.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/tests/test_errors.py +0 -0
- {viur_core-3.9.0.dev4 → viur_core-3.9.0.dev5}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: viur-core
|
|
3
|
-
Version: 3.9.0.
|
|
3
|
+
Version: 3.9.0.dev5
|
|
4
4
|
Summary: The core component of ViUR, a development framework for Google App Engine
|
|
5
5
|
Author-email: Mausbrand Informationssysteme GmbH <devs@viur.dev>
|
|
6
6
|
Maintainer-email: Jan Max Meyer <jm@mausbrand.de>
|
|
@@ -48,6 +48,7 @@ Requires-Dist: google-cloud-bigquery~=3.0
|
|
|
48
48
|
Requires-Dist: google-cloud-datastore~=2.0
|
|
49
49
|
Requires-Dist: google-cloud-iam~=2.0
|
|
50
50
|
Requires-Dist: google-cloud-logging~=3.0
|
|
51
|
+
Requires-Dist: google-cloud-recaptcha-enterprise~=1.0
|
|
51
52
|
Requires-Dist: google-cloud-secret-manager~=2.0
|
|
52
53
|
Requires-Dist: google-cloud-storage~=2.0
|
|
53
54
|
Requires-Dist: google-cloud-tasks~=2.0
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import typing as t
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
from viur.core import conf, current
|
|
6
|
+
from viur.core.bones.base import BaseBone, ReadFromClientError, ReadFromClientErrorSeverity
|
|
7
|
+
|
|
8
|
+
from google.cloud import recaptchaenterprise_v1
|
|
9
|
+
from google.cloud.recaptchaenterprise_v1 import Assessment
|
|
10
|
+
|
|
11
|
+
if t.TYPE_CHECKING:
|
|
12
|
+
from viur.core.skeleton import SkeletonInstance
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CaptchaBone(BaseBone):
|
|
16
|
+
r"""
|
|
17
|
+
The CaptchaBone validates reCAPTCHA Enterprise tokens to protect forms from bots.
|
|
18
|
+
|
|
19
|
+
It uses the Google reCAPTCHA Enterprise API and supports both invisible (v3-style score-based)
|
|
20
|
+
and visible (checkbox widget) challenges via the ``render_challenge`` parameter.
|
|
21
|
+
|
|
22
|
+
The token is submitted by the client as the bone's field value and verified server-side
|
|
23
|
+
against the configured site key. A configurable score threshold determines whether
|
|
24
|
+
invisible challenges pass.
|
|
25
|
+
|
|
26
|
+
.. seealso::
|
|
27
|
+
|
|
28
|
+
`Google reCAPTCHA Enterprise setup
|
|
29
|
+
<https://cloud.google.com/recaptcha/docs/set-up-non-google-cloud-environments-api-keys>`
|
|
30
|
+
for creating a site key and enabling the API.
|
|
31
|
+
|
|
32
|
+
Option :attr:`core.config.Security.captcha_default_public_key`
|
|
33
|
+
for global security settings.
|
|
34
|
+
|
|
35
|
+
Option :attr:`core.config.Security.captcha_enforce_always`
|
|
36
|
+
to enforce validation even on development servers.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
type = "captcha"
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
*,
|
|
44
|
+
public_key: str = None,
|
|
45
|
+
score_threshold: float = 0.5,
|
|
46
|
+
render_challenge: bool = False,
|
|
47
|
+
recaptcha_action: str = "",
|
|
48
|
+
**kwargs: t.Any
|
|
49
|
+
):
|
|
50
|
+
"""
|
|
51
|
+
Initializes a new CaptchaBone.
|
|
52
|
+
|
|
53
|
+
:param public_key: The reCAPTCHA Enterprise site key shown to the client.
|
|
54
|
+
Can be omitted if set globally via :attr:`core.config.Security.captcha_default_public_key`.
|
|
55
|
+
:param score_threshold: Minimum score (0–1) required for invisible challenges to pass.
|
|
56
|
+
Ignored when ``render_challenge`` is ``True``.
|
|
57
|
+
:param render_challenge: If ``True``, renders a visible checkbox widget instead of
|
|
58
|
+
running an invisible background check.
|
|
59
|
+
:param recaptcha_action: The action name passed to reCAPTCHA for analytics and scoring.
|
|
60
|
+
Should match the action used on the client side.
|
|
61
|
+
"""
|
|
62
|
+
if "publicKey" in kwargs:
|
|
63
|
+
warnings.warn("publicKey parameter is deprecated, please use public_key",
|
|
64
|
+
DeprecationWarning, stacklevel=2)
|
|
65
|
+
public_key = kwargs.pop("publicKey")
|
|
66
|
+
super().__init__(**kwargs)
|
|
67
|
+
if not public_key and conf.security.captcha_default_public_key:
|
|
68
|
+
public_key = conf.security.captcha_default_public_key
|
|
69
|
+
if not public_key:
|
|
70
|
+
raise ValueError("CaptchaBone requires either a public_key or conf.security.captcha_default_public_key")
|
|
71
|
+
|
|
72
|
+
self.public_key = public_key
|
|
73
|
+
|
|
74
|
+
if not (0 < score_threshold <= 1):
|
|
75
|
+
raise ValueError("score_threshold must be between 0 and 1.")
|
|
76
|
+
self.render_challenge = render_challenge
|
|
77
|
+
self.recaptcha_action = recaptcha_action
|
|
78
|
+
self.score_threshold = score_threshold
|
|
79
|
+
self.required = True
|
|
80
|
+
|
|
81
|
+
def serialize(self, skel: "SkeletonInstance", name: str, parentIndexed: bool) -> bool:
|
|
82
|
+
"""
|
|
83
|
+
Serializing the Captcha bone is not possible so it return False
|
|
84
|
+
"""
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
def unserialize(self, skel: "SkeletonInstance", name) -> t.Literal[True]:
|
|
88
|
+
"""
|
|
89
|
+
Stores the public_key in the SkeletonInstance
|
|
90
|
+
|
|
91
|
+
:param skel: The target :class:`SkeletonInstance`.
|
|
92
|
+
:param name: The name of the CaptchaBone in the :class:`SkeletonInstance`.
|
|
93
|
+
|
|
94
|
+
:returns: boolean, that is true, as the Captcha bone is always unserialized successfully.
|
|
95
|
+
"""
|
|
96
|
+
skel.accessedValues[name] = self.public_key
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
def fromClient(self, skel: "SkeletonInstance", name: str, data: dict) -> None | list[ReadFromClientError]:
|
|
100
|
+
"""
|
|
101
|
+
Load the reCAPTCHA token from the provided data and validate it with the help of the API.
|
|
102
|
+
|
|
103
|
+
reCAPTCHA provides the token via callback usually as "g-recaptcha-response",
|
|
104
|
+
but to fit into the skeleton logic, we support both names.
|
|
105
|
+
So the token can be provided as "g-recaptcha-response" or the name of the CaptchaBone in the Skeleton.
|
|
106
|
+
While the latter one is the preferred name.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
if not conf.security.captcha_enforce_always and conf.instance.is_dev_server:
|
|
110
|
+
logging.info("Skipping captcha validation on development server")
|
|
111
|
+
return None
|
|
112
|
+
if not conf.security.captcha_enforce_always and (user := current.user.get()) and "root" in user["access"]:
|
|
113
|
+
logging.info("Skipping captcha validation for root user")
|
|
114
|
+
return None # Don't bother trusted users with this (not supported by admin/vi anyway)
|
|
115
|
+
|
|
116
|
+
client = recaptchaenterprise_v1.RecaptchaEnterpriseServiceClient()
|
|
117
|
+
|
|
118
|
+
# Set the attributes of the event to be tracked.
|
|
119
|
+
event = recaptchaenterprise_v1.Event()
|
|
120
|
+
event.site_key = self.public_key
|
|
121
|
+
if name in data:
|
|
122
|
+
event.token = data[name]
|
|
123
|
+
else:
|
|
124
|
+
return [ReadFromClientError(
|
|
125
|
+
ReadFromClientErrorSeverity.NotSet,
|
|
126
|
+
"Token not set"
|
|
127
|
+
)]
|
|
128
|
+
assessment = recaptchaenterprise_v1.Assessment()
|
|
129
|
+
assessment.event = event
|
|
130
|
+
|
|
131
|
+
project_name = f"projects/{conf.instance.project_id}"
|
|
132
|
+
|
|
133
|
+
# Create the assessment request.
|
|
134
|
+
request = recaptchaenterprise_v1.CreateAssessmentRequest()
|
|
135
|
+
request.assessment = assessment
|
|
136
|
+
request.parent = project_name
|
|
137
|
+
|
|
138
|
+
response = client.create_assessment(request)
|
|
139
|
+
|
|
140
|
+
if not response.token_properties.valid:
|
|
141
|
+
logging.info(
|
|
142
|
+
"The CreateAssessment call failed because the token was "
|
|
143
|
+
+ "invalid for the following reasons: "
|
|
144
|
+
+ str(response.token_properties.invalid_reason)
|
|
145
|
+
)
|
|
146
|
+
return [ReadFromClientError(
|
|
147
|
+
ReadFromClientErrorSeverity.Invalid,
|
|
148
|
+
"Invalid Token"
|
|
149
|
+
)]
|
|
150
|
+
|
|
151
|
+
# Check if the expected action was executed.
|
|
152
|
+
if response.token_properties.action != self.recaptcha_action:
|
|
153
|
+
logging.info(
|
|
154
|
+
"The action attribute in your reCAPTCHA tag does not match the action you are expecting to score"
|
|
155
|
+
)
|
|
156
|
+
return [ReadFromClientError(
|
|
157
|
+
ReadFromClientErrorSeverity.Invalid,
|
|
158
|
+
f"Invalid Action: {self.recaptcha_action}"
|
|
159
|
+
)]
|
|
160
|
+
else:
|
|
161
|
+
# Retrieve the risk score and reasons.
|
|
162
|
+
# For more information on interpreting the assessment, see:
|
|
163
|
+
# https://cloud.google.com/recaptcha/docs/interpret-assessment
|
|
164
|
+
if response.risk_analysis.score < self.score_threshold:
|
|
165
|
+
return [ReadFromClientError(
|
|
166
|
+
ReadFromClientErrorSeverity.Invalid,
|
|
167
|
+
f"Invalid Captcha: {response.risk_analysis.score}"
|
|
168
|
+
)]
|
|
169
|
+
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
def structure(self) -> dict:
|
|
173
|
+
return super().structure() | {
|
|
174
|
+
"public_key": self.public_key,
|
|
175
|
+
"render_challenge": self.render_challenge,
|
|
176
|
+
"action": self.recaptcha_action
|
|
177
|
+
}
|
|
@@ -112,13 +112,13 @@ class RandomSliceBone(BaseBone):
|
|
|
112
112
|
q = db.QueryDefinition(origKind, {}, [])
|
|
113
113
|
property, value = applyFilterHook(dbFilter, f"{name} <=", rndVal)
|
|
114
114
|
q.filters[property] = value
|
|
115
|
-
q.orders = [(name, db.SortOrder.Descending)]
|
|
115
|
+
q.orders = [db.QueryOrder(name, db.SortOrder.Descending)]
|
|
116
116
|
queries.append(q)
|
|
117
117
|
# Left Side
|
|
118
118
|
q = db.QueryDefinition(origKind, {}, [])
|
|
119
119
|
property, value = applyFilterHook(dbFilter, f"{name} >", rndVal)
|
|
120
120
|
q.filters[property] = value
|
|
121
|
-
q.orders = [(name
|
|
121
|
+
q.orders = [db.QueryOrder(name)]
|
|
122
122
|
queries.append(q)
|
|
123
123
|
dbFilter.queries = queries
|
|
124
124
|
# Map the original filter back in
|
|
@@ -965,7 +965,12 @@ class RelationalBone(BaseBone):
|
|
|
965
965
|
raise RuntimeError()
|
|
966
966
|
return f"src.{param}", value
|
|
967
967
|
|
|
968
|
-
def orderHook(
|
|
968
|
+
def orderHook(
|
|
969
|
+
self,
|
|
970
|
+
name: str,
|
|
971
|
+
query: db.Query,
|
|
972
|
+
orderings: list[db.QueryOrder | str] | tuple[db.QueryOrder | str, ...],
|
|
973
|
+
) -> list[db.QueryOrder | str] | tuple[db.QueryOrder | str]:
|
|
969
974
|
"""
|
|
970
975
|
Hook installed by buildDbFilter that rewrites orderings added to the query to match the layout of the
|
|
971
976
|
viur-relations index and performs sanity checks on the query.
|
|
@@ -977,21 +982,23 @@ class RelationalBone(BaseBone):
|
|
|
977
982
|
:param name: The name of the bone.
|
|
978
983
|
:param query: The datastore query to be modified.
|
|
979
984
|
:param orderings: A list or tuple of orderings to be checked and potentially modified.
|
|
980
|
-
:type orderings: List[Union[str, Tuple[str, db.SortOrder]]] or Tuple[Union[str, Tuple[str, db.SortOrder]]]
|
|
981
985
|
|
|
982
986
|
:return: A list of modified orderings that are compatible with the viur-relations index.
|
|
983
|
-
:rtype: List[Union[str, Tuple[str, db.SortOrder]]]
|
|
984
987
|
|
|
985
988
|
:raises RuntimeError: If the ordering is invalid, e.g., using properties not in 'refKeys' or 'parentKeys'.
|
|
986
989
|
"""
|
|
987
990
|
res = []
|
|
988
|
-
if
|
|
991
|
+
if isinstance(orderings, (str, db.QueryOrder)):
|
|
989
992
|
orderings = [orderings]
|
|
993
|
+
elif not isinstance(orderings, list):
|
|
994
|
+
orderings = list(orderings)
|
|
990
995
|
for order in orderings:
|
|
991
|
-
if isinstance(order,
|
|
992
|
-
orderKey = order
|
|
993
|
-
|
|
996
|
+
if isinstance(order, db.QueryOrder):
|
|
997
|
+
orderKey = order.name
|
|
998
|
+
elif isinstance(order, str):
|
|
994
999
|
orderKey = order
|
|
1000
|
+
else:
|
|
1001
|
+
raise TypeError(f"Invalid ordering {order!r} in orderHook")
|
|
995
1002
|
if orderKey.startswith("dest.") or orderKey.startswith("rel.") or orderKey.startswith("src."):
|
|
996
1003
|
# This is already valid for our relational index
|
|
997
1004
|
res.append(order)
|
|
@@ -1005,7 +1012,7 @@ class RelationalBone(BaseBone):
|
|
|
1005
1012
|
res.append(order)
|
|
1006
1013
|
else:
|
|
1007
1014
|
if isinstance(order, tuple):
|
|
1008
|
-
res.append((f"dest.{k}", order[1]))
|
|
1015
|
+
res.append(db.QueryOrder(f"dest.{k}", order[1]))
|
|
1009
1016
|
else:
|
|
1010
1017
|
res.append(f"dest.{k}")
|
|
1011
1018
|
else:
|
|
@@ -1019,7 +1026,7 @@ class RelationalBone(BaseBone):
|
|
|
1019
1026
|
f"Invalid ordering! {orderKey} is not in parentKeys of RelationalBone {name}!")
|
|
1020
1027
|
raise RuntimeError()
|
|
1021
1028
|
if isinstance(order, tuple):
|
|
1022
|
-
res.append((f"src.{orderKey}", order[1]))
|
|
1029
|
+
res.append(db.QueryOrder(f"src.{orderKey}", order[1]))
|
|
1023
1030
|
else:
|
|
1024
1031
|
res.append(f"src.{orderKey}")
|
|
1025
1032
|
return res
|
|
@@ -281,22 +281,22 @@ class SpatialBone(BaseBone):
|
|
|
281
281
|
q1 = deepcopy(origQuery)
|
|
282
282
|
q1.filters[name + ".coordinates.lat >="] = lat
|
|
283
283
|
q1.filters[name + ".tiles.lat ="] = tileLat
|
|
284
|
-
q1.orders = [(name + ".coordinates.lat"
|
|
284
|
+
q1.orders = [db.QueryOrder(name + ".coordinates.lat")]
|
|
285
285
|
# Lat - Left Side
|
|
286
286
|
q2 = deepcopy(origQuery)
|
|
287
287
|
q2.filters[name + ".coordinates.lat <"] = lat
|
|
288
288
|
q2.filters[name + ".tiles.lat ="] = tileLat
|
|
289
|
-
q2.orders = [(name + ".coordinates.lat", db.SortOrder.Descending)]
|
|
289
|
+
q2.orders = [db.QueryOrder(name + ".coordinates.lat", db.SortOrder.Descending)]
|
|
290
290
|
# Lng - Down
|
|
291
291
|
q3 = deepcopy(origQuery)
|
|
292
292
|
q3.filters[name + ".coordinates.lng >="] = lng
|
|
293
293
|
q3.filters[name + ".tiles.lng ="] = tileLng
|
|
294
|
-
q3.orders = [(name + ".coordinates.lng"
|
|
294
|
+
q3.orders = [db.QueryOrder(name + ".coordinates.lng")]
|
|
295
295
|
# Lng - Top
|
|
296
296
|
q4 = deepcopy(origQuery)
|
|
297
297
|
q4.filters[name + ".coordinates.lng <"] = lng
|
|
298
298
|
q4.filters[name + ".tiles.lng ="] = tileLng
|
|
299
|
-
q4.orders = [(name + ".coordinates.lng", db.SortOrder.Descending)]
|
|
299
|
+
q4.orders = [db.QueryOrder(name + ".coordinates.lng", db.SortOrder.Descending)]
|
|
300
300
|
dbFilter.queries = [q1, q2, q3, q4]
|
|
301
301
|
dbFilter._customMultiQueryMerge = lambda *args, **kwargs: self.customMultiQueryMerge(name, lat, lng, *args,
|
|
302
302
|
**kwargs)
|
|
@@ -378,7 +378,8 @@ class SpatialBone(BaseBone):
|
|
|
378
378
|
|
|
379
379
|
:param skel: Dictionary with the current values from the skeleton the bone belongs to
|
|
380
380
|
:param boneName: The name of the bone that should be modified
|
|
381
|
-
:param value: The value that should be assigned.
|
|
381
|
+
:param value: The value that should be assigned. Either a tuple/list of (lat, lng) or a dict
|
|
382
|
+
with keys ``lat`` and ``lng``
|
|
382
383
|
:param append: If True, the given value will be appended to the existing bone values instead of
|
|
383
384
|
replacing them. Only supported on bones with multiple=True
|
|
384
385
|
:param language: Optional, the language of the value if the bone is language-aware
|
|
@@ -387,7 +388,12 @@ class SpatialBone(BaseBone):
|
|
|
387
388
|
"""
|
|
388
389
|
if append:
|
|
389
390
|
raise ValueError(f"append is not possible on {self.type} bones")
|
|
390
|
-
if
|
|
391
|
+
if isinstance(value, dict):
|
|
392
|
+
try:
|
|
393
|
+
value = (float(value["lat"]), float(value["lng"]))
|
|
394
|
+
except (KeyError, TypeError, ValueError):
|
|
395
|
+
raise ValueError("Value dict must contain 'lat' and 'lng' keys as floats")
|
|
396
|
+
if not isinstance(value, (tuple, list)) or len(value) != 2:
|
|
391
397
|
raise ValueError("Value must be a tuple or a list of (lat, lng)")
|
|
392
398
|
skel[boneName] = tuple(value)
|
|
393
399
|
|
|
@@ -414,7 +414,7 @@ class TextBone(RawBone):
|
|
|
414
414
|
from viur.core.bones.file import ensureDerived
|
|
415
415
|
for blob_key in blob_keys:
|
|
416
416
|
file_obj = db.Query("file").filter("dlkey =", blob_key) \
|
|
417
|
-
.order(("creationdate"
|
|
417
|
+
.order(db.QueryOrder("creationdate")).getEntry()
|
|
418
418
|
if file_obj:
|
|
419
419
|
ensureDerived(file_obj.key, f"{skel.kindName}_{name}", derive_dict, skel["key"])
|
|
420
420
|
|
|
@@ -28,10 +28,6 @@ _T = t.TypeVar("_T")
|
|
|
28
28
|
Multiple: t.TypeAlias = list[_T] | tuple[_T] | set[_T] | frozenset[_T] # TODO: Refactor for Python 3.12
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
class CaptchaDefaultCredentialsType(t.TypedDict):
|
|
32
|
-
"""Expected type of global captcha credential, see :attr:`Security.captcha_default_credentials`"""
|
|
33
|
-
sitekey: str
|
|
34
|
-
secret: str
|
|
35
31
|
|
|
36
32
|
|
|
37
33
|
class ConfigType:
|
|
@@ -415,7 +411,7 @@ class Security(ConfigType):
|
|
|
415
411
|
x_permitted_cross_domain_policies: t.Optional[t.Literal["none", "master-only", "by-content-type", "all"]] = "none"
|
|
416
412
|
"""Unless set to logical none; ViUR will emit a X-Permitted-Cross-Domain-Policies with each request"""
|
|
417
413
|
|
|
418
|
-
|
|
414
|
+
captcha_default_public_key: t.Optional[str] = None
|
|
419
415
|
"""The default sitekey and secret to use for the :class:`CaptchaBone`.
|
|
420
416
|
If set, must be a dictionary of "sitekey" and "secret".
|
|
421
417
|
"""
|
|
@@ -508,8 +504,6 @@ class Security(ConfigType):
|
|
|
508
504
|
"xXssProtection": "x_xss_protection",
|
|
509
505
|
"xContentTypeOptions": "x_content_type_options",
|
|
510
506
|
"xPermittedCrossDomainPolicies": "x_permitted_cross_domain_policies",
|
|
511
|
-
"captcha_defaultCredentials": "captcha_default_credentials",
|
|
512
|
-
"captcha.defaultCredentials": "captcha_default_credentials",
|
|
513
507
|
}
|
|
514
508
|
|
|
515
509
|
|
|
@@ -27,6 +27,7 @@ from .types import (
|
|
|
27
27
|
Key,
|
|
28
28
|
KeyType,
|
|
29
29
|
QueryDefinition,
|
|
30
|
+
QueryOrder,
|
|
30
31
|
SortOrder,
|
|
31
32
|
)
|
|
32
33
|
from .utils import (
|
|
@@ -51,6 +52,7 @@ __all__ = [
|
|
|
51
52
|
"KEY_SPECIAL_PROPERTY",
|
|
52
53
|
"DATASTORE_BASE_TYPES",
|
|
53
54
|
"SortOrder",
|
|
55
|
+
"QueryOrder",
|
|
54
56
|
"Entity",
|
|
55
57
|
"QueryDefinition",
|
|
56
58
|
"Key",
|
|
@@ -13,6 +13,7 @@ from .types import (
|
|
|
13
13
|
Entity,
|
|
14
14
|
KEY_SPECIAL_PROPERTY,
|
|
15
15
|
QueryDefinition,
|
|
16
|
+
QueryOrder,
|
|
16
17
|
SortOrder,
|
|
17
18
|
TFilters,
|
|
18
19
|
TOrders,
|
|
@@ -282,14 +283,14 @@ class Query(object):
|
|
|
282
283
|
if op in {"<", "<=", ">", ">="}:
|
|
283
284
|
if isinstance(self.queries, list):
|
|
284
285
|
for queryObj in self.queries:
|
|
285
|
-
if not queryObj.orders or queryObj.orders[0]
|
|
286
|
-
queryObj.orders = [(field
|
|
286
|
+
if not queryObj.orders or queryObj.orders[0].name != field:
|
|
287
|
+
queryObj.orders = [QueryOrder(field)] + (queryObj.orders or [])
|
|
287
288
|
else:
|
|
288
|
-
if not self.queries.orders or self.queries.orders[0]
|
|
289
|
-
self.queries.orders = [(field
|
|
289
|
+
if not self.queries.orders or self.queries.orders[0].name != field:
|
|
290
|
+
self.queries.orders = [QueryOrder(field)] + (self.queries.orders or [])
|
|
290
291
|
return self
|
|
291
292
|
|
|
292
|
-
def order(self, *orderings: t.Tuple[str, SortOrder]) -> t.Self:
|
|
293
|
+
def order(self, *orderings: QueryOrder | t.Tuple[str, SortOrder] | str) -> t.Self:
|
|
293
294
|
"""
|
|
294
295
|
Specify a query sorting.
|
|
295
296
|
|
|
@@ -301,7 +302,10 @@ class Query(object):
|
|
|
301
302
|
.. code-block:: python
|
|
302
303
|
|
|
303
304
|
query = Query("Person")
|
|
304
|
-
query.order(
|
|
305
|
+
query.order(
|
|
306
|
+
db.QueryOrder("bday"),
|
|
307
|
+
db.QueryOrder("age", db.SortOrder.Descending),
|
|
308
|
+
)
|
|
305
309
|
|
|
306
310
|
sorts every Person in order of their birthday, starting with January 1.
|
|
307
311
|
People with the same birthday are sorted by age, oldest to youngest.
|
|
@@ -311,7 +315,7 @@ class Query(object):
|
|
|
311
315
|
from scratch.
|
|
312
316
|
|
|
313
317
|
If an inequality filter exists in this Query it must be the first property
|
|
314
|
-
passed to ``order()``.
|
|
318
|
+
passed to ``order()``. Any number of sort orders may be used after the
|
|
315
319
|
inequality filter property. Without inequality filters, any number of
|
|
316
320
|
filters with different orders may be specified.
|
|
317
321
|
|
|
@@ -328,8 +332,9 @@ class Query(object):
|
|
|
328
332
|
made to compare property values across types.
|
|
329
333
|
|
|
330
334
|
|
|
331
|
-
:param orderings: The properties to sort by, in sort order.
|
|
332
|
-
|
|
335
|
+
:param orderings: The properties to sort by, in sort order. Each argument may be a
|
|
336
|
+
:class:`QueryOrder`, a ``(name, direction)`` tuple, or a plain ``str`` (implies
|
|
337
|
+
``SortOrder.Ascending``).
|
|
333
338
|
:returns: Returns the query itself for chaining.
|
|
334
339
|
"""
|
|
335
340
|
if self.queries is None:
|
|
@@ -340,12 +345,20 @@ class Query(object):
|
|
|
340
345
|
orders = []
|
|
341
346
|
for order in orderings:
|
|
342
347
|
if isinstance(order, str):
|
|
343
|
-
order = (order
|
|
344
|
-
|
|
345
|
-
|
|
348
|
+
order = QueryOrder(order)
|
|
349
|
+
elif isinstance(order, QueryOrder):
|
|
350
|
+
pass
|
|
351
|
+
elif (
|
|
352
|
+
isinstance(order, (tuple, list)) and
|
|
353
|
+
len(order) == 2 and
|
|
354
|
+
isinstance(order[0], str) and isinstance(order[1], SortOrder)
|
|
355
|
+
):
|
|
356
|
+
order = QueryOrder(order[0], order[1])
|
|
357
|
+
else:
|
|
346
358
|
raise TypeError(
|
|
347
|
-
f"Invalid ordering {order},
|
|
348
|
-
|
|
359
|
+
f"Invalid ordering {order!r}, expected a (str, SortOrder) tuple or QueryOrder."
|
|
360
|
+
f' Try: `QueryOrder("{order}")`'
|
|
361
|
+
)
|
|
349
362
|
orders.append(order)
|
|
350
363
|
|
|
351
364
|
if self._orderHook is not None:
|
|
@@ -445,7 +458,7 @@ class Query(object):
|
|
|
445
458
|
q = self.queries[0]
|
|
446
459
|
return base64.urlsafe_b64encode(q.currentCursor).decode("ASCII") if q.currentCursor else None
|
|
447
460
|
|
|
448
|
-
def get_orders(self) -> t.List[
|
|
461
|
+
def get_orders(self) -> t.List[QueryOrder] | None:
|
|
449
462
|
"""
|
|
450
463
|
Get the orders from this query.
|
|
451
464
|
|
|
@@ -502,10 +515,10 @@ class Query(object):
|
|
|
502
515
|
return self._resort_result(res, {}, self.queries[0].orders)
|
|
503
516
|
|
|
504
517
|
def _resort_result(
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
518
|
+
self,
|
|
519
|
+
entities: t.List[Entity],
|
|
520
|
+
filters: t.Dict[str, DATASTORE_BASE_TYPES],
|
|
521
|
+
orders: t.List[QueryOrder],
|
|
509
522
|
) -> t.List[Entity]:
|
|
510
523
|
"""
|
|
511
524
|
Internal helper that takes a (deduplicated) list of entities that has been fetched from different internal
|
|
@@ -550,8 +563,8 @@ class Query(object):
|
|
|
550
563
|
if "<" in end or ">" in end:
|
|
551
564
|
ineqFilter = k.split(" ")[0]
|
|
552
565
|
break
|
|
553
|
-
if ineqFilter and (not orders or not orders[0]
|
|
554
|
-
orders = [(ineqFilter
|
|
566
|
+
if ineqFilter and (not orders or not orders[0].name == ineqFilter):
|
|
567
|
+
orders = [QueryOrder(ineqFilter)] + (orders or [])
|
|
555
568
|
|
|
556
569
|
for orderField, direction in orders[::-1]:
|
|
557
570
|
if orderField == KEY_SPECIAL_PROPERTY:
|
|
@@ -572,10 +585,10 @@ class Query(object):
|
|
|
572
585
|
"""
|
|
573
586
|
resultList = list(resultList)
|
|
574
587
|
if (
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
588
|
+
resultList
|
|
589
|
+
and resultList[0].key.kind != self.origKind
|
|
590
|
+
and resultList[0].key.parent
|
|
591
|
+
and resultList[0].key.parent.kind == self.origKind
|
|
579
592
|
):
|
|
580
593
|
return list(get(list(dict.fromkeys([x.key.parent for x in resultList]))))
|
|
581
594
|
|
|
@@ -207,14 +207,12 @@ def run_single_filter(query: QueryDefinition, limit: int, keys_only: bool) -> t.
|
|
|
207
207
|
|
|
208
208
|
if query.orders:
|
|
209
209
|
hasInvertedOrderings = any(
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
for x in query.orders
|
|
213
|
-
]
|
|
210
|
+
order.order in (SortOrder.InvertedAscending, SortOrder.InvertedDescending)
|
|
211
|
+
for order in query.orders
|
|
214
212
|
)
|
|
215
213
|
qry.order = [
|
|
216
|
-
|
|
217
|
-
for
|
|
214
|
+
order.name if order.order in (SortOrder.Ascending, SortOrder.InvertedDescending) else f"-{order.name}"
|
|
215
|
+
for order in query.orders
|
|
218
216
|
]
|
|
219
217
|
|
|
220
218
|
if query.distinct:
|
|
@@ -36,6 +36,12 @@ class SortOrder(enum.Enum):
|
|
|
36
36
|
"""Fetch A->Z, then flip the results (useful in pagination)"""
|
|
37
37
|
|
|
38
38
|
|
|
39
|
+
class QueryOrder(t.NamedTuple):
|
|
40
|
+
"""A named tuple describing a single sort order for a datastore query."""
|
|
41
|
+
name: str
|
|
42
|
+
order: SortOrder = SortOrder.Ascending
|
|
43
|
+
|
|
44
|
+
|
|
39
45
|
class Key(Datastore_key):
|
|
40
46
|
"""
|
|
41
47
|
The python representation of one datastore key. Unlike the original implementation, we don't store a
|
|
@@ -165,7 +171,7 @@ KeyType: t.TypeAlias = Key | str | int
|
|
|
165
171
|
Alias that describes a key-type.
|
|
166
172
|
"""
|
|
167
173
|
|
|
168
|
-
TOrders: t.TypeAlias = list[
|
|
174
|
+
TOrders: t.TypeAlias = list[QueryOrder]
|
|
169
175
|
TFilters: t.TypeAlias = dict[str, DATASTORE_BASE_TYPES | list[DATASTORE_BASE_TYPES]]
|
|
170
176
|
|
|
171
177
|
|
|
@@ -748,7 +748,8 @@ class File(Tree):
|
|
|
748
748
|
return ""
|
|
749
749
|
|
|
750
750
|
if isinstance(file, str):
|
|
751
|
-
file = db.Query("file").filter("dlkey =", file).order(
|
|
751
|
+
file = db.Query("file").filter("dlkey =", file).order(
|
|
752
|
+
db.QueryOrder("creationdate")).getEntry()
|
|
752
753
|
|
|
753
754
|
if not file:
|
|
754
755
|
return ""
|
|
@@ -120,6 +120,11 @@ class Script(Tree):
|
|
|
120
120
|
super().onEdit(skelType, skel)
|
|
121
121
|
|
|
122
122
|
def onEdited(self, skelType, skel):
|
|
123
|
+
old_path = skel["path"]
|
|
124
|
+
self.update_path(skel)
|
|
125
|
+
if skel["path"] != old_path:
|
|
126
|
+
skel.patch({"path": skel["path"]})
|
|
127
|
+
|
|
123
128
|
if skelType == "node":
|
|
124
129
|
self.update_path_recursive("node", skel["path"], skel["key"])
|
|
125
130
|
self.update_path_recursive("leaf", skel["path"], skel["key"])
|
|
@@ -158,9 +163,9 @@ class Script(Tree):
|
|
|
158
163
|
if not parent_skel.read(key) or parent_skel["key"] == skel["parentrepo"]:
|
|
159
164
|
break
|
|
160
165
|
|
|
161
|
-
|
|
166
|
+
if parent_skel["name"]:
|
|
167
|
+
path.insert(0, parent_skel["name"])
|
|
162
168
|
key = parent_skel["parententry"]
|
|
163
|
-
|
|
164
169
|
skel["path"] = "/".join(path)
|
|
165
170
|
|
|
166
171
|
@exposed
|
|
@@ -10,7 +10,7 @@ from viur.core.decorators import exposed
|
|
|
10
10
|
from viur.core.bones import *
|
|
11
11
|
from viur.core.i18n import KINDNAME, initializeTranslations, systemTranslations, translate
|
|
12
12
|
from viur.core.prototypes.list import List
|
|
13
|
-
from viur.core.skeleton import Skeleton, ViurTagsSearchAdapter
|
|
13
|
+
from viur.core.skeleton import Skeleton, SkeletonInstance, ViurTagsSearchAdapter
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class Creator(enum.Enum):
|
|
@@ -211,7 +211,7 @@ class Translation(List):
|
|
|
211
211
|
"admin": "*",
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
def addSkel(self):
|
|
214
|
+
def addSkel(self) -> SkeletonInstance["TranslationSkel"]:
|
|
215
215
|
"""
|
|
216
216
|
Returns a custom TranslationSkel where the name is editable.
|
|
217
217
|
The name becomes part of the key.
|
|
@@ -557,7 +557,7 @@ class UserPassword(UserPrimaryAuthentication):
|
|
|
557
557
|
def canAdd(self) -> bool:
|
|
558
558
|
return self.registrationEnabled
|
|
559
559
|
|
|
560
|
-
def addSkel(self):
|
|
560
|
+
def addSkel(self) -> skeleton.SkeletonInstance["UserSkel"]:
|
|
561
561
|
"""
|
|
562
562
|
Prepare the add-Skel for rendering.
|
|
563
563
|
Currently only calls self._user_module.addSkel() and sets skel["status"] depending on
|
|
@@ -1350,7 +1350,7 @@ class User(List):
|
|
|
1350
1350
|
|
|
1351
1351
|
return ret
|
|
1352
1352
|
|
|
1353
|
-
def addSkel(self):
|
|
1353
|
+
def addSkel(self) -> skeleton.SkeletonInstance["UserSkel"]:
|
|
1354
1354
|
skel = super().addSkel().clone()
|
|
1355
1355
|
|
|
1356
1356
|
if self.is_admin(current.user.get()):
|
|
@@ -1377,7 +1377,7 @@ class User(List):
|
|
|
1377
1377
|
skel.name.readOnly = False # Don't enforce readonly name in user/add
|
|
1378
1378
|
return skel
|
|
1379
1379
|
|
|
1380
|
-
def editSkel(self, *args, **kwargs):
|
|
1380
|
+
def editSkel(self, *args, **kwargs) -> skeleton.SkeletonInstance["UserSkel"]:
|
|
1381
1381
|
skel = super().editSkel().clone()
|
|
1382
1382
|
|
|
1383
1383
|
if "password" in skel:
|