From 28ef5f81e2b7d4c79f8067307dfde66c591699ba Mon Sep 17 00:00:00 2001 From: saxix Date: Tue, 2 Jul 2024 12:34:57 +0200 Subject: [PATCH] add js validation --- README.md | 2 +- pdm.lock | 41 ++++++++++++++++++- pyproject.toml | 2 + src/hope_flex_fields/models.py | 6 ++- src/hope_flex_fields/validators.py | 41 +++++++++++++++++++ tests/test_jsvalidator.py | 66 ++++++++++++++++++++++++++++++ tests/test_validate.py | 6 ++- 7 files changed, 158 insertions(+), 6 deletions(-) create mode 100644 src/hope_flex_fields/validators.py create mode 100644 tests/test_jsvalidator.py diff --git a/README.md b/README.md index 87132ab..86da7bb 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # HOPR FlexFields [![Test](https://github.com/unicef/hope-flex-fields/actions/workflows/test.yml/badge.svg)](https://github.com/unicef/hope-flex-fields/actions/workflows/test.yml) - +[![codecov](https://codecov.io/gh/unicef/hope-flex-fields/graph/badge.svg?token=GSYAH4IEUK)](https://codecov.io/gh/unicef/hope-flex-fields) ## Install CSP_SCRIPT_SRC = [ diff --git a/pdm.lock b/pdm.lock index 4df1ef7..bcb295e 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev"] strategy = ["cross_platform", "inherit_metadata"] lock_version = "4.4.1" -content_hash = "sha256:fa212da36570eeddcac91b1bb982d8dd181b0661eb0f0e7b0ccc2581a885aecf" +content_hash = "sha256:7a3ae33dd96b1b19033c7d0f8c69380d3e940b4a68b01f52166883833c312ff2" [[package]] name = "asgiref" @@ -74,6 +74,10 @@ dependencies = [ "platformdirs>=2", ] files = [ + {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, + {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, + {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, + {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, @@ -150,7 +154,7 @@ version = "0.4.6" requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" summary = "Cross-platform colored terminal text." groups = ["dev"] -marker = "sys_platform == \"win32\" or platform_system == \"Windows\"" +marker = "platform_system == \"Windows\" or sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -465,6 +469,17 @@ files = [ {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, ] +[[package]] +name = "jsonpickle" +version = "3.2.2" +requires_python = ">=3.7" +summary = "Python library for serializing arbitrary object graphs into JSON" +groups = ["default"] +files = [ + {file = "jsonpickle-3.2.2-py3-none-any.whl", hash = "sha256:87cd82d237fd72c5a34970e7222dddc0accc13fddf49af84111887ed9a9445aa"}, + {file = "jsonpickle-3.2.2.tar.gz", hash = "sha256:d425fd2b8afe9f5d7d57205153403fbf897782204437882a477e8eed60930f8c"}, +] + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -501,6 +516,23 @@ files = [ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[[package]] +name = "mini-racer" +version = "0.12.4" +requires_python = ">=3.8" +summary = "Minimal, modern embedded V8 for Python." +groups = ["default"] +files = [ + {file = "mini_racer-0.12.4-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:bce8a3cee946575a352f5e65335903bc148da42c036d0c738ac67e931600e455"}, + {file = "mini_racer-0.12.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:56c832e6ac2db6a304d1e8e80030615297aafbc6940f64f3479af4ba16abccd5"}, + {file = "mini_racer-0.12.4-py3-none-manylinux_2_31_aarch64.whl", hash = "sha256:b82c4bd2976e280ed0a72c9c2de01b13f18ccfbe6f4892cbc22aae04410fac3c"}, + {file = "mini_racer-0.12.4-py3-none-manylinux_2_31_x86_64.whl", hash = "sha256:69a1c44d02a9069b881684cef15a2d747fe0743df29eadc881fda7002aae5fd2"}, + {file = "mini_racer-0.12.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:499dbc267dfe60e954bc1b6c3787f7b10fc41fe1975853c9a6ddb55eb83dc4d9"}, + {file = "mini_racer-0.12.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:231f949f5787d18351939f1fe59e5a6fe134bccb5ecf8f836b9beab69d91c8d9"}, + {file = "mini_racer-0.12.4-py3-none-win_amd64.whl", hash = "sha256:9446e3bd6a4eb9fbedf1861326f7476080995a31c9b69308acef17e5b7ecaa1b"}, + {file = "mini_racer-0.12.4.tar.gz", hash = "sha256:84c67553ce9f3736d4c617d8a3f882949d37a46cfb47fe11dab33dd6704e62a4"}, +] + [[package]] name = "mypy" version = "1.10.1" @@ -512,6 +544,11 @@ dependencies = [ "typing-extensions>=4.1.0", ] files = [ + {file = "mypy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bd6f629b67bb43dc0d9211ee98b96d8dabc97b1ad38b9b25f5e4c4d7569a0c6a"}, + {file = "mypy-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1bbb3a6f5ff319d2b9d40b4080d46cd639abe3516d5a62c070cf0114a457d84"}, + {file = "mypy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8edd4e9bbbc9d7b79502eb9592cab808585516ae1bcc1446eb9122656c6066f"}, + {file = "mypy-1.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6166a88b15f1759f94a46fa474c7b1b05d134b1b61fca627dd7335454cc9aa6b"}, + {file = "mypy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:5bb9cd11c01c8606a9d0b83ffa91d0b236a0e91bc4126d9ba9ce62906ada868e"}, {file = "mypy-1.10.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d8681909f7b44d0b7b86e653ca152d6dff0eb5eb41694e163c6092124f8246d7"}, {file = "mypy-1.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:378c03f53f10bbdd55ca94e46ec3ba255279706a6aacaecac52ad248f98205d3"}, {file = "mypy-1.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bacf8f3a3d7d849f40ca6caea5c055122efe70e81480c8328ad29c55c69e93e"}, diff --git a/pyproject.toml b/pyproject.toml index 4c76820..15c5d3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,8 @@ dependencies = [ "django-admin-extra-buttons>=1.5.8", "django-jsoneditor>=0.2.4", "djangorestframework>=3.15.1", + "mini-racer>=0.12.4", + "jsonpickle>=3.2.2", ] requires-python = ">=3.11" readme = "README.md" diff --git a/src/hope_flex_fields/models.py b/src/hope_flex_fields/models.py index 9653b5b..6439aa3 100644 --- a/src/hope_flex_fields/models.py +++ b/src/hope_flex_fields/models.py @@ -5,6 +5,7 @@ from django import forms from django.core.exceptions import ValidationError from django.db import models +from django.utils.text import slugify from django_regex.fields import RegexField from django_regex.validators import RegexValidator @@ -53,11 +54,14 @@ def clean(self): try: self.set_default_arguments() self.get_field() + self.name = slugify(str(self.name)) except TypeError as e: raise ValidationError(e) def set_default_arguments(self): - stored = self.attrs or {} + if not isinstance(self.attrs, dict): + self.attrs = {} + stored = self.attrs sig: inspect.Signature = inspect.signature(self.field_type) defaults = { k.name: k.default diff --git a/src/hope_flex_fields/validators.py b/src/hope_flex_fields/validators.py new file mode 100644 index 0000000..b24a4d6 --- /dev/null +++ b/src/hope_flex_fields/validators.py @@ -0,0 +1,41 @@ +import json + +from django.core.exceptions import ValidationError +from django.core.validators import BaseValidator +from django.utils.translation import gettext as _ + +from py_mini_racer import JSArray, JSObject, MiniRacer + + +class JsValidator(BaseValidator): + + @property + def code(self): + return self.limit_value + + def __call__(self, value): + ctx = MiniRacer() + + pickled = json.dumps(value or "") + base = f"var value = {pickled};" + ctx.eval(base) + ret = ctx.eval(self.code) + + # try: + # ret = jsonpickle.decode(result) + # except (JSONDecodeError, TypeError): + # ret = result + + if isinstance(ret, JSArray): + raise ValidationError(list(ret)) + + if isinstance(ret, JSObject): + errors = {s: k for s, k in ret.items()} + raise ValidationError(errors) + + if isinstance(ret, str): + raise ValidationError(_(ret)) + elif isinstance(ret, bool) and not ret: + raise ValidationError(_("Please insert a valid value")) + + return True diff --git a/tests/test_jsvalidator.py b/tests/test_jsvalidator.py new file mode 100644 index 0000000..2c3567f --- /dev/null +++ b/tests/test_jsvalidator.py @@ -0,0 +1,66 @@ +from django.core.exceptions import ValidationError + +import pytest + +from hope_flex_fields.validators import JsValidator + + +def test_jsvalidator(): + code = "result=value*2" + v = JsValidator(code) + assert v(22) + + +# +# def test_jsvalidator_error(): +# code = 'value=2' +# v = JsValidator(code) +# with pytest.raises(ValueError) as e: +# v(22) +# assert e.value.messages == [' Validator code must returns something'] + + +def test_jsvalidator_fail(): + code = "result=value==2" + v = JsValidator(code) + with pytest.raises(ValidationError) as e: + v(22) + assert e.value.messages == ["Please insert a valid value"] + + +def test_jsvalidator_return_dict(): + code = 'result={"value": "error"}' + v = JsValidator(code) + with pytest.raises(ValidationError) as e: + v(22) + assert e.value.message_dict == {"value": ["error"]} + + +def test_jsvalidator_return_tuple(): + code = 'result=["1","2","3"]' + v = JsValidator(code) + with pytest.raises(ValidationError) as e: + v(22) + assert e.value.messages == ["1", "2", "3"] + + +def test_jsvalidator_fail_custom_message(): + code = """ +if (value <2){ + result = "Provided number must be less than or equal to 2!"; +}else if (value > 5){ + result = "Provided number must be greater than or equal to 5!"; +} +""" + v = JsValidator(code) + with pytest.raises(ValidationError) as e: + v(22) + assert e.value.messages == ["Provided number must be greater than or equal to 5!"] + + +def test_jsvalidator_return_error(): + code = "result={}" + v = JsValidator(code) + with pytest.raises(ValidationError) as e: + v(22) + assert e.value.message_dict == {} diff --git a/tests/test_validate.py b/tests/test_validate.py index f0ca06d..d62e724 100644 --- a/tests/test_validate.py +++ b/tests/test_validate.py @@ -11,7 +11,10 @@ def config(db): from hope_flex_fields.models import FieldDefinition, Fieldset, FieldsetField fd1 = FieldDefinition.objects.create( - name="IntField", field_type=forms.IntegerField, attrs={"min_value": 1} + name="IntField", + field_type=forms.IntegerField, + attrs={"min_value": 1}, + validation="", ) fd2 = FieldDefinition.objects.create( name="FloatField", field_type=forms.FloatField, attrs={"min_value": 1} @@ -24,7 +27,6 @@ def config(db): def test_validate_row(config): - # try to validate json formatted data against a FieldSet data = {"int": 1, "float": 1.1, "str": "string"} fs: Fieldset = config["fs"]