├── .envrc
├── tests
├── data
│ ├── test_data_src_url_form_no_value.json
│ ├── test_datagrid_in_panel_one_row_form.json
│ ├── test_conditional_visibility_json_logic_hide_secret.json
│ ├── test_conditional_visibility_simple_hide_password.json
│ ├── test_conditional_visibility_json_logic_show_secret.json
│ ├── test_conditional_visibility_simple_show_textfield.json
│ ├── test_conditional_visibility_simple_show_selectboxes.json
│ ├── test_conditional_visibility_nested_json_logic_hide_secret.json
│ ├── test_example_form_empty.json
│ ├── test_conditional_visibility_nested_json_logic_hide_global_secret_only.json
│ ├── test_conditional_visibility_nested_json_logic_show_global_secret_only.json
│ ├── test_conditional_visibility_nested_json_logic_show_secret.json
│ ├── test_example_form_validation_errors.json
│ ├── test_data_src_url_builder.json
│ ├── test_resources_submission.json
│ ├── test_conditional_visibility_json_logic_builder.json
│ └── test_example_form_check_default.json
├── __init__.py
├── test_default_value_component.py
├── test_view_render_component.py
├── test_builder.py
├── test_submission.py
├── test_component_panel.py
├── test_common.py
├── utils.py
├── test_component_resource.py
├── test_component_email.py
├── test_datagrid_in_panel.py
├── test_component_number.py
├── test_component_phoneNumber.py
├── test_component_tabs.py
├── test_component_class_mapping.py
├── test_component_address.py
├── test_component_select_one_data_src_url.py
├── test_component_table.py
├── test_performance_nested_components.py
├── test_component_radio.py
├── test_component_textfield.py
├── test_component_select_multiple.py
├── test_component_survey.py
├── test_conditional_visibility_json_logic.py
├── test_component_select_one.py
├── test_component.py
├── test_validation_error_simple.py
├── test_component_datagrid.py
├── test_conditional_visibility_simple.py
├── test_component_selectboxes.py
├── test_component_datetime.py
├── test_component_file_storage_base64.py
├── test_component_editgrid.py
├── test_component_day.py
└── test_component_file_storage_url.py
├── .gitignore
├── formiodata
├── components
│ ├── __init__.py
│ ├── time.py
│ ├── url.py
│ ├── email.py
│ ├── number.py
│ ├── checkbox.py
│ ├── currency.py
│ ├── password.py
│ ├── signature.py
│ ├── textarea.py
│ ├── textfield.py
│ ├── layout_base.py
│ ├── phoneNumber.py
│ ├── phone_number.py
│ ├── content.py
│ ├── datagrid.py
│ ├── editgrid.py
│ ├── htmlelement.py
│ ├── button.py
│ ├── fieldset.py
│ ├── selectboxes.py
│ ├── tabs.py
│ ├── panel.py
│ ├── file.py
│ ├── table.py
│ ├── radio.py
│ ├── address.py
│ ├── survey.py
│ ├── datetime.py
│ ├── resource.py
│ ├── select.py
│ ├── columns.py
│ ├── day.py
│ ├── grid_base.py
│ └── component.py
├── utils.py
├── builder.py
└── form.py
├── shell.nix
├── nix
└── pkgs.nix
├── pyproject.toml
├── LICENSE
├── .github
└── workflows
│ └── unittests.yml
├── CHANGELOG.md
└── README.md
/.envrc:
--------------------------------------------------------------------------------
1 | use nix
2 |
--------------------------------------------------------------------------------
/tests/data/test_data_src_url_form_no_value.json:
--------------------------------------------------------------------------------
1 | {"country": ""}
2 |
--------------------------------------------------------------------------------
/tests/data/test_datagrid_in_panel_one_row_form.json:
--------------------------------------------------------------------------------
1 | {"dataGrid": [{"textField": "testing row 1"}], "submit": true}
2 |
--------------------------------------------------------------------------------
/tests/data/test_conditional_visibility_json_logic_hide_secret.json:
--------------------------------------------------------------------------------
1 | {"username": "wrong", "password": "incorrect", "submit": true}
2 |
--------------------------------------------------------------------------------
/tests/data/test_conditional_visibility_simple_hide_password.json:
--------------------------------------------------------------------------------
1 | {
2 | "submit": true,
3 | "textField": "hide!"
4 | }
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.egg-info
2 | *.py[co]
3 | __pycache__
4 | env
5 | dist/
6 | build/
7 | tmp
8 | .direnv
9 | .idea
10 | poetry.lock
--------------------------------------------------------------------------------
/formiodata/components/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
--------------------------------------------------------------------------------
/tests/data/test_conditional_visibility_json_logic_show_secret.json:
--------------------------------------------------------------------------------
1 | {"username": "user", "password": "secret", "submit": true, "secret": "Secret message"}
2 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | import os, sys
5 | sys.path.append(os.path.dirname(os.path.realpath(__file__)))
6 |
--------------------------------------------------------------------------------
/tests/data/test_conditional_visibility_simple_show_textfield.json:
--------------------------------------------------------------------------------
1 | {
2 | "maybeTextField": "maybe yes",
3 | "submit": true,
4 | "maybePassword": "hunter2",
5 | "textField": "show!"
6 | }
7 |
--------------------------------------------------------------------------------
/shell.nix:
--------------------------------------------------------------------------------
1 | {
2 | pkgs ? import ./nix/pkgs.nix
3 | }:
4 |
5 | pkgs.mkShell {
6 | name = "pyhon-formio-data";
7 | buildInputs = with pkgs; [
8 | python314
9 | poetry
10 | ];
11 | }
12 |
--------------------------------------------------------------------------------
/formiodata/components/time.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from .component import Component
5 |
6 |
7 | class timeComponent(Component):
8 | pass
9 |
--------------------------------------------------------------------------------
/formiodata/components/url.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from .component import Component
5 |
6 |
7 | class urlComponent(Component):
8 | pass
9 |
--------------------------------------------------------------------------------
/nix/pkgs.nix:
--------------------------------------------------------------------------------
1 | import (builtins.fetchTarball {
2 | url = "https://github.com/NixOS/nixpkgs/archive/0e6684e6c5755325f801bda1751a8a4038145d7d.tar.gz";
3 | sha256 = "sha256-6tooT142NLcFjt24Gi4B0G1pgWLvfw7y93sYEfSHlLI=";
4 | }) {}
5 |
--------------------------------------------------------------------------------
/formiodata/components/email.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from .component import Component
5 |
6 |
7 | class emailComponent(Component):
8 | pass
9 |
--------------------------------------------------------------------------------
/formiodata/components/number.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from .component import Component
5 |
6 |
7 | class numberComponent(Component):
8 | pass
9 |
--------------------------------------------------------------------------------
/formiodata/components/checkbox.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from .component import Component
5 |
6 |
7 | class checkboxComponent(Component):
8 | pass
9 |
--------------------------------------------------------------------------------
/formiodata/components/currency.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from .component import Component
5 |
6 |
7 | class currencyComponent(Component):
8 | pass
9 |
--------------------------------------------------------------------------------
/formiodata/components/password.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from .component import Component
5 |
6 |
7 | class passwordComponent(Component):
8 | pass
9 |
--------------------------------------------------------------------------------
/formiodata/components/signature.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from .component import Component
5 |
6 |
7 | class signatureComponent(Component):
8 | pass
9 |
--------------------------------------------------------------------------------
/formiodata/components/textarea.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from .component import Component
5 |
6 |
7 | class textareaComponent(Component):
8 | pass
9 |
--------------------------------------------------------------------------------
/formiodata/components/textfield.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from .component import Component
5 |
6 |
7 | class textfieldComponent(Component):
8 | pass
9 |
--------------------------------------------------------------------------------
/formiodata/components/layout_base.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from .component import Component
5 |
6 |
7 | class layoutComponentBase(Component):
8 | pass
9 |
--------------------------------------------------------------------------------
/formiodata/components/phoneNumber.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from .component import Component
5 |
6 |
7 | class phoneNumberComponent(Component):
8 | pass
9 |
--------------------------------------------------------------------------------
/formiodata/components/phone_number.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from .component import Component
5 |
6 |
7 | class phoneNumberComponent(Component):
8 | pass
9 |
--------------------------------------------------------------------------------
/formiodata/components/content.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from .component import Component
5 |
6 |
7 | class contentComponent(Component):
8 | # XXX should we move the initEmpty from base
9 | pass
10 |
--------------------------------------------------------------------------------
/formiodata/components/datagrid.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from .grid_base import baseGridComponent
5 |
6 |
7 | class datagridComponent(baseGridComponent):
8 | # XXX should we move the initEmpty from base
9 | pass
10 |
--------------------------------------------------------------------------------
/formiodata/components/editgrid.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from .grid_base import baseGridComponent
5 |
6 |
7 | class editgridComponent(baseGridComponent):
8 |
9 | @property
10 | def initEmpty(self):
11 | return not self.raw.get('openWhenEmpty')
12 |
--------------------------------------------------------------------------------
/tests/data/test_conditional_visibility_simple_show_selectboxes.json:
--------------------------------------------------------------------------------
1 | {
2 | "textField": "",
3 | "maybePassword": "",
4 | "jobArea": {
5 | "finance": false,
6 | "sales": false,
7 | "technology": true
8 | },
9 | "submit": true,
10 | "technology": {
11 | "analyst": false,
12 | "developer": true,
13 | "devops": false
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/formiodata/components/htmlelement.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from .component import Component
5 |
6 |
7 | class htmlelementComponent(Component):
8 |
9 | @property
10 | def html(self):
11 | html = '<%s>%s%s>' % (self.raw['tag'], self.raw['content'], self.raw['tag'])
12 | return html
13 |
--------------------------------------------------------------------------------
/formiodata/components/button.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from .component import Component
5 |
6 |
7 | class buttonComponent(Component):
8 |
9 | @property
10 | def is_form_component(self):
11 | return False
12 |
13 | def load_data(self, data, is_form=False):
14 | # just bypass this
15 | pass
16 |
--------------------------------------------------------------------------------
/tests/test_default_value_component.py:
--------------------------------------------------------------------------------
1 | from formiodata.form import Form
2 | from test_component import ComponentTestCase
3 |
4 |
5 | class valueDefaultEmailComponent(ComponentTestCase):
6 |
7 | def test_default_value(self):
8 | self.form_check_default = Form(self.form_json_check_default, self.builder)
9 | # EmailComponent
10 | email = self.form_check_default.input_components['email']
11 | self.assertEqual(email.value, 'yourmail@yourlife.io')
12 |
--------------------------------------------------------------------------------
/tests/data/test_conditional_visibility_nested_json_logic_hide_secret.json:
--------------------------------------------------------------------------------
1 | {
2 | "username": "wrong",
3 | "password": "incorrect",
4 | "username1": "wrong",
5 | "password1": "incorrect",
6 | "username2": "wrong",
7 | "password2": "incorrect",
8 | "dataGrid": [
9 | {
10 | "username3": "wrong",
11 | "password3": "incorrect"
12 | },
13 | {
14 | "username3": "wrong",
15 | "password3": "incorrect"
16 | }
17 | ],
18 | "username3": "",
19 | "password3": "",
20 | "submit": true
21 | }
22 |
--------------------------------------------------------------------------------
/tests/test_view_render_component.py:
--------------------------------------------------------------------------------
1 | from formiodata.form import Form
2 | from test_component import ComponentTestCase
3 |
4 |
5 | class viewRenderEmailComponent(ComponentTestCase):
6 |
7 | def test_view_render(self):
8 | self.form_check_default = Form(self.form_json_check_default, self.builder)
9 | self.form_check_default.render_components()
10 | # EmailComponent
11 | email = self.form_check_default.input_components['email']
12 | self.assertEqual(email.html_component, '
yourmail@yourlife.io
')
13 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "formio-data"
3 | version = "2.1.6"
4 | homepage = "https://github.com/novacode-nl/python-formio-data"
5 | description = "formio.js JSON-data API"
6 | readme = "README.md"
7 | authors = ["Bob Leers "]
8 | license = "MIT"
9 | packages = [ { include = "formiodata/**/*.py" } ]
10 | exclude = ["tests/*"]
11 |
12 | [tool.poetry.dependencies]
13 | python = "^3.6"
14 |
15 | python-dateutil = { version = "^2.8.2", python = "<= 3.6" }
16 | requests = "*"
17 | json_logic_qubit = { version = "^0.9.1", optional = true }
18 |
19 | [tool.poetry.extras]
20 | json_logic = ["json_logic_qubit"]
21 |
--------------------------------------------------------------------------------
/tests/data/test_example_form_empty.json:
--------------------------------------------------------------------------------
1 | {
2 | "firstName": "",
3 | "email": "yourmail@yourlife.io",
4 | "birthdate": "",
5 | "appointmentDateTime": "",
6 | "lastName": "",
7 | "phoneNumber": "",
8 | "cardinalDirection": "",
9 | "favouriteSeason": "",
10 | "favouriteFood": [],
11 | "monthDayYear": "00/00/0000",
12 | "monthYear": "00/00/0000",
13 | "dayMonthYear": "00/00/0000",
14 | "dayMonth": "00/00/0000",
15 | "day": "00/00/0000",
16 | "month": "00/00/0000",
17 | "year": "00/00/0000",
18 | "survey": null,
19 | "signature": "",
20 | "dataGrid": [
21 | {
22 | "textField": "",
23 | "checkbox": false
24 | }
25 | ],
26 | "uploadBase64": [],
27 | "uploadUrl": [],
28 | "submit": true
29 | }
30 |
--------------------------------------------------------------------------------
/tests/test_builder.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from test_common import CommonTestCase
5 | from formiodata.builder import Builder
6 |
7 |
8 | class BuilderTestCase(CommonTestCase):
9 |
10 | def _builder(self):
11 | return Builder(self.builder_json)
12 |
13 | def test_builder(self):
14 | Builder(self.builder_json)
15 |
16 | def test_components(self):
17 | builder = self._builder()
18 | # NOTE: submit button is not considered a form component
19 | keys = ('firstName', 'email', 'lastName', 'phoneNumber', 'survey', 'signature')
20 | for k in keys:
21 | self.assertIn(k, builder.input_components.keys())
22 |
--------------------------------------------------------------------------------
/tests/data/test_conditional_visibility_nested_json_logic_hide_global_secret_only.json:
--------------------------------------------------------------------------------
1 | {
2 | "username": "wrong",
3 | "password": "incorrect",
4 | "username1": "user",
5 | "password1": "secret",
6 | "username2": "user",
7 | "password2": "secret",
8 | "dataGrid": [
9 | {
10 | "username3": "user",
11 | "password3": "secret",
12 | "secret3": "Secret message"
13 | },
14 | {
15 | "username3": "user",
16 | "password3": "secret",
17 | "secret3": "Secret message"
18 | }
19 | ],
20 | "secret1": "Secret message",
21 | "secret2": "Secret message",
22 | "submit": true,
23 | "password3": "",
24 | "username3": ""
25 | }
26 |
--------------------------------------------------------------------------------
/tests/data/test_conditional_visibility_nested_json_logic_show_global_secret_only.json:
--------------------------------------------------------------------------------
1 | {
2 | "username": "user",
3 | "password": "secret",
4 | "secret": "Secret message",
5 | "username1": "wrong",
6 | "password1": "incorrect",
7 | "username2": "wrong",
8 | "password2": "incorrect",
9 | "dataGrid": [
10 | {
11 | "username3": "wrong",
12 | "password3": "incorrect",
13 | "globalSecret": "Another secret message"
14 | },
15 | {
16 | "username3": "wrong",
17 | "password3": "incorrect",
18 | "globalSecret": "Another secret message"
19 | }
20 | ],
21 | "username3": "",
22 | "password3": "",
23 | "submit": true
24 | }
25 |
26 |
--------------------------------------------------------------------------------
/formiodata/components/fieldset.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from .layout_base import layoutComponentBase
5 |
6 |
7 | class fieldsetComponent(layoutComponentBase):
8 |
9 | def load_data(self, data, is_form=False):
10 | for component in self.raw.get('components', []):
11 | # Only determine and load class if component type.
12 | if 'type' in component:
13 | component_obj = self.builder.get_component_object(component)
14 | component_obj.load(
15 | self.child_component_owner,
16 | parent=self,
17 | data=data,
18 | all_data=self._all_data,
19 | is_form=is_form,
20 | )
21 |
--------------------------------------------------------------------------------
/tests/data/test_conditional_visibility_nested_json_logic_show_secret.json:
--------------------------------------------------------------------------------
1 | {
2 | "username": "user",
3 | "password": "secret",
4 | "username1": "user",
5 | "password1": "secret",
6 | "username2": "user",
7 | "password2": "secret",
8 | "dataGrid": [
9 | {
10 | "username3": "user",
11 | "password3": "secret",
12 | "secret3": "Secret message",
13 | "globalSecret": "Another secret message"
14 | },
15 | {
16 | "username3": "user",
17 | "password3": "secret",
18 | "secret3": "Secret message",
19 | "globalSecret": "Another secret message"
20 | }
21 | ],
22 | "password3": "",
23 | "username3": "",
24 | "secret": "Secret message",
25 | "secret1": "Secret message",
26 | "secret2": "Secret message",
27 | "submit": true
28 | }
29 |
--------------------------------------------------------------------------------
/tests/test_submission.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from test_common import CommonTestCase
5 | from formiodata.form import Form
6 |
7 |
8 | class FormTestCase(CommonTestCase):
9 |
10 | def x_test_constructor_validation_ok(self):
11 | sub = Form(self.form_json, None, self.builder_json)
12 | self.assertIsInstance(sub, Form)
13 |
14 | sub = Form(self.form_json, self.builder)
15 | self.assertIsInstance(sub, Form)
16 | # self.assertIsInstance(self.form.store, FormStore)
17 |
18 | def x_test_constructor_validation_fails(self):
19 | with self.assertRaisesRegexp(Exception, "Provide either the argument: builder or builder_schema_json."):
20 | Form(self.form_json)
21 |
22 | with self.assertRaisesRegexp(Exception, "Constructor accepts either builder or builder_schema_json."):
23 | Form(self.form_json, self.builder, self.builder_schema_json)
24 |
--------------------------------------------------------------------------------
/tests/data/test_example_form_validation_errors.json:
--------------------------------------------------------------------------------
1 | {
2 | "firstName": "",
3 | "email": "yourmail@yourlife.io",
4 | "birthdate": "",
5 | "appointmentDateTime": "",
6 | "lastName": "",
7 | "phoneNumber": "",
8 | "cardinalDirection": "",
9 | "favouriteSeason": "",
10 | "favouriteFood": [],
11 | "monthDayYear": "00/00/0000",
12 | "monthYear": "00/00/0000",
13 | "dayMonthYear": "00/00/0000",
14 | "dayMonth": "00/00/0000",
15 | "day": "00/00/0000",
16 | "month": "00/00/0000",
17 | "year": "00/00/0000",
18 | "survey": null,
19 | "signature": "",
20 | "dataGrid": [
21 | {
22 | "textField": "",
23 | "checkbox": false
24 | },
25 | {
26 | "textField": "Some #1 text here",
27 | "checkbox": false
28 | },
29 | {
30 | "textField": "",
31 | "checkbox": false
32 | },
33 | {
34 | "textField": "Some #2 text here",
35 | "checkbox": false
36 | }
37 | ],
38 | "uploadBase64": [],
39 | "uploadUrl": [],
40 | "submit": true
41 | }
42 |
--------------------------------------------------------------------------------
/formiodata/components/selectboxes.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from .component import Component
5 |
6 |
7 | class selectboxesComponent(Component):
8 |
9 | @property
10 | def dataSrc(self):
11 | return self.raw.get('dataSrc')
12 |
13 | @property
14 | def values_labels(self):
15 | comp = self.component_owner.input_components.get(self.key)
16 | builder_values = comp.raw.get('values')
17 | values_labels = {}
18 | for b_val in builder_values:
19 | if self.value and b_val.get('value'):
20 | if self.i18n.get(self.language):
21 | label = self.i18n[self.language].get(b_val['label'], b_val['label'])
22 | else:
23 | label = b_val['label']
24 | val = {'key': b_val['value'], 'label': label, 'value': self.value.get(b_val['value'])}
25 | values_labels[b_val['value']] = val
26 | return values_labels
27 |
--------------------------------------------------------------------------------
/formiodata/components/tabs.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from .layout_base import layoutComponentBase
5 |
6 |
7 | class tabsComponent(layoutComponentBase):
8 |
9 | def load_data(self, data, is_form=False):
10 | self.tabs = []
11 |
12 | for data_tab in self.raw.get('components', []):
13 | tab = {'tab': data_tab, 'components': []}
14 |
15 | for component in data_tab['components']:
16 | # Only determine and load class if component type.
17 | if 'type' in component:
18 | component_obj = self.builder.get_component_object(component)
19 | component_obj.load(
20 | self.child_component_owner,
21 | parent=self,
22 | data=data,
23 | all_data=self._all_data,
24 | is_form=is_form,
25 | )
26 | tab['components'].append(component_obj)
27 |
28 | self.tabs.append(tab)
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Nova Code
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/formiodata/components/panel.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from .layout_base import layoutComponentBase
5 |
6 |
7 | class panelComponent(layoutComponentBase):
8 |
9 | def load_data(self, data, is_form=False):
10 | for component in self.raw.get('components', []):
11 | # Only determine and load class if component type.
12 | if 'type' in component:
13 | component_obj = self.builder.get_component_object(component)
14 | component_obj.load(
15 | self.child_component_owner,
16 | parent=self,
17 | data=data,
18 | all_data=self._all_data,
19 | is_form=is_form,
20 | )
21 |
22 | @property
23 | def title(self):
24 | title = self.raw.get('title')
25 | if not title:
26 | title = self.raw.get('label')
27 |
28 | if self.i18n.get(self.language):
29 | return self.i18n[self.language].get(title, title)
30 | else:
31 | return title
32 |
--------------------------------------------------------------------------------
/.github/workflows/unittests.yml:
--------------------------------------------------------------------------------
1 | name: Test Python-formio-data
2 |
3 | on:
4 | push:
5 | branches:
6 | - '**'
7 |
8 | jobs:
9 | test-without-json-logic:
10 | name: Basic unit tests
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - name: Check out the repo
16 | uses: actions/checkout@v3
17 |
18 | - name: Set up Nix with direnv support
19 | uses: aldoborrero/use-nix-action@v2
20 | with:
21 | nix_path: nixpkgs=channel:nixos-23.05
22 |
23 | - name: Install basic dependencies via poetry
24 | run: poetry install
25 |
26 | - name: Run the tests
27 | run: poetry run python -m unittest
28 |
29 |
30 | test-with-json-logic:
31 | name: Unit tests with json_logic
32 |
33 | runs-on: ubuntu-latest
34 |
35 | steps:
36 | - name: Check out the repo
37 | uses: actions/checkout@v3
38 |
39 | - name: Set up Nix with direnv support
40 | uses: aldoborrero/use-nix-action@v2
41 | with:
42 | nix_path: nixpkgs=channel:nixos-23.05
43 |
44 | - name: Install dependencies via poetry, including json_logic
45 | run: poetry install -E json_logic
46 |
47 | - name: Run the tests
48 | run: poetry run python -m unittest
49 |
--------------------------------------------------------------------------------
/tests/data/test_data_src_url_builder.json:
--------------------------------------------------------------------------------
1 | {
2 | "components": [
3 | {
4 | "label": "Country",
5 | "widget": "choicesjs",
6 | "tableView": true,
7 | "dataSrc": "url",
8 | "defaultValue": [
9 | {}
10 | ],
11 | "data": {
12 | "headers": [
13 | {
14 | "key": "",
15 | "value": ""
16 | }
17 | ]
18 | },
19 | "refreshOn": "",
20 | "clearOnRefresh": true,
21 | "customDefaultValue": "value = {};",
22 | "validate": {
23 | "required": true
24 | },
25 | "validateWhenHidden": false,
26 | "key": "country",
27 | "properties": null,
28 | "type": "select",
29 | "lazyLoad": false,
30 | "disableLimit": false,
31 | "filter": "api=getData&model=res.country&label=display_name&domain_api=countryGroup&country_group={{data.countryGroup}}",
32 | "noRefreshOnScroll": false,
33 | "ignoreCache": true,
34 | "input": true
35 | }
36 | ]
37 | }
38 |
--------------------------------------------------------------------------------
/formiodata/components/file.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from .component import Component
5 |
6 | from formiodata.utils import base64_encode_url
7 |
8 |
9 | class fileComponent(Component):
10 |
11 | def __init__(self, raw, builder, **kwargs):
12 | super().__init__(raw, builder, **kwargs)
13 |
14 | @property
15 | def storage(self):
16 | return self.raw.get('storage')
17 |
18 | @property
19 | def url(self):
20 | return self.raw.get('url')
21 |
22 | @property
23 | def base64(self):
24 | if self.storage == 'url':
25 | res = ''
26 | for val in self.form.get('value'):
27 | url = val.get('url')
28 | res += base64_encode_url(url)
29 | return res
30 | elif self.storage == 'base64':
31 | return super().value
32 |
33 | # @value.setter
34 | # def value(self, value):
35 | # """ Inherit property setter the right way, URLs:
36 | # - https://gist.github.com/Susensio/979259559e2bebcd0273f1a95d7c1e79
37 | # - https://stackoverflow.com/questions/35290540/understanding-property-decorator-and-inheritance
38 | # """
39 | # super(self.__class__, self.__class__).value.fset(self, value)
40 |
--------------------------------------------------------------------------------
/formiodata/components/table.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from .layout_base import layoutComponentBase
5 |
6 |
7 | class tableComponent(layoutComponentBase):
8 | def __init__(self, raw, builder, **kwargs):
9 | self.rows = []
10 | super().__init__(raw, builder, **kwargs)
11 |
12 | def load_data(self, data, is_form=False):
13 | self.rows = []
14 |
15 | for data_row in self.raw.get('rows', []):
16 | row = []
17 |
18 | for col in data_row:
19 | components = []
20 | for component in col['components']:
21 | # Only determine and load class if component type.
22 | if 'type' in component:
23 | component_obj = self.builder.get_component_object(component)
24 | component_obj.load(
25 | self.child_component_owner,
26 | parent=self,
27 | data=data,
28 | all_data=self._all_data,
29 | is_form=is_form,
30 | )
31 | components.append(component_obj)
32 |
33 | row.append({'column': col, 'components': components})
34 |
35 | self.rows.append(row)
36 |
--------------------------------------------------------------------------------
/tests/data/test_resources_submission.json:
--------------------------------------------------------------------------------
1 | {
2 | "60034d19942c745a300b32aa": [
3 | {
4 | "_id": {
5 | "$oid": "601d5419d88e677933e55029"
6 | },
7 | "data": {
8 | "name": "ResA",
9 | "description": "DescA"
10 | }
11 | },
12 | {
13 | "_id": {
14 | "$oid": "60034ec3942c74ca500b32b1"
15 | },
16 | "data": {
17 | "name": "ResB",
18 | "description": "DescB"
19 | }
20 | },
21 | {
22 | "_id": {
23 | "$oid": "60034eb1942c7410180b32af"
24 | },
25 | "data": {
26 | "name": "ResC",
27 | "description": "DescC"
28 | }
29 | },
30 | {
31 | "_id": {
32 | "$oid": "60034e72942c74a3500b32ad"
33 | },
34 | "data": {
35 | "name": "ResD",
36 | "description": "DescD"
37 | }
38 | }
39 | ],
40 | "60034d19942c745a300b32ac": [
41 | {
42 | "data": {
43 | "name": "ResA-1",
44 | "description": "DescA"
45 | }
46 | },
47 | {
48 | "data": {
49 | "name": "ResB-1",
50 | "description": "DescB"
51 | }
52 | },
53 | {
54 | "data": {
55 | "name": "ResC-1",
56 | "description": "DescC"
57 | }
58 | },
59 | {
60 | "data": {
61 | "name": "ResD-1",
62 | "description": "DescD"
63 | }
64 | }
65 | ]
66 | }
--------------------------------------------------------------------------------
/tests/test_component_panel.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from test_component import ComponentTestCase
5 | from formiodata.components.panel import panelComponent
6 |
7 |
8 | class panelComponentTestCase(ComponentTestCase):
9 |
10 | def test_object(self):
11 | # TextfieldComponent
12 | panel = self.builder.components['panel']
13 | self.assertIsInstance(panel, panelComponent)
14 |
15 | # Not TextfieldComponent
16 | email = self.builder.input_components['email']
17 | self.assertNotIsInstance(email, panelComponent)
18 |
19 | def test_get_key(self):
20 | panel = self.builder.components['panel']
21 | self.assertEqual(panel.key, 'panel')
22 |
23 | def test_get_type(self):
24 | panel = self.builder.components['panel']
25 | self.assertEqual(panel.type, 'panel')
26 |
27 | def test_get_label(self):
28 | panel = self.builder.components['panel']
29 | self.assertEqual(panel.label, 'Panel')
30 |
31 | def test_get_title(self):
32 | panel = self.builder.components['panel']
33 | self.assertEqual(panel.title, 'My Favourites')
34 |
35 | # i18n translations
36 | def test_get_title_i18n_nl(self):
37 | panel = self.builder_i18n_nl.components['panel']
38 | self.assertEqual(panel.title, 'Mijn favorieten')
39 |
--------------------------------------------------------------------------------
/tests/test_common.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | import json
5 | import unittest
6 | import sys
7 |
8 | from tests.utils import readfile
9 | from formiodata.builder import Builder
10 | from formiodata.form import Form
11 |
12 |
13 | class CommonTestCase(unittest.TestCase):
14 |
15 | def setUp(self):
16 | super(CommonTestCase, self).setUp()
17 |
18 | # test_example_builder.json
19 | # - shown: https://formio.github.io/formio.js/
20 | # - source: https://examples.form.io/example
21 | self.builder_json = readfile('data', 'test_example_builder.json')
22 | self.builder_resource = readfile('data', 'test_example_builder_with_resource.json')
23 | self.builder_with_resource = readfile('data', 'test_example_builder_with_resource.json')
24 | self.form_json = readfile('data', 'test_example_form.json')
25 | self.form_empty_json = readfile('data', 'test_example_form_empty.json')
26 | self.form_json_check_default = readfile('data', 'test_example_form_check_default.json')
27 | self.form_with_resource = readfile("data", "test_example_form_with_resource.json")
28 | self.builder_json_resource = readfile('data', 'test_resources_submission.json')
29 |
30 | # self.builder = Builder(self.builder_json)
31 | # self.form = Form(self.form_json, None, self.builder_json)
32 |
--------------------------------------------------------------------------------
/tests/utils.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | import logging
5 | import os
6 |
7 | def readfile(dir_path, filename):
8 | cwd = os.path.dirname(os.path.realpath(__file__))
9 | path = '%s/%s/%s' % (cwd, dir_path, filename)
10 | with open(path, "r") as fp:
11 | return fp.read()
12 |
13 | def log_unittest(unittest_obj, msg, log_level='info'):
14 | log = '%s %s' % (log_level.upper(), unittest_obj.id())
15 | if unittest_obj.shortDescription():
16 | log += ' -- %s' % unittest_obj.shortDescription()
17 | log += ' - %s' % msg
18 | getattr(logging, log_level)(log)
19 |
20 |
21 | class ConditionalVisibilityTestHelpers:
22 | def setUp(self):
23 | super(ConditionalVisibilityTestHelpers, self).setUp()
24 | try:
25 | from json_logic import jsonLogic
26 | self._have_json_logic = True
27 | except ImportError:
28 | self._have_json_logic = False
29 |
30 | def assertVisible(self, component):
31 | self.assertTrue(component.conditionally_visible)
32 |
33 | def assertNotVisible(self, component):
34 | if component.raw['conditional'].get('json') and not self._have_json_logic:
35 | # Without json_logic, all components with json conditionals
36 | # are considered always visible
37 | self.assertTrue(component.conditionally_visible)
38 | else:
39 | self.assertFalse(component.conditionally_visible)
40 |
--------------------------------------------------------------------------------
/tests/test_component_resource.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from formiodata.builder import Builder
5 | from formiodata.components.resource import resourceComponent
6 | from formiodata.form import Form
7 | from tests.test_component import ComponentTestCase
8 |
9 |
10 | class resourceComponentTestCase(ComponentTestCase):
11 |
12 | def setUp(self):
13 | super(resourceComponentTestCase, self).setUp()
14 | self.builder_res = Builder(self.builder_with_resource, resources=self.builder_json_resource)
15 | self.form_res = Form(self.form_with_resource, self.builder_res)
16 |
17 | def test_object(self):
18 | # TextfieldComponent
19 | res = self.builder_res.input_components['resourceObj']
20 | self.assertIsInstance(res, resourceComponent)
21 |
22 | def test_get_key(self):
23 | res = self.builder_res.input_components['resourceObj']
24 | self.assertEqual(res.key, 'resourceObj')
25 |
26 | def test_get_type(self):
27 | res = self.builder_res.input_components['resourceObj']
28 | self.assertEqual(res.type, 'resource')
29 |
30 | def test_values(self):
31 | res = self.builder_res.input_components['resourceObj']
32 | self.assertEqual(len(res.values), 4)
33 |
34 | def test_first_value(self):
35 | res = self.builder_res.input_components['resourceObj']
36 | self.assertEqual(res.values[1], {'label': 'ResB', 'value': '60034ec3942c74ca500b32b1'})
37 |
--------------------------------------------------------------------------------
/formiodata/components/radio.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from .component import Component
5 |
6 |
7 | class radioComponent(Component):
8 |
9 | def _encode_value(self, value):
10 | # A number value got casted to integer, by json.loads().
11 | # Ensure this becomes a string.
12 | return str(value)
13 |
14 | @property
15 | def values_labels(self):
16 | comp = self.component_owner.input_components.get(self.key)
17 | builder_values = comp.raw.get('values')
18 | values_labels = {}
19 |
20 | for b_val in builder_values:
21 | if self.i18n.get(self.language):
22 | label = self.i18n[self.language].get(b_val['label'], b_val['label'])
23 | else:
24 | label = b_val['label']
25 | val = {'key': b_val['value'], 'label': label, 'value': b_val['value'] == self.value}
26 | values_labels[b_val['value']] = val
27 | return values_labels
28 |
29 | @property
30 | def value_label(self):
31 | comp = self.component_owner.input_components.get(self.key)
32 | builder_values = comp.raw.get('values')
33 | for b_val in builder_values:
34 | if b_val['value'] == self.value:
35 | if self.i18n.get(self.language):
36 | return self.i18n[self.language].get(b_val['label'], b_val['label'])
37 | else:
38 | return b_val['label']
39 | else:
40 | return False
41 |
--------------------------------------------------------------------------------
/tests/test_component_email.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from test_component import ComponentTestCase
5 | from formiodata.components.email import emailComponent
6 |
7 |
8 | class emailComponentTestCase(ComponentTestCase):
9 |
10 | def test_object(self):
11 | # EmailComponent
12 | email = self.builder.input_components['email']
13 | self.assertIsInstance(email, emailComponent)
14 |
15 | # Not EmailComponent
16 | firstName = self.builder.input_components['firstName']
17 | self.assertNotIsInstance(firstName, emailComponent)
18 |
19 | def test_get_key(self):
20 | email = self.builder.input_components['email']
21 | self.assertEqual(email.key, 'email')
22 |
23 | def test_get_type(self):
24 | email = self.builder.input_components['email']
25 | self.assertEqual(email.type, 'email')
26 |
27 | def test_get_label(self):
28 | email = self.builder.input_components['email']
29 | self.assertEqual(email.label, 'Email')
30 |
31 | def test_set_label(self):
32 | email = self.builder.input_components['email']
33 | self.assertEqual(email.label, 'Email')
34 | email.label = 'Foobar'
35 | self.assertEqual(email.label, 'Foobar')
36 |
37 | def test_get_form(self):
38 | email = self.form.input_components['email']
39 | self.assertEqual(email.label, 'Email')
40 | self.assertEqual(email.value, 'bob@novacode.nl')
41 | self.assertEqual(email.type, 'email')
42 |
43 | def test_get_form_data(self):
44 | email = self.form.input.email
45 | self.assertEqual(email.label, 'Email')
46 | self.assertEqual(email.value, 'bob@novacode.nl')
47 | self.assertEqual(email.type, 'email')
48 |
--------------------------------------------------------------------------------
/tests/test_datagrid_in_panel.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | import unittest
5 |
6 | from tests.utils import readfile
7 |
8 | from formiodata.builder import Builder
9 | from formiodata.form import Form
10 |
11 |
12 | # Regression test for #17
13 | class DatagridInPanelTestcase(unittest.TestCase):
14 | def setUp(self):
15 | super(DatagridInPanelTestcase, self).setUp()
16 |
17 | self.builder_json = readfile('data', 'test_datagrid_in_panel_builder.json')
18 | self.one_row_form_json = readfile('data', 'test_datagrid_in_panel_one_row_form.json')
19 |
20 | def test_default_state_in_builder_has_one_row(self):
21 | builder = Builder(self.builder_json)
22 | self.assertEqual({'panel', 'submit'}, set(builder.components.keys()))
23 |
24 | panel = builder.components['panel']
25 | self.assertEqual({'dataGrid'}, set(panel.components.keys()))
26 |
27 | datagrid = builder.components['panel'].components['dataGrid']
28 | self.assertEqual({'textField'}, set(datagrid.components.keys()))
29 |
30 | # datagrid will have no visible rows when initialized
31 | self.assertTrue(datagrid.initEmpty)
32 | self.assertEqual(len(datagrid.rows), 0)
33 |
34 |
35 | def test_form_with_one_row_has_the_one_row_created_by_submission(self):
36 | builder = Builder(self.builder_json)
37 |
38 | form = Form(self.one_row_form_json, builder)
39 | self.assertEqual({'panel', 'submit'}, set(form.components.keys()))
40 |
41 | panel = form.components['panel']
42 | self.assertEqual({'dataGrid'}, set(panel.components.keys()))
43 |
44 | datagrid = form.components['panel'].components['dataGrid']
45 | self.assertEqual({'textField'}, set(datagrid.components.keys()))
46 | self.assertEqual(len(datagrid.rows), 1)
47 |
--------------------------------------------------------------------------------
/tests/test_component_number.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from test_component import ComponentTestCase
5 | from formiodata.components.number import numberComponent
6 |
7 |
8 | class numberComponentTestCase(ComponentTestCase):
9 |
10 | def test_object(self):
11 | howManySeats = self.builder.input_components['howManySeats']
12 | self.assertIsInstance(howManySeats, numberComponent)
13 |
14 | # Not numberComponent
15 | firstName = self.builder.input_components['firstName']
16 | self.assertNotIsInstance(firstName, numberComponent)
17 |
18 | def test_get_key(self):
19 | howManySeats = self.builder.input_components['howManySeats']
20 | self.assertEqual(howManySeats.key, 'howManySeats')
21 |
22 | def test_get_type(self):
23 | howManySeats = self.builder.input_components['howManySeats']
24 | self.assertEqual(howManySeats.type, 'number')
25 |
26 | def test_get_label(self):
27 | howManySeats = self.builder.input_components['howManySeats']
28 | self.assertEqual(howManySeats.label, 'How Many Seats?')
29 |
30 | def test_set_label(self):
31 | howManySeats = self.builder.input_components['howManySeats']
32 | self.assertEqual(howManySeats.label, 'How Many Seats?')
33 | howManySeats.label = 'Foobar?'
34 | self.assertEqual(howManySeats.label, 'Foobar?')
35 |
36 | def test_get_form(self):
37 | howManySeats = self.form.input_components['howManySeats']
38 | self.assertEqual(howManySeats.label, 'How Many Seats?')
39 | self.assertEqual(howManySeats.value, 4)
40 | self.assertEqual(howManySeats.type, 'number')
41 |
42 | def test_get_form_data(self):
43 | howManySeats = self.form.input.howManySeats
44 | self.assertEqual(howManySeats.label, 'How Many Seats?')
45 | self.assertEqual(howManySeats.value, 4)
46 | self.assertEqual(howManySeats.type, 'number')
47 |
--------------------------------------------------------------------------------
/tests/test_component_phoneNumber.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from test_component import ComponentTestCase
5 | from formiodata.components.phoneNumber import phoneNumberComponent
6 |
7 |
8 | class phoneNumberComponentTestCase(ComponentTestCase):
9 |
10 | def test_object(self):
11 | # phoneNumberComponent
12 | phoneNumber = self.builder.input_components['phoneNumber']
13 | self.assertIsInstance(phoneNumber, phoneNumberComponent)
14 |
15 | # Not phoneNumberComponent
16 | firstName = self.builder.input_components['firstName']
17 | self.assertNotIsInstance(firstName, phoneNumberComponent)
18 |
19 | def test_get_key(self):
20 | phoneNumber = self.builder.input_components['phoneNumber']
21 | self.assertEqual(phoneNumber.key, 'phoneNumber')
22 |
23 | def test_get_type(self):
24 | phoneNumber = self.builder.input_components['phoneNumber']
25 | self.assertEqual(phoneNumber.type, 'phoneNumber')
26 |
27 | def test_get_label(self):
28 | phoneNumber = self.builder.input_components['phoneNumber']
29 | self.assertEqual(phoneNumber.label, 'Phone Number')
30 |
31 | def test_set_label(self):
32 | phoneNumber = self.builder.input_components['phoneNumber']
33 | self.assertEqual(phoneNumber.label, 'Phone Number')
34 | phoneNumber.label = 'Foobar'
35 | self.assertEqual(phoneNumber.label, 'Foobar')
36 |
37 | def test_get_form(self):
38 | phoneNumber = self.form.input_components['phoneNumber']
39 | self.assertEqual(phoneNumber.label, 'Phone Number')
40 | self.assertEqual(phoneNumber.value, '(069) 999-9999')
41 | self.assertEqual(phoneNumber.type, 'phoneNumber')
42 |
43 | def test_get_form_data(self):
44 | phoneNumber = self.form.input.phoneNumber
45 | self.assertEqual(phoneNumber.label, 'Phone Number')
46 | self.assertEqual(phoneNumber.value, '(069) 999-9999')
47 | self.assertEqual(phoneNumber.type, 'phoneNumber')
48 |
--------------------------------------------------------------------------------
/formiodata/components/address.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from .component import Component
5 |
6 |
7 | class addressComponent(Component):
8 |
9 | _none_value = {}
10 |
11 | # XXX other providers not analysed and implemented yet.
12 | PROVIDER_GOOGLE = 'google'
13 |
14 | def _address_google(self, get_type, notation='long_name'):
15 | comps = self.value.get('address_components')
16 | if not comps:
17 | return None
18 | else:
19 | for comp in comps:
20 | if comp.get('types') and get_type in comp['types']:
21 | return comp.get(notation)
22 | return None
23 |
24 | @property
25 | def provider(self):
26 | return self.raw.get('provider')
27 |
28 | @property
29 | def postal_code(self):
30 | if self.provider == self.PROVIDER_GOOGLE:
31 | return self._address_google('postal_code')
32 | else:
33 | return None
34 |
35 | @property
36 | def street_name(self):
37 | if self.provider == self.PROVIDER_GOOGLE:
38 | return self._address_google('route')
39 | else:
40 | return None
41 |
42 | @property
43 | def street_number(self):
44 | if self.provider == self.PROVIDER_GOOGLE:
45 | return self._address_google('street_number')
46 | else:
47 | return None
48 |
49 | @property
50 | def city(self):
51 | if self.provider == self.PROVIDER_GOOGLE:
52 | return self._address_google('locality')
53 | else:
54 | return None
55 |
56 | @property
57 | def country(self):
58 | if self.provider == self.PROVIDER_GOOGLE:
59 | return self._address_google('country')
60 | else:
61 | return None
62 |
63 | @property
64 | def country_code(self):
65 | if self.provider == self.PROVIDER_GOOGLE:
66 | return self._address_google('country', 'short_name')
67 | else:
68 | return None
69 |
--------------------------------------------------------------------------------
/formiodata/components/survey.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from .component import Component
5 |
6 |
7 | class surveyComponent(Component):
8 |
9 | @property
10 | def values_labels(self):
11 | comp = self.component_owner.input_components.get(self.key)
12 | builder_values = comp.raw.get('values')
13 | labels = []
14 | for val in builder_values:
15 | if self.i18n.get(self.language):
16 | label = self.i18n[self.language].get(val['label'], val['label'])
17 | else:
18 | label = val['label']
19 | labels.append(label)
20 | return labels
21 |
22 | @property
23 | def grid(self):
24 | comp = self.component_owner.input_components.get(self.key)
25 | builder_questions = comp.raw.get('questions')
26 | builder_values = comp.raw.get('values')
27 | grid = []
28 | for question in builder_questions:
29 | # question
30 | if self.i18n.get(self.language):
31 | question_label = self.i18n[self.language].get(question['label'], question['label'])
32 | else:
33 | question_label = question['label']
34 | question_dict = {'question_value': question['value'], 'question_label': question_label, 'values': []}
35 |
36 | # value
37 | for b_val in builder_values:
38 | if self.i18n.get(self.language):
39 | val_label = self.i18n[self.language].get(b_val['label'], b_val['label'])
40 | else:
41 | val_label = b_val['label']
42 |
43 | value = {
44 | 'label': val_label,
45 | 'value': b_val['value'],
46 | 'checked': False # default as fallback (if new values in builder)
47 | }
48 |
49 | if self.value.get(question['value']):
50 | value['checked'] = self.value[question['value']] == b_val['value']
51 |
52 | question_dict['values'].append(value)
53 |
54 | # append
55 | grid.append(question_dict)
56 | return grid
57 |
--------------------------------------------------------------------------------
/formiodata/utils.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | import base64
5 | import requests
6 | import tempfile
7 | import re
8 |
9 | from datetime import datetime
10 |
11 |
12 | def base64_encode_url(url):
13 | content = requests.get(url).content
14 | tf = tempfile.TemporaryFile()
15 | tf.write(content)
16 | tf.seek(0)
17 | b64encode = base64.b64encode(tf.read())
18 | tf.close()
19 | # prefix and decode bytes to str
20 | b64encode = '%s,%s' % ('data:image/png;base64', b64encode.decode())
21 | return b64encode
22 |
23 |
24 | def decode_resource_template(tmp):
25 | res = re.sub(r"<.*?>", " ", tmp)
26 | strcleaned = re.sub(r'\{{ |\ }}', "", res)
27 | list_kyes = strcleaned.strip().split(".")
28 | return list_kyes[1:]
29 |
30 |
31 | def fetch_dict_get_value(dict_src, list_keys):
32 | if len(list_keys) == 0:
33 | return
34 | node = list_keys[0]
35 | list_keys.remove(node)
36 | nextdict = dict_src.get(node)
37 | if len(list_keys) >= 1:
38 | return fetch_dict_get_value(nextdict, list_keys)
39 | else:
40 | return dict_src.get(node)
41 |
42 | def datetime_fromisoformat(date_string):
43 | # Backport of Python 3.7 datetime.fromisoformat
44 | if hasattr(datetime, 'fromisoformat'):
45 | # Python >= 3.7
46 | return datetime.fromisoformat(date_string)
47 | else:
48 | # Python < 3.7
49 | # replaces the fromisoformat, not available in Python < 3.7
50 | #
51 | # XXX following:
52 | # - Raises: '2021-02-25T00:00:00+01:00' does not match format '%Y-%m-%dT%H:%M%z'
53 | # - Due to %z not obtaing the colon in '+1:00' (tz offset)
54 | # - More info: https://stackoverflow.com/questions/54268458/datetime-strptime-issue-with-a-timezone-offset-with-colons
55 | # fmt_str = r"%Y-%m-%dT%H:%M:%S%z"
56 | # return datetime.strptime(value, fmt_str)
57 | #
58 | # REQUIREMENT (TODO document, setup dependency or try/except raise exception)
59 | # - pip install dateutil
60 | # - https://dateutil.readthedocs.io/
61 | from dateutil.parser import parse
62 | return parse(date_string)
63 |
--------------------------------------------------------------------------------
/tests/test_component_tabs.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from test_component import ComponentTestCase
5 | from formiodata.components.tabs import tabsComponent
6 | from formiodata.components.textfield import textfieldComponent
7 | from formiodata.components.number import numberComponent
8 |
9 |
10 | class tabsComponentTestCase(ComponentTestCase):
11 |
12 | def test_object(self):
13 | tabs = self.builder.components['tabs']
14 | self.assertIsInstance(tabs, tabsComponent)
15 |
16 | # Not tabsComponent
17 | email = self.builder.input_components['email']
18 | self.assertNotIsInstance(email, tabsComponent)
19 |
20 | def test_get_key(self):
21 | tabs = self.builder.components['tabs']
22 | self.assertEqual(tabs.key, 'tabs')
23 |
24 | def test_get_type(self):
25 | tabs = self.builder.components['tabs']
26 | self.assertEqual(tabs.type, 'tabs')
27 |
28 | def test_get_label(self):
29 | tabs = self.builder.components['tabs']
30 | self.assertEqual(tabs.label, 'Tabs')
31 |
32 | def test_get_tabs(self):
33 | builder_tabs_component = self.builder.components['tabs']
34 | tabs_component = self.form.components[builder_tabs_component.key]
35 |
36 | self.assertEqual(len(tabs_component.tabs), 2)
37 |
38 | for tab in tabs_component.tabs:
39 | if tab['tab']['key'] == 'tab1':
40 | self.assertEqual(tab['tab']['label'], 'Tab 1')
41 | # components in tab
42 | self.assertEqual(len(tab['components']), 1)
43 | textfieldTab1 = tab['components'][0]
44 | self.assertIsInstance(textfieldTab1, textfieldComponent)
45 | self.assertEqual(textfieldTab1.value, 'text in tab 1')
46 | if tab['tab']['key'] == 'tab2':
47 | self.assertEqual(tab['tab']['label'], 'Tab 2')
48 | # components in tab
49 | self.assertEqual(len(tab['components']), 1)
50 | numberTab2 = tab['components'][0]
51 | self.assertIsInstance(numberTab2, numberComponent)
52 | self.assertEqual(numberTab2.value, 2)
53 |
--------------------------------------------------------------------------------
/formiodata/components/datetime.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | import logging
5 |
6 | from copy import copy
7 | from datetime import datetime
8 |
9 | from .component import Component
10 | from ..utils import datetime_fromisoformat
11 |
12 | logger = logging.getLogger(__name__)
13 |
14 |
15 | class datetimeComponent(Component):
16 |
17 | @property
18 | def enableTime(self):
19 | return self.raw.get('enableTime')
20 |
21 | def _format_mappings(self):
22 | """
23 | Dictionary of mappings between Formio Datetime component
24 | (key) to Python format (value).
25 |
26 | Formio uses the (JS uibDateParser) format codes referenced in:
27 | https://github.com/angular-ui/bootstrap/tree/master/src/dateparser/docs#uibdateparsers-format-codes
28 | """
29 | return {
30 | 'year': {'yyyy': '%Y', 'yy': '%y', 'y': '%y'},
31 | 'month': {'MMMM': '%B', 'MMM': '%b', 'MM': '%m', 'M': '%-m'},
32 | 'day': {'dd': '%d', 'd': '%-d'},
33 | 'hour': {'HH': '%H', 'H': '%-H', 'hh': '%I', 'h': '%-I'},
34 | 'minute': {'mm': '%M', 'm': '%-M'},
35 | 'second': {'ss': '%S', 's': '%-S'},
36 | 'am_pm': {'a': '%p'}
37 | }
38 |
39 | def _fromisoformat(self, value):
40 | return datetime_fromisoformat(value)
41 |
42 | @property
43 | def value(self):
44 | return super().value
45 |
46 | @value.setter
47 | def value(self, value):
48 | """ Inherit property setter the right way, URLs:
49 | - https://gist.github.com/Susensio/979259559e2bebcd0273f1a95d7c1e79
50 | - https://stackoverflow.com/questions/35290540/understanding-property-decorator-and-inheritance
51 | """
52 | if not value:
53 | return value
54 | else:
55 | return super(self.__class__, self.__class__).value.fset(self, value)
56 |
57 | def to_datetime(self):
58 | if not self.raw_value:
59 | return None
60 | dt = self._fromisoformat(self.raw_value)
61 | return dt
62 |
63 | def to_date(self):
64 | if not self.raw_value:
65 | return None
66 | return self.to_datetime().date()
67 |
--------------------------------------------------------------------------------
/tests/test_component_class_mapping.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | import json
5 |
6 | from test_common import CommonTestCase
7 | from formiodata.builder import Builder
8 | from formiodata.components.component import Component
9 | from formiodata.components.editgrid import editgridComponent
10 |
11 |
12 | class ComponentClassMappingTestCase(CommonTestCase):
13 |
14 | def setUp(self):
15 | super().setUp()
16 |
17 | schema_dict = json.loads(self.builder_json)
18 | for comp in schema_dict['components']:
19 | if comp['key'] == 'editGrid':
20 | comp['type'] = 'custom_editgrid'
21 |
22 | self.schema_json_component_class_mapping = json.dumps(schema_dict)
23 |
24 | def test_component_class_mapping_with_class(self):
25 | component_class_mapping = {'custom_editgrid': editgridComponent}
26 | builder = Builder(
27 | self.schema_json_component_class_mapping,
28 | component_class_mapping=component_class_mapping,
29 | )
30 | custom_editgrid = builder.components['editGrid']
31 | self.assertIsInstance(custom_editgrid, editgridComponent)
32 | self.assertEqual(custom_editgrid.type, 'custom_editgrid')
33 |
34 | def test_component_class_mapping_with_string(self):
35 | component_class_mapping = {'custom_editgrid': 'editgrid'}
36 | builder = Builder(
37 | self.schema_json_component_class_mapping,
38 | component_class_mapping=component_class_mapping,
39 | )
40 | custom_editgrid = builder.components['editGrid']
41 | self.assertIsInstance(custom_editgrid, editgridComponent)
42 | self.assertEqual(custom_editgrid.type, 'custom_editgrid')
43 |
44 | def test_component_no_class_mapping_import_error(self):
45 | schema_dict = json.loads(self.builder_json)
46 | for comp in schema_dict['components']:
47 | if comp['key'] == 'editGrid':
48 | comp['type'] = 'editgrid_no_class_mapping'
49 |
50 | schema_json = json.dumps(schema_dict)
51 | builder = Builder(schema_json)
52 | custom_editgrid = builder.components['editGrid']
53 | self.assertIsInstance(custom_editgrid, Component)
54 |
--------------------------------------------------------------------------------
/tests/test_component_address.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from test_component import ComponentTestCase
5 | from formiodata.components.address import addressComponent
6 |
7 |
8 | class addressComponentTestCase(ComponentTestCase):
9 |
10 | def test_object(self):
11 | address = self.builder.input_components['deliveryAddress']
12 | self.assertIsInstance(address, addressComponent)
13 |
14 | # Not addressComponent
15 | email = self.builder.input_components['email']
16 | self.assertNotIsInstance(email, addressComponent)
17 |
18 | def test_get_form_empty_address(self):
19 | address = self.form_empty.input_components['deliveryAddress']
20 | self.assertEqual(address.type, 'address')
21 | self.assertEqual(address.label, 'Delivery Address')
22 | self.assertIsInstance(address.value, dict)
23 | self.assertEqual(address.value, {})
24 |
25 | # parts
26 | # TODO lat, lon (coordinates)
27 | self.assertIsNone(address.postal_code)
28 | self.assertIsNone(address.street_name)
29 | self.assertIsNone(address.street_number)
30 | self.assertIsNone(address.city)
31 | self.assertIsNone(address.country)
32 |
33 | def test_get_form_address(self):
34 | address = self.form.input_components['deliveryAddress']
35 | self.assertEqual(address.type, 'address')
36 | self.assertEqual(address.label, 'Delivery Address')
37 | self.assertIsNotNone(address.value)
38 |
39 | # parts
40 | # TODO lat, lon (coordinates)
41 | self.assertEqual(address.postal_code, '1017 CT')
42 | self.assertEqual(address.street_name, 'Rembrandtplein')
43 | self.assertEqual(address.street_number, '33')
44 | self.assertEqual(address.city, 'Amsterdam')
45 | self.assertEqual(address.country, 'Netherlands')
46 | self.assertEqual(address.country_code, 'NL')
47 |
48 | # i18n translations
49 | def test_get_label_i18n_nl(self):
50 | address = self.builder_i18n_nl.input_components['deliveryAddress']
51 | self.assertEqual(address.label, 'Afleveradres')
52 |
53 | def test_get_form_data_i18n_nl(self):
54 | self.assertEqual(self.form_i18n_nl.input.deliveryAddress.label, 'Afleveradres')
55 |
--------------------------------------------------------------------------------
/tests/test_component_select_one_data_src_url.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from test_component import ComponentTestCase
5 | from tests.utils import readfile
6 | from formiodata.builder import Builder
7 | from formiodata.form import Form
8 | from formiodata.components.select import selectComponent
9 |
10 |
11 | class selectOneDataSrcUrlComponentTestCase(ComponentTestCase):
12 |
13 | def setUp(self):
14 | super(selectOneDataSrcUrlComponentTestCase, self).setUp()
15 |
16 | self.builder_json = readfile('data', 'test_data_src_url_builder.json')
17 | self.builder = Builder(self.builder_json)
18 | self.form_no_value_json = readfile('data', 'test_data_src_url_form_no_value.json')
19 | self.form_no_value = Form(self.form_no_value_json, self.builder)
20 |
21 | def test_object(self):
22 | # selectComponent
23 | country = self.builder.input_components['country']
24 | self.assertIsInstance(country, selectComponent)
25 |
26 | def test_key(self):
27 | country = self.builder.input_components['country']
28 | self.assertEqual(country.key, 'country')
29 |
30 | def test_type(self):
31 | country = self.builder.input_components['country']
32 | self.assertEqual(country.type, 'select')
33 |
34 | def test_data_rc(self):
35 | country = self.builder.input_components['country']
36 | self.assertEqual(country.dataSrc, 'url')
37 |
38 | def test_label(self):
39 | country = self.builder.input_components['country']
40 | self.assertEqual(country.label, 'Country')
41 |
42 | def test_form(self):
43 | country = self.form_no_value.input_components['country']
44 | self.assertEqual(country.type, 'select')
45 | self.assertEqual(country.label, 'Country')
46 | self.assertEqual(country.value, {})
47 | self.assertEqual(country.raw_value, '')
48 | self.assertEqual(country.value_label, None)
49 |
50 | def test_form_data(self):
51 | country = self.form_no_value.input.country
52 | self.assertEqual(country.type, 'select')
53 | self.assertEqual(country.label, 'Country')
54 | self.assertEqual(country.value, {})
55 | self.assertEqual(country.raw_value, '')
56 | self.assertEqual(country.value_label, None)
57 |
--------------------------------------------------------------------------------
/tests/test_component_table.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from test_component import ComponentTestCase
5 | from formiodata.components.table import tableComponent
6 |
7 |
8 | class tableComponentTestCase(ComponentTestCase):
9 |
10 | def test_object(self):
11 | # tableComponent
12 | table = self.builder.components['table']
13 | self.assertIsInstance(table, tableComponent)
14 |
15 | # Not tableComponent
16 | email = self.builder.input_components['email']
17 | self.assertNotIsInstance(email, tableComponent)
18 |
19 | def test_get_key(self):
20 | table = self.builder.components['table']
21 | self.assertEqual(table.key, 'table')
22 |
23 | def test_get_type(self):
24 | table = self.builder.components['table']
25 | self.assertEqual(table.type, 'table')
26 |
27 | def test_get_label(self):
28 | table = self.builder.components['table']
29 | self.assertEqual(table.label, 'Table')
30 |
31 | def test_get_row_labels(self):
32 | builder_table = self.builder.components['table']
33 | table = self.form.components[builder_table.key]
34 |
35 | self.assertEqual(len(table.rows), 2)
36 |
37 | labels = ['Text Field', 'Checkbox']
38 | for row in table.rows:
39 | for col in row:
40 | for comp in col['components']:
41 | self.assertIn(comp.label , labels)
42 |
43 | def test_get_rows_values(self):
44 | builder_table = self.builder.components['table']
45 | table = self.form.components[builder_table.key]
46 |
47 | self.assertEqual(len(table.rows), 2)
48 |
49 | # Accessing directly...
50 | self.assertEqual(table.components['textField'].value, 'Elephant')
51 | self.assertEqual(table.components['checkbox'].value, True)
52 | self.assertEqual(table.components['textField1'].value, 'Lion')
53 | self.assertEqual(table.components['checkbox1'].value, False)
54 |
55 | # Or through rows/cols:
56 | self.assertEqual(table.rows[0][0]['components'][0], table.components['textField'])
57 | self.assertEqual(table.rows[0][1]['components'][0], table.components['checkbox'])
58 | self.assertEqual(table.rows[1][0]['components'][0], table.components['textField1'])
59 | self.assertEqual(table.rows[1][1]['components'][0], table.components['checkbox1'])
60 |
--------------------------------------------------------------------------------
/tests/test_performance_nested_components.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | import logging
5 | import time
6 | import unittest
7 |
8 | from tests.utils import readfile
9 | from formiodata.builder import Builder
10 |
11 |
12 | class PerformanceNestedTestCase(unittest.TestCase):
13 | logger = logging.getLogger(__name__)
14 | logging.basicConfig(format='\n%(message)s', level=logging.INFO)
15 |
16 | def setUp(self):
17 | super(PerformanceNestedTestCase, self).setUp()
18 | self.builder_json = readfile('data', 'test_nested_components_builder.json')
19 | self.form_json = readfile('data', 'test_nested_components_form.json')
20 |
21 | def load_builders_range(self, range_num, load_path_objects):
22 | start = time.time()
23 | builders = {}
24 | for n in range(range_num):
25 | builders[n] = Builder(self.builder_json, load_path_objects=load_path_objects)
26 | end = time.time()
27 | msg_lines = [
28 | '----------------------------------------',
29 | 'Load Builders range: %s' % range_num,
30 | 'Duration: %s' % str(end - start),
31 | '----------------------------------------'
32 | ]
33 | self.logger.info('\n'.join(msg_lines))
34 | # self.logger.info(end - start)
35 |
36 | def test_Builder_component_with_path_objects(self):
37 | """ Builder: component path objects """
38 |
39 | msg_lines = [
40 | '========================================',
41 | 'Load Builder WITH path objects',
42 | '========================================',
43 | ]
44 | self.logger.info('\n'.join(msg_lines))
45 | self.load_builders_range(10, load_path_objects=True)
46 | self.load_builders_range(100, load_path_objects=True)
47 | self.load_builders_range(1000, load_path_objects=True)
48 |
49 | def test_Builder_component_no_path_objects(self):
50 | """ Builder: component NO path objects """
51 |
52 | msg_lines = [
53 | '========================================',
54 | 'Load Builder NO path objects',
55 | '========================================',
56 | ]
57 | self.logger.info('\n'.join(msg_lines))
58 | self.load_builders_range(10, load_path_objects=False)
59 | self.load_builders_range(100, load_path_objects=False)
60 | self.load_builders_range(1000, load_path_objects=False)
61 |
--------------------------------------------------------------------------------
/formiodata/components/resource.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from formiodata.utils import decode_resource_template, fetch_dict_get_value
5 |
6 | from .component import Component
7 |
8 |
9 | class resourceComponent(Component):
10 |
11 | def __init__(self, raw, builder, **kwargs):
12 | super().__init__(raw, builder, **kwargs)
13 | self.item_data = {}
14 | self.template_label_keys = decode_resource_template(self.raw.get('template'))
15 | self.compute_resources()
16 |
17 | def compute_resources(self):
18 | if self.resources:
19 | resource_id = self.raw.get('resource')
20 | if resource_id and not resource_id == "" and resource_id in self.resources:
21 | resource_list = self.resources[resource_id]
22 | self.raw['data'] = {"values": []}
23 | for item in resource_list:
24 | label = fetch_dict_get_value(item, self.template_label_keys[:])
25 | self.raw['data']['values'].append({
26 | "label": label,
27 | "value": item['_id']['$oid']
28 | })
29 |
30 | @property
31 | def value_label(self):
32 | comp = self.component_owner.input_components.get(self.key)
33 | values = comp.raw.get('data') and comp.raw['data'].get('values')
34 | for val in values:
35 | if val['value'] == self.value:
36 | label = val['label']
37 | if self.i18n.get(self.language):
38 | return self.i18n[self.language].get(label, label)
39 | else:
40 | return label
41 | else:
42 | return False
43 |
44 | @property
45 | def value_labels(self):
46 | comp = self.component_owner.input_components.get(self.key)
47 | values = comp.raw.get('data') and comp.raw['data'].get('values')
48 | value_labels = []
49 | for val in values:
50 | if val['value'] in self.value:
51 | if self.i18n.get(self.language):
52 | value_labels.append(self.i18n[self.language].get(val['label'], val['label']))
53 | else:
54 | value_labels.append(val['label'])
55 | return value_labels
56 |
57 | @property
58 | def data(self):
59 | return self.raw.get('data')
60 |
61 | @property
62 | def values(self):
63 | return self.raw.get('data').get('values')
64 |
--------------------------------------------------------------------------------
/tests/test_component_radio.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from test_component import ComponentTestCase
5 | from formiodata.components.radio import radioComponent
6 |
7 |
8 | class radioComponentTestCase(ComponentTestCase):
9 |
10 | def test_object(self):
11 | cd = self.builder.input_components['cardinalDirection']
12 | self.assertIsInstance(cd, radioComponent)
13 |
14 | # Not radioComponent
15 | email = self.builder.input_components['email']
16 | self.assertNotIsInstance(email, radioComponent)
17 |
18 | def test_get_key(self):
19 | cd = self.builder.input_components['cardinalDirection']
20 | self.assertEqual(cd.key, 'cardinalDirection')
21 |
22 | def test_get_type(self):
23 | cd = self.builder.input_components['cardinalDirection']
24 | self.assertEqual(cd.type, 'radio')
25 |
26 | def test_get_label(self):
27 | cd = self.builder.input_components['cardinalDirection']
28 | self.assertEqual(cd.label, 'Cardinal Direction')
29 |
30 | def test_set_label(self):
31 | cd = self.builder.input_components['cardinalDirection']
32 | self.assertEqual(cd.label, 'Cardinal Direction')
33 | cd.label = 'Compass Direction'
34 | self.assertEqual(cd.label, 'Compass Direction')
35 |
36 | def test_get_form(self):
37 | cd = self.form.components['cardinalDirection']
38 | self.assertEqual(cd.label, 'Cardinal Direction')
39 |
40 | #self.assertEqual(cd.values_labels, 'south')
41 | self.assertEqual(cd.value, 'south')
42 | self.assertEqual(cd.value_label, 'South')
43 | self.assertEqual(cd.type, 'radio')
44 |
45 | def test_get_form_data(self):
46 | cd = self.form.input.cardinalDirection
47 | self.assertEqual(cd.label, 'Cardinal Direction')
48 | self.assertEqual(cd.value, 'south')
49 | self.assertEqual(cd.value_label, 'South')
50 | self.assertEqual(cd.type, 'radio')
51 |
52 | # i18n translations
53 | def test_get_label_i18n_nl(self):
54 | cd = self.builder_i18n_nl.input_components['cardinalDirection']
55 | self.assertEqual(cd.label, 'Kardinale richting')
56 |
57 | def test_get_form_data_i18n_nl(self):
58 | self.assertEqual(self.form_i18n_nl.input.cardinalDirection.label, 'Kardinale richting')
59 | self.assertEqual(self.form_i18n_nl.input.cardinalDirection.value, 'south')
60 | self.assertEqual(self.form_i18n_nl.input.cardinalDirection.value_label, 'Zuid')
61 |
--------------------------------------------------------------------------------
/tests/test_component_textfield.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from test_component import ComponentTestCase
5 | from formiodata.components.textfield import textfieldComponent
6 |
7 |
8 | class textfieldComponentTestCase(ComponentTestCase):
9 |
10 | def test_object(self):
11 | # TextfieldComponent
12 | firstName = self.builder.input_components['firstName']
13 | self.assertIsInstance(firstName, textfieldComponent)
14 |
15 | lastName = self.builder.input_components['lastName']
16 | self.assertIsInstance(lastName, textfieldComponent)
17 |
18 | # Not TextfieldComponent
19 | email = self.builder.input_components['email']
20 | self.assertNotIsInstance(email, textfieldComponent)
21 |
22 | def test_get_key(self):
23 | firstName = self.builder.input_components['firstName']
24 | self.assertEqual(firstName.key, 'firstName')
25 |
26 | def test_get_type(self):
27 | firstName = self.builder.input_components['firstName']
28 | self.assertEqual(firstName.type, 'textfield')
29 |
30 | def test_get_label(self):
31 | firstName = self.builder.input_components['firstName']
32 | self.assertEqual(firstName.label, 'First Name')
33 |
34 | def test_set_label(self):
35 | firstName = self.builder.input_components['firstName']
36 | self.assertEqual(firstName.label, 'First Name')
37 | firstName.label = 'Foobar'
38 | self.assertEqual(firstName.label, 'Foobar')
39 |
40 | def test_get_form(self):
41 | firstName = self.form.input_components['firstName']
42 | self.assertEqual(firstName.label, 'First Name')
43 | self.assertEqual(firstName.value, 'Bob')
44 | self.assertEqual(firstName.type, 'textfield')
45 |
46 | def test_get_form_data(self):
47 | firstName = self.form.input.firstName
48 | self.assertEqual(firstName.label, 'First Name')
49 | self.assertEqual(firstName.value, 'Bob')
50 | self.assertEqual(firstName.type, 'textfield')
51 |
52 | # i18n translations
53 | def test_get_label_i18n_nl(self):
54 | firstName = self.builder_i18n_nl.input_components['firstName']
55 | self.assertEqual(firstName.label, 'Voornaam')
56 | lastName = self.builder_i18n_nl.input_components['lastName']
57 | self.assertEqual(lastName.label, 'Achternaam')
58 |
59 | def test_get_form_data_i18n_nl(self):
60 | self.assertEqual(self.form_i18n_nl.input.firstName.label, 'Voornaam')
61 | self.assertEqual(self.form_i18n_nl.input.lastName.label, 'Achternaam')
62 |
--------------------------------------------------------------------------------
/tests/test_component_select_multiple.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from test_component import ComponentTestCase
5 | from formiodata.components.select import selectComponent
6 |
7 |
8 | class selectMultipleComponentTestCase(ComponentTestCase):
9 |
10 | def test_object(self):
11 | # selectComponent
12 | food = self.builder.input_components['favouriteFood']
13 | self.assertIsInstance(food, selectComponent)
14 |
15 | # Not selectComponent
16 | email = self.builder.input_components['email']
17 | self.assertNotIsInstance(email, selectComponent)
18 |
19 | def test_get_key(self):
20 | food = self.builder.input_components['favouriteFood']
21 | self.assertEqual(food.key, 'favouriteFood')
22 |
23 | def test_get_type(self):
24 | food = self.builder.input_components['favouriteFood']
25 | self.assertEqual(food.type, 'select')
26 |
27 | def test_get_label(self):
28 | food = self.builder.input_components['favouriteFood']
29 | self.assertEqual(food.label, 'Favourite Food')
30 |
31 | def test_set_label(self):
32 | food = self.builder.input_components['favouriteFood']
33 | self.assertEqual(food.label, 'Favourite Food')
34 | food.label = 'Gimme which Food'
35 | self.assertEqual(food.label, 'Gimme which Food')
36 |
37 | def test_get_form(self):
38 | food = self.form.input_components['favouriteFood']
39 | self.assertEqual(food.label, 'Favourite Food')
40 | self.assertEqual(food.value, ['mexican', 'chinese'])
41 | self.assertEqual(food.value_labels, ['Mexican', 'Chinese'])
42 | self.assertEqual(food.value_label, None)
43 | self.assertEqual(food.type, 'select')
44 |
45 | def test_get_form_data(self):
46 | food = self.form.input.favouriteFood
47 | self.assertEqual(food.label, 'Favourite Food')
48 | self.assertEqual(food.value, ['mexican', 'chinese'])
49 | self.assertEqual(food.value_labels, ['Mexican', 'Chinese'])
50 | self.assertEqual(food.value_label, None)
51 | self.assertEqual(food.type, 'select')
52 |
53 | # i18n translations
54 | def test_get_label_i18n_nl(self):
55 | food = self.builder_i18n_nl.input_components['favouriteFood']
56 | self.assertEqual(food.label, 'Lievelingseten')
57 |
58 | def test_get_form_data_i18n_nl(self):
59 | self.assertEqual(self.form_i18n_nl.input.favouriteFood.label, 'Lievelingseten')
60 | self.assertEqual(self.form_i18n_nl.input.favouriteFood.value, ['mexican', 'chinese'])
61 | self.assertEqual(self.form_i18n_nl.input.favouriteFood.value_labels, ['Mexican', 'Chinese'])
62 |
--------------------------------------------------------------------------------
/tests/test_component_survey.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from test_component import ComponentTestCase
5 | from formiodata.components.survey import surveyComponent
6 |
7 |
8 | class surveyComponentTestCase(ComponentTestCase):
9 |
10 | def test_object(self):
11 | # surveyComponent
12 | survey = self.builder.input_components['survey']
13 | self.assertIsInstance(survey, surveyComponent)
14 |
15 | # Not surveyComponent
16 | firstName = self.builder.input_components['firstName']
17 | self.assertNotIsInstance(firstName, surveyComponent)
18 |
19 | def test_get_key(self):
20 | survey = self.builder.input_components['survey']
21 | self.assertEqual(survey.key, 'survey')
22 |
23 | def test_get_type(self):
24 | survey = self.builder.input_components['survey']
25 | self.assertEqual(survey.type, 'survey')
26 |
27 | def test_get_label(self):
28 | survey = self.builder.input_components['survey']
29 | self.assertEqual(survey.label, 'Survey')
30 |
31 | def test_set_label(self):
32 | survey = self.builder.input_components['survey']
33 | self.assertEqual(survey.label, 'Survey')
34 | survey.label = 'Foobar'
35 | self.assertEqual(survey.label, 'Foobar')
36 |
37 | def test_get_form(self):
38 | survey = self.form.components['survey']
39 | self.assertEqual(survey.label, 'Survey')
40 | self.assertEqual(survey.value['overallExperience'], 'excellent')
41 | self.assertEqual(survey.value['howWasCustomerSupport'], 'great')
42 | self.assertEqual(survey.value['howWouldYouRateTheFormIoPlatform'], 'excellent')
43 | self.assertEqual(survey.type, 'survey')
44 |
45 | def test_get_form_data(self):
46 | survey = self.form.input.survey
47 | self.assertEqual(survey.label, 'Survey')
48 | self.assertEqual(survey.value['overallExperience'], 'excellent')
49 | self.assertEqual(survey.value['howWasCustomerSupport'], 'great')
50 | self.assertEqual(survey.value['howWouldYouRateTheFormIoPlatform'], 'excellent')
51 | self.assertEqual(survey.type, 'survey')
52 |
53 | # i18n translations
54 | def test_get_label_i18n_nl(self):
55 | survey = self.builder_i18n_nl.input_components['survey']
56 | self.assertEqual(survey.label, 'Enquête')
57 |
58 | def test_get_form_data_i18n_nl(self):
59 | survey = self.form_i18n_nl.input.survey
60 | self.assertEqual(survey.label, 'Enquête')
61 | # TODO Labels for questions and values
62 | # self.assertEqual(survey.value['overallExperience'], 'uitstekend')
63 | # self.assertEqual(survey.value['howWasCustomerSupport'], 'super goed')
64 | # self.assertEqual(survey.value['howWouldYouRateTheFormIoPlatform'], 'uitstekend')
65 |
--------------------------------------------------------------------------------
/tests/test_conditional_visibility_json_logic.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | import unittest
5 |
6 | from tests.utils import readfile, ConditionalVisibilityTestHelpers
7 | from formiodata.builder import Builder
8 | from formiodata.form import Form
9 |
10 |
11 | class ConditionalVisibilityJsonLogicTestCase(ConditionalVisibilityTestHelpers, unittest.TestCase):
12 | def setUp(self):
13 | super(ConditionalVisibilityJsonLogicTestCase, self).setUp()
14 |
15 | self.builder_json = readfile('data', 'test_conditional_visibility_json_logic_builder.json')
16 | self.hide_secret_form_json = readfile('data', 'test_conditional_visibility_json_logic_hide_secret.json')
17 | self.show_secret_form_json = readfile('data', 'test_conditional_visibility_json_logic_show_secret.json')
18 |
19 | def test_conditionally_shown_components_have_default_state_in_builder(self):
20 | builder = Builder(self.builder_json)
21 |
22 | self.assertVisible(builder.input_components['username'])
23 | self.assertVisible(builder.input_components['password'])
24 | self.assertNotVisible(builder.input_components['secret'])
25 |
26 | def test_conditionally_shown_components_toggle_on_condition_being_met(self):
27 | builder = Builder(self.builder_json)
28 |
29 | hide_secret_form = Form(self.hide_secret_form_json, builder)
30 | self.assertVisible(hide_secret_form.input_components['username'])
31 | self.assertVisible(hide_secret_form.input_components['password'])
32 | self.assertNotVisible(hide_secret_form.input_components['secret'])
33 |
34 | show_secret_form = Form(self.show_secret_form_json, builder)
35 | self.assertVisible(show_secret_form.input_components['username'])
36 | self.assertVisible(show_secret_form.input_components['password'])
37 | self.assertVisible(show_secret_form.input_components['secret'])
38 |
39 | def test_conditionally_shown_components_do_not_render_when_hidden(self):
40 | builder = Builder(self.builder_json)
41 |
42 | hide_secret_form = Form(self.hide_secret_form_json, builder)
43 | hide_secret_form.render_components()
44 | self.assertEqual('wrong
', hide_secret_form.input_components['username'].html_component)
45 | self.assertEqual('incorrect
', hide_secret_form.input_components['password'].html_component)
46 | self.assertEqual('', hide_secret_form.input_components['secret'].html_component)
47 |
48 | show_secret_form = Form(self.show_secret_form_json, builder)
49 | show_secret_form.render_components()
50 | self.assertEqual('user
', show_secret_form.input_components['username'].html_component)
51 | self.assertEqual('secret
', show_secret_form.input_components['password'].html_component)
52 | self.assertEqual('Secret message
', show_secret_form.input_components['secret'].html_component)
53 |
--------------------------------------------------------------------------------
/tests/test_component_select_one.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from test_component import ComponentTestCase
5 | from formiodata.components.select import selectComponent
6 |
7 |
8 | class selectOneComponentTestCase(ComponentTestCase):
9 |
10 | def test_object(self):
11 | # selectComponent
12 | season = self.builder.input_components['favouriteSeason']
13 | self.assertIsInstance(season, selectComponent)
14 |
15 | # Not selectComponent
16 | email = self.builder.input_components['email']
17 | self.assertNotIsInstance(email, selectComponent)
18 |
19 | def test_get_key(self):
20 | season = self.builder.input_components['favouriteSeason']
21 | self.assertEqual(season.key, 'favouriteSeason')
22 |
23 | def test_get_type(self):
24 | season = self.builder.input_components['favouriteSeason']
25 | self.assertEqual(season.type, 'select')
26 |
27 | def test_get_label(self):
28 | season = self.builder.input_components['favouriteSeason']
29 | self.assertEqual(season.label, 'Favourite Season')
30 |
31 | def test_set_label(self):
32 | season = self.builder.input_components['favouriteSeason']
33 | self.assertEqual(season.label, 'Favourite Season')
34 | season.label = 'Which Season'
35 | self.assertEqual(season.label, 'Which Season')
36 |
37 | def test_get_form(self):
38 | season = self.form.input_components['favouriteSeason']
39 | self.assertEqual(season.label, 'Favourite Season')
40 | self.assertEqual(season.value, 'autumn')
41 | self.assertEqual(season.value_label, 'Autumn')
42 | self.assertEqual(season.type, 'select')
43 |
44 | def test_get_form_data(self):
45 | season = self.form.input.favouriteSeason
46 | self.assertEqual(season.label, 'Favourite Season')
47 | self.assertEqual(season.value, 'autumn')
48 | self.assertEqual(season.value_label, 'Autumn')
49 | self.assertEqual(season.type, 'select')
50 |
51 | def test_empty_value(self):
52 | season = self.form.input_components['favouriteSeason']
53 | self.assertEqual(season.label, 'Favourite Season')
54 | self.assertEqual(season.value, 'autumn')
55 | season.value = None
56 | self.assertEqual(season.value, None)
57 | self.assertEqual(season.value_label, None)
58 | self.assertEqual(season.type, 'select')
59 |
60 | # i18n translations
61 | def test_get_label_i18n_nl(self):
62 | season = self.builder_i18n_nl.input_components['favouriteSeason']
63 | self.assertEqual(season.label, 'Favoriete seizoen')
64 |
65 | def test_get_form_data_i18n_nl(self):
66 | self.assertEqual(self.form_i18n_nl.input.favouriteSeason.label, 'Favoriete seizoen')
67 | self.assertEqual(self.form_i18n_nl.input.favouriteSeason.value, 'autumn')
68 | self.assertEqual(self.form_i18n_nl.input.favouriteSeason.value_label, 'Herfst')
69 |
--------------------------------------------------------------------------------
/tests/test_component.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | import json
5 |
6 | from requests.exceptions import ConnectionError
7 |
8 | from tests.utils import log_unittest
9 | from tests.test_common import CommonTestCase
10 |
11 | from formiodata.builder import Builder
12 | from formiodata.form import Form
13 |
14 |
15 | class ComponentTestCase(CommonTestCase):
16 |
17 | def setUp(self):
18 | super(ComponentTestCase, self).setUp()
19 | self.builder = Builder(self.builder_json)
20 | self.form = Form(self.form_json, self.builder)
21 | self.form_empty = Form(self.form_empty_json, self.builder)
22 |
23 | self.builder_i18n_nl = Builder(self.builder_json, language='nl', i18n=self._i18n())
24 | self.form_i18n_nl = Form(self.form_json, self.builder_i18n_nl)
25 | self.form_empty_i18n_nl = Form(self.form_empty_json, self.builder_i18n_nl)
26 |
27 | def _i18n(self):
28 | return {
29 | 'nl': {
30 | 'First Name': 'Voornaam',
31 | 'Last Name': 'Achternaam',
32 | 'Appointment Date / Time': 'Afspraak Tijdstip',
33 | 'Delivery Address': 'Afleveradres',
34 | 'Survey': 'Enquête',
35 | 'excellent': 'uitstekend',
36 | 'great': 'supergoed',
37 | 'My Favourites': 'Mijn favorieten',
38 | 'Favourite Season': 'Favoriete seizoen',
39 | 'Autumn': 'Herfst',
40 | 'Favourite Food': 'Lievelingseten',
41 | 'Cardinal Direction': 'Kardinale richting',
42 | 'North': 'Noord',
43 | 'East': 'Oost',
44 | 'South': 'Zuid',
45 | 'West': 'West',
46 | 'Select Boxes': 'Select aanvink opties',
47 | 'Month Day Year': 'Maand dag jaar',
48 | 'Day Month Year': 'Dag maand jaar',
49 | 'May': 'Mei',
50 | 'Text Field': 'Tekstveld',
51 | 'Upload Base64': 'Upload binair naar ASCII',
52 | 'Upload Url': 'Upload naar locatie',
53 | '{{field}} is required': '{{field}} is verplicht'
54 | }
55 | }
56 |
57 | def test_schema_dict(self):
58 | builder_json = json.loads(self.builder_json)
59 | form_json = json.loads(self.form_json)
60 | self.builder = Builder(builder_json)
61 | self.form = Form(form_json, self.builder)
62 |
63 | self.builder_i18n_nl = Builder(builder_json, language='nl', i18n=self._i18n())
64 | self.form_i18n_nl = Form(form_json, self.builder_i18n_nl)
65 |
66 | def assertUrlBase64(self, component, expected_base64, log_level='warning'):
67 | try:
68 | self.assertEqual(component.base64, expected_base64)
69 | except ConnectionError as e:
70 | msg = 'Internet access is required, %s...\n%s' % (e.__class__.__name__, e)
71 | log_unittest(self, msg, log_level)
72 |
--------------------------------------------------------------------------------
/formiodata/components/select.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from .component import Component
5 |
6 |
7 | class selectComponent(Component):
8 |
9 | @property
10 | def dataSrc(self):
11 | return self.raw.get('dataSrc')
12 |
13 | @property
14 | def multiple(self):
15 | return self.raw.get('multiple')
16 |
17 | @property
18 | def value(self):
19 | value = super().value
20 | if self.dataSrc == 'url' and isinstance(value, str) and not value:
21 | return {}
22 | else:
23 | return value
24 |
25 | @value.setter
26 | def value(self, value):
27 | return super(self.__class__, self.__class__).value.fset(self, value)
28 |
29 | @property
30 | def value_label(self):
31 | if not self.value:
32 | return None
33 | comp = self.component_owner.input_components.get(self.key)
34 | if self.dataSrc == 'url':
35 | label = self.value['label']
36 | if self.i18n.get(self.language):
37 | return self.i18n[self.language].get(label, label)
38 | else:
39 | return label
40 | else:
41 | data_type = comp.raw.get('dataType')
42 | values = comp.raw.get('data') and comp.raw['data'].get('values')
43 | for val in values:
44 | if data_type == 'number':
45 | data_val = int(val['value'])
46 | else:
47 | data_val = val['value']
48 |
49 | if data_val == self.value:
50 | label = val['label']
51 | if self.i18n.get(self.language):
52 | return self.i18n[self.language].get(label, label)
53 | else:
54 | return label
55 | else:
56 | return None
57 |
58 | @property
59 | def value_labels(self):
60 | comp = self.component_owner.input_components.get(self.key)
61 | value_labels = []
62 |
63 | if self.dataSrc == 'url':
64 | for val in self.value:
65 | label = val['label']
66 | if self.i18n.get(self.language):
67 | label = self.i18n[self.language].get(label, label)
68 | value_labels.append(label)
69 | else:
70 | data_type = comp.raw.get('dataType')
71 | values = comp.raw.get('data') and comp.raw['data'].get('values')
72 |
73 | for val in values:
74 | if data_type == 'number':
75 | data_val = int(val['value'])
76 | else:
77 | data_val = val['value']
78 |
79 | if self.value and data_val in self.value:
80 | if self.i18n.get(self.language):
81 | value_labels.append(self.i18n[self.language].get(val['label'], val['label']))
82 | else:
83 | value_labels.append(val['label'])
84 | return value_labels
85 |
--------------------------------------------------------------------------------
/tests/test_validation_error_simple.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from tests.utils import readfile
5 | from test_component import ComponentTestCase
6 |
7 | from formiodata.form import Form
8 |
9 |
10 | class ValidationErrorSimpleTestCase(ComponentTestCase):
11 |
12 | def setUp(self):
13 | super(ValidationErrorSimpleTestCase, self).setUp()
14 | self.form_validation_errors_json = readfile('data', 'test_example_form_validation_errors.json')
15 | self.form_validation_errors = Form(self.form_validation_errors_json, self.builder)
16 | self.form_validation_errors_i18n_nl = Form(self.form_validation_errors_json, self.builder_i18n_nl)
17 |
18 | def test_required_components_in_builder(self):
19 | firstName = self.builder.input_components['firstName']
20 | self.assertTrue(firstName.required)
21 |
22 | lastName = self.builder.input_components['lastName']
23 | self.assertTrue(lastName.required)
24 |
25 | email = self.builder.input_components['email']
26 | self.assertFalse(email.required)
27 |
28 | def test_required_components_form_validation_errors(self):
29 | errors = self.form_validation_errors.validation_errors()
30 |
31 | self.assertEqual(
32 | errors['firstName']['required'],
33 | 'First Name is required'
34 | )
35 | self.assertEqual(
36 | errors['lastName']['required'],
37 | 'Last Name is required'
38 | )
39 | self.assertEqual(
40 | errors['dataGrid'][0]['textField']['required'],
41 | 'Text Field is required'
42 | )
43 | self.assertEqual(
44 | errors['dataGrid'][1],
45 | {}
46 | )
47 | self.assertEqual(
48 | errors['dataGrid'][2]['textField']['required'],
49 | 'Text Field is required'
50 | )
51 | self.assertEqual(
52 | errors['dataGrid'][3],
53 | {}
54 | )
55 |
56 | def test_required_components_form_validation_errors_i18n_nl(self):
57 | errors = self.form_validation_errors_i18n_nl.validation_errors()
58 |
59 | self.assertEqual(
60 | errors['firstName']['required'],
61 | 'Voornaam is verplicht'
62 | )
63 | self.assertEqual(
64 | errors['lastName']['required'],
65 | 'Achternaam is verplicht'
66 | )
67 | self.assertEqual(
68 | errors['dataGrid'][0]['textField']['required'],
69 | 'Tekstveld is verplicht'
70 | )
71 | self.assertEqual(
72 | errors['dataGrid'][1],
73 | {}
74 | )
75 | self.assertEqual(
76 | errors['dataGrid'][2]['textField']['required'],
77 | 'Tekstveld is verplicht'
78 | )
79 | self.assertEqual(
80 | errors['dataGrid'][3],
81 | {}
82 | )
83 |
84 | def test_not_required_components_form(self):
85 | errors = self.form_validation_errors_i18n_nl.validation_errors()
86 | self.assertNotIn('email', errors)
87 |
--------------------------------------------------------------------------------
/formiodata/components/columns.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from .layout_base import layoutComponentBase
5 |
6 |
7 | class columnsComponent(layoutComponentBase):
8 |
9 | def load_data(self, data, is_form=False):
10 | for column in self.raw['columns']:
11 | for component in column['components']:
12 | # Only determine and load class if component type.
13 | if 'type' in component:
14 | component_obj = self.builder.get_component_object(component)
15 | component_obj.load(
16 | self.child_component_owner,
17 | parent=self,
18 | data=data,
19 | all_data=self._all_data,
20 | is_form=is_form,
21 | )
22 |
23 | @property
24 | def rows(self):
25 | rows = []
26 |
27 | row = []
28 | col_data = {'column': None, 'components': []}
29 | total_width = 0
30 |
31 | for col in self.raw['columns']:
32 | components = []
33 |
34 | for col_comp in col['components']:
35 | for key, comp in self.components.items():
36 | if col_comp['id'] == comp.id:
37 | components.append(comp)
38 |
39 | if col['width'] >= 12:
40 | # add previous (loop) row
41 | if row:
42 | rows.append(row)
43 |
44 | # init new row and add to rows
45 | row = [{'column': col, 'components': components}]
46 | rows.append(row)
47 |
48 | # init next loop (new row and total_width)
49 | row = []
50 | total_width = 0
51 | elif total_width >= 12:
52 | # add previous (loop) row
53 | rows.append(row)
54 | row = []
55 | # init new row for next loop
56 | col_data = {'column': col, 'components': components}
57 | row.append(col_data)
58 | total_width = col['width']
59 | else:
60 | if not row:
61 | row = [{'column': col, 'components': components}]
62 | else:
63 | col_data = {'column': col, 'components': components}
64 | row.append(col_data)
65 | total_width += col['width']
66 | if row:
67 | # add last generated row
68 | rows.append(row)
69 | return rows
70 |
71 | def render(self):
72 | html_rows = []
73 | for row in self.rows:
74 | html_cells = []
75 | for col in row:
76 | for component in col['components']:
77 | if component.is_visible:
78 | component.render()
79 | else:
80 | component.html_component = ''
81 | html_cells.append(''+component.html_component+' | ')
82 |
83 | html_rows.append(''+(''.join(html_cells))+'
')
84 |
85 | self.html_component = ''
86 |
--------------------------------------------------------------------------------
/formiodata/components/day.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | import calendar
5 |
6 | from collections import OrderedDict
7 |
8 | from .component import Component
9 |
10 |
11 | class dayComponent(Component):
12 |
13 | @property
14 | def dayFirst(self):
15 | return self.raw.get('dayFirst')
16 |
17 | @property
18 | def value(self):
19 | return super().value
20 |
21 | @value.setter
22 | def value(self, value):
23 | """
24 | Notes:
25 | - value format: dd/dd/yyyy
26 | - Empty value: '00/00/0000'
27 | """
28 | val = OrderedDict()
29 | fields = self.raw['fields']
30 |
31 | # XXX Maybe future formio versions have more formatting possibilites.
32 | if self.dayFirst:
33 | if not fields['day'].get('hide'):
34 | day_val = value[0:2]
35 | if day_val != '00':
36 | val['day'] = int(day_val)
37 | else:
38 | val['day'] = None
39 | if not fields['month'].get('hide'):
40 | month_val = value[3:5]
41 | if month_val != '00':
42 | val['month'] = int(month_val)
43 | else:
44 | val['month'] = None
45 | else:
46 | if not fields['month'].get('hide'):
47 | month_val = value[0:2]
48 | if month_val != '00':
49 | val['month'] = int(month_val)
50 | else:
51 | val['month'] = None
52 | if not fields['day'].get('hide'):
53 | day_val = value[3:5]
54 | if day_val != '00':
55 | val['day'] = int(day_val)
56 | else:
57 | val['day'] = None
58 |
59 | if not fields['year'].get('hide'):
60 | if not fields['year'].get('hide'):
61 | year_val = value[6:10]
62 | if year_val != '0000':
63 | val['year'] = int(year_val)
64 | else:
65 | val['year'] = None
66 |
67 | super(self.__class__, self.__class__).value.fset(self, val)
68 |
69 | @property
70 | def day(self):
71 | fields = self.raw['fields']
72 | if not fields['day'].get('hide'):
73 | return self.value['day']
74 | else:
75 | return None
76 |
77 | @property
78 | def month(self):
79 | fields = self.raw['fields']
80 | if not fields['month'].get('hide'):
81 | return self.value['month']
82 | else:
83 | return None
84 |
85 | @property
86 | def month_name(self):
87 | fields = self.raw['fields']
88 | if self.value['month'] and not fields['month'].get('hide'):
89 | month_name = calendar.month_name[self.value['month']]
90 | if self.i18n.get(self.language):
91 | return self.i18n[self.language].get(month_name, month_name)
92 | else:
93 | return month_name
94 | else:
95 | return None
96 |
97 | @property
98 | def year(self):
99 | fields = self.raw['fields']
100 | if not fields['year'].get('hide'):
101 | return self.value['year']
102 | else:
103 | return None
104 |
--------------------------------------------------------------------------------
/tests/test_component_datagrid.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from test_component import ComponentTestCase
5 | from formiodata.components.datagrid import datagridComponent
6 |
7 |
8 | class datagridComponentTestCase(ComponentTestCase):
9 |
10 | def test_object(self):
11 | dataGrid = self.builder.components['dataGrid']
12 | self.assertIsInstance(dataGrid, datagridComponent)
13 | email = self.builder.input_components['email']
14 | self.assertNotIsInstance(email, datagridComponent)
15 |
16 | def test_get_key(self):
17 | dataGrid = self.builder.components['dataGrid']
18 | self.assertEqual(dataGrid.key, 'dataGrid')
19 |
20 | def test_get_type(self):
21 | dataGrid = self.builder.components['dataGrid']
22 | self.assertEqual(dataGrid.type, 'datagrid')
23 |
24 | def test_get_label(self):
25 | dataGrid = self.builder.components['dataGrid']
26 | self.assertEqual(dataGrid.label, 'Data Grid')
27 |
28 | def test_paths(self):
29 | dataGrid = self.builder.components['dataGrid']
30 | # datagrid
31 | self.assertEqual(dataGrid.builder_path_key, ['dataGrid'])
32 | self.assertEqual(dataGrid.builder_path_label, ['Data Grid'])
33 | self.assertEqual(dataGrid.builder_input_path_key, ['dataGrid'])
34 | self.assertEqual(dataGrid.builder_input_path_label, ['Data Grid'])
35 | # datagrid inputs
36 | for pos, row_with_components in enumerate(dataGrid.rows):
37 | for component in row_with_components.input_components.values():
38 | if component.key == 'textField':
39 | self.assertEqual(component.builder_path_key, ['dataGrid', 'textField'])
40 | self.assertEqual(component.builder_path_label, ['Data Grid', 'Text Field'])
41 | self.assertEqual(component.builder_input_path_key, ['dataGrid', 'textField'])
42 | self.assertEqual(component.builder_input_path_label, ['Data Grid', 'Text Field'])
43 | if component.key == 'checkbox':
44 | self.assertEqual(component.builder_path_key, ['dataGrid', 'checkbox'])
45 | self.assertEqual(component.builder_path_label, ['Data Grid', 'Checkbox'])
46 | self.assertEqual(component.builder_input_path_key, ['dataGrid', 'checkbox'])
47 | self.assertEqual(component.builder_input_path_label, ['Data Grid', 'Checkbox'])
48 |
49 | def test_get_row_labels(self):
50 | builder_dataGrid = self.builder.components['dataGrid']
51 | dataGrid = self.form.input_components[builder_dataGrid.key]
52 |
53 | self.assertEqual(len(dataGrid.rows), 2)
54 |
55 | labels = ['Text Field', 'Checkbox']
56 | for key, label in dataGrid.labels.items():
57 | self.assertIn(label , labels)
58 |
59 | def test_get_rows_values(self):
60 | builder_dataGrid = self.builder.components['dataGrid']
61 | dataGrid = self.form.input_components[builder_dataGrid.key]
62 |
63 | self.assertEqual(len(dataGrid.rows), 2)
64 |
65 | textField_values = ['abc', 'def']
66 | checkbox_values = [True, False]
67 | for pos, row_with_components in enumerate(dataGrid.rows):
68 | for component in row_with_components.input_components.values():
69 | if component.key == 'textField':
70 | self.assertEqual(textField_values[pos], component.value)
71 | if component.key == 'checkbox':
72 | self.assertEqual(checkbox_values[pos], component.value)
73 |
--------------------------------------------------------------------------------
/tests/test_conditional_visibility_simple.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | import unittest
5 |
6 | from tests.utils import readfile, ConditionalVisibilityTestHelpers
7 | from formiodata.builder import Builder
8 | from formiodata.form import Form
9 |
10 |
11 | class ConditionalVisibilitySimpleTestCase(ConditionalVisibilityTestHelpers, unittest.TestCase):
12 | def setUp(self):
13 | super(ConditionalVisibilitySimpleTestCase, self).setUp()
14 |
15 | self.builder_json = readfile('data', 'test_conditional_visibility_simple_builder.json')
16 | self.hide_password_form_json = readfile('data', 'test_conditional_visibility_simple_hide_password.json')
17 | self.show_selectboxes_form_json = readfile('data', 'test_conditional_visibility_simple_show_selectboxes.json')
18 | self.show_textfield_form_json = readfile('data', 'test_conditional_visibility_simple_show_textfield.json')
19 |
20 | def test_conditionally_shown_components_have_default_state_in_builder(self):
21 | builder = Builder(self.builder_json)
22 |
23 | self.assertVisible(builder.input_components['textField'])
24 | self.assertNotVisible(builder.input_components['maybeTextField'])
25 | self.assertVisible(builder.input_components['maybePassword'])
26 | self.assertNotVisible(builder.input_components['sales'])
27 | self.assertNotVisible(builder.input_components['technology'])
28 |
29 | def test_conditionally_shown_components_toggle_on_condition_being_met(self):
30 | builder = Builder(self.builder_json)
31 |
32 | hide_password_form = Form(self.hide_password_form_json, builder)
33 | self.assertVisible(hide_password_form.input_components['textField'])
34 | self.assertNotVisible(hide_password_form.input_components['maybeTextField'])
35 | self.assertNotVisible(hide_password_form.input_components['maybePassword'])
36 |
37 | show_textfield_form = Form(self.show_textfield_form_json, builder)
38 | self.assertVisible(show_textfield_form.input_components['textField'])
39 | self.assertVisible(show_textfield_form.input_components['maybeTextField'])
40 | self.assertVisible(show_textfield_form.input_components['maybePassword'])
41 |
42 | show_selectboxes_form = Form(self.show_selectboxes_form_json, builder)
43 | self.assertVisible(show_selectboxes_form.input_components['jobArea'])
44 | self.assertVisible(show_selectboxes_form.input_components['technology'])
45 | self.assertNotVisible(show_selectboxes_form.input_components['sales'])
46 |
47 | def test_conditionally_shown_components_do_not_render_when_hidden(self):
48 | builder = Builder(self.builder_json)
49 |
50 | hide_password_form = Form(self.hide_password_form_json, builder)
51 | hide_password_form.render_components()
52 | self.assertEqual('hide!
', hide_password_form.input_components['textField'].html_component)
53 | self.assertEqual('', hide_password_form.input_components['maybeTextField'].html_component)
54 | self.assertEqual('', hide_password_form.input_components['maybePassword'].html_component)
55 |
56 | show_textfield_form = Form(self.show_textfield_form_json, builder)
57 | show_textfield_form.render_components()
58 | self.assertEqual('show!
', show_textfield_form.input_components['textField'].html_component)
59 | self.assertEqual('maybe yes
', show_textfield_form.input_components['maybeTextField'].html_component)
60 | self.assertEqual('hunter2
', show_textfield_form.input_components['maybePassword'].html_component)
61 |
--------------------------------------------------------------------------------
/tests/test_component_selectboxes.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from test_component import ComponentTestCase
5 | from formiodata.components.selectboxes import selectboxesComponent
6 |
7 |
8 | class selectboxesComponentTestCase(ComponentTestCase):
9 |
10 | def setUp(self):
11 | super(selectboxesComponentTestCase, self).setUp()
12 | self.selectboxes_value = {
13 | "north": True,
14 | "east": False,
15 | "south": True,
16 | "west": False,
17 | }
18 | self.selectboxes_values_labels = {
19 | "north": {"key": "north", "label": "North", "value": True},
20 | "east": {"key": "east", "label": "East", "value": False},
21 | "south": {"key": "south", "label": "South", "value": True},
22 | "west": {"key": "west", "label": "West", "value": False},
23 | }
24 | self.selectboxes_values_labels_i18n_nl = {
25 | "north": {"key": "north", "label": "Noord", "value": True},
26 | "east": {"key": "east", "label": "Oost", "value": False},
27 | "south": {"key": "south", "label": "Zuid", "value": True},
28 | "west": {"key": "west", "label": "West", "value": False},
29 | }
30 |
31 | def test_object(self):
32 | # selectboxesComponent
33 | selectBoxes = self.builder.input_components['selectBoxes']
34 | self.assertIsInstance(selectBoxes, selectboxesComponent)
35 |
36 | # Not selectboxesComponent
37 | email = self.builder.input_components['email']
38 | self.assertNotIsInstance(email, selectboxesComponent)
39 |
40 | def test_get_key(self):
41 | selectBoxes = self.builder.input_components['selectBoxes']
42 | self.assertEqual(selectBoxes.key, 'selectBoxes')
43 |
44 | def test_get_type(self):
45 | selectBoxes = self.builder.input_components['selectBoxes']
46 | self.assertEqual(selectBoxes.type, 'selectboxes')
47 |
48 | def test_get_label(self):
49 | selectBoxes = self.builder.input_components['selectBoxes']
50 | self.assertEqual(selectBoxes.label, 'Select Boxes')
51 |
52 | def test_set_label(self):
53 | selectBoxes = self.builder.input_components['selectBoxes']
54 | self.assertEqual(selectBoxes.label, 'Select Boxes')
55 | selectBoxes.label = 'Other Select Boxes'
56 | self.assertEqual(selectBoxes.label, 'Other Select Boxes')
57 |
58 | def test_get_form(self):
59 | selectBoxes = self.form.input_components['selectBoxes']
60 | self.assertEqual(selectBoxes.label, 'Select Boxes')
61 | self.assertEqual(selectBoxes.value, self.selectboxes_value)
62 | self.assertEqual(selectBoxes.values_labels, self.selectboxes_values_labels)
63 | self.assertEqual(selectBoxes.type, 'selectboxes')
64 |
65 | def test_get_form_data(self):
66 | selectBoxes = self.form.input.selectBoxes
67 | self.assertEqual(selectBoxes.label, 'Select Boxes')
68 | self.assertEqual(selectBoxes.value, self.selectboxes_value)
69 | self.assertEqual(selectBoxes.values_labels, self.selectboxes_values_labels)
70 | self.assertEqual(selectBoxes.type, 'selectboxes')
71 |
72 | # i18n translations
73 | def test_get_label_i18n_nl(self):
74 | food = self.builder_i18n_nl.input_components['selectBoxes']
75 | self.assertEqual(food.label, 'Select aanvink opties')
76 |
77 | def test_get_form_data_i18n_nl(self):
78 | self.assertEqual(self.form_i18n_nl.input.selectBoxes.label, 'Select aanvink opties')
79 | self.assertEqual(self.form_i18n_nl.input.selectBoxes.value, self.selectboxes_value)
80 | self.assertEqual(
81 | self.form_i18n_nl.input.selectBoxes.values_labels,
82 | self.selectboxes_values_labels_i18n_nl,
83 | )
84 |
--------------------------------------------------------------------------------
/tests/test_component_datetime.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from datetime import datetime, date
5 |
6 | from test_component import ComponentTestCase
7 | from formiodata.components.datetime import datetimeComponent
8 |
9 |
10 | class datetimeComponentTestCase(ComponentTestCase):
11 |
12 | def test_object(self):
13 | # datetimeComponent
14 | birthdate = self.builder.input_components['birthdate']
15 | self.assertIsInstance(birthdate, datetimeComponent)
16 |
17 | appointmentDateTime = self.builder.input_components['appointmentDateTime']
18 | self.assertIsInstance(appointmentDateTime, datetimeComponent)
19 |
20 | # Not datetimeComponent
21 | email = self.builder.input_components['email']
22 | self.assertNotIsInstance(email, datetimeComponent)
23 |
24 | def test_get_key(self):
25 | birthdate = self.builder.input_components['birthdate']
26 | self.assertEqual(birthdate.key, 'birthdate')
27 |
28 | appointmentDateTime = self.builder.input_components['appointmentDateTime']
29 | self.assertEqual(appointmentDateTime.key, 'appointmentDateTime')
30 |
31 | def test_get_type(self):
32 | birthdate = self.builder.input_components['birthdate']
33 | self.assertEqual(birthdate.type, 'datetime')
34 |
35 | appointmentDateTime = self.builder.input_components['appointmentDateTime']
36 | self.assertEqual(appointmentDateTime.type, 'datetime')
37 |
38 | def test_get_label(self):
39 | birthdate = self.builder.input_components['birthdate']
40 | self.assertEqual(birthdate.label, 'Birthdate')
41 |
42 | appointmentDateTime = self.builder.input_components['appointmentDateTime']
43 | self.assertEqual(appointmentDateTime.label, 'Appointment Date / Time')
44 |
45 | def test_set_label(self):
46 | birthdate = self.builder.input_components['birthdate']
47 | self.assertEqual(birthdate.label, 'Birthdate')
48 | birthdate.label = 'Born On'
49 | self.assertEqual(birthdate.label, 'Born On')
50 |
51 | appointmentDateTime = self.builder.input_components['appointmentDateTime']
52 | self.assertEqual(appointmentDateTime.label, 'Appointment Date / Time')
53 | appointmentDateTime.label = 'Appointment On'
54 | self.assertEqual(appointmentDateTime.label, 'Appointment On')
55 |
56 | def test_get_form(self):
57 | birthdate = self.form.input_components['birthdate']
58 | self.assertEqual(birthdate.label, 'Birthdate')
59 | self.assertEqual(birthdate.value, '1999-12-31T12:00:00+01:00')
60 | self.assertEqual(birthdate.type, 'datetime')
61 |
62 | appointmentDateTime = self.form.input_components['appointmentDateTime']
63 | self.assertEqual(appointmentDateTime.label, 'Appointment Date / Time')
64 | self.assertEqual(appointmentDateTime.value, '2021-02-26T12:30:00+01:00')
65 | self.assertEqual(appointmentDateTime.type, 'datetime')
66 |
67 | def test_get_form_data(self):
68 | birthdate = self.form.input.birthdate
69 | self.assertEqual(birthdate.label, 'Birthdate')
70 | self.assertEqual(birthdate.value, '1999-12-31T12:00:00+01:00')
71 | self.assertEqual(birthdate.type, 'datetime')
72 |
73 | appointmentDateTime = self.form.input.appointmentDateTime
74 | self.assertEqual(appointmentDateTime.label, 'Appointment Date / Time')
75 | self.assertEqual(appointmentDateTime.value, '2021-02-26T12:30:00+01:00')
76 | self.assertEqual(appointmentDateTime.type, 'datetime')
77 |
78 | def test_to_datetime(self):
79 | birthdate = self.form.input.birthdate
80 | self.assertIsInstance(birthdate.to_datetime(), datetime)
81 | self.assertIsInstance(birthdate.to_datetime().date(), date)
82 |
83 | appointmentDateTime = self.form.input.appointmentDateTime
84 | self.assertIsInstance(appointmentDateTime.to_datetime(), datetime)
85 |
86 | # i18n translations
87 | def test_get_label_i18n_nl(self):
88 | appointmentDateTime = self.builder_i18n_nl.input_components['appointmentDateTime']
89 | self.assertEqual(appointmentDateTime.label, 'Afspraak Tijdstip')
90 |
91 | def test_get_form_data_i18n_nl(self):
92 | self.assertEqual(self.form_i18n_nl.input.appointmentDateTime.label, 'Afspraak Tijdstip')
93 |
--------------------------------------------------------------------------------
/tests/test_component_file_storage_base64.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from test_component import ComponentTestCase
5 | from formiodata.components.file import fileComponent
6 |
7 |
8 | class fileComponentStorageBase64TestCase(ComponentTestCase):
9 |
10 | def setUp(self):
11 | super(fileComponentStorageBase64TestCase, self).setUp()
12 |
13 | self.image_value_base64 = "data:image/gif;base64,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"
14 |
15 | def test_object(self):
16 | uploadBase64 = self.builder.input_components['uploadBase64']
17 | self.assertIsInstance(uploadBase64, fileComponent)
18 |
19 | # Not fileComponent
20 | email = self.builder.input_components['email']
21 | self.assertNotIsInstance(email, fileComponent)
22 |
23 | def test_get_key(self):
24 | uploadBase64 = self.builder.input_components['uploadBase64']
25 | self.assertEqual(uploadBase64.key, 'uploadBase64')
26 |
27 | def test_get_type(self):
28 | uploadBase64 = self.builder.input_components['uploadBase64']
29 | self.assertEqual(uploadBase64.type, 'file')
30 |
31 | def test_get_label(self):
32 | uploadBase64 = self.builder.input_components['uploadBase64']
33 | self.assertEqual(uploadBase64.label, 'Upload Base64')
34 |
35 | def test_set_label(self):
36 | uploadBase64 = self.builder.input_components['uploadBase64']
37 | self.assertEqual(uploadBase64.label, 'Upload Base64')
38 | uploadBase64.label = 'Foobar'
39 | self.assertEqual(uploadBase64.label, 'Foobar')
40 |
41 | def test_get_form(self):
42 | uploadBase64 = self.form.components['uploadBase64']
43 | self.assertEqual(uploadBase64.label, 'Upload Base64')
44 | self.assertEqual(uploadBase64.type, 'file')
45 | self.assertEqual(uploadBase64.storage, 'base64')
46 | self.assertEqual(uploadBase64.value, self.image_value_base64)
47 |
48 | def test_get_form_data(self):
49 | uploadBase64 = self.form.input.uploadBase64
50 | self.assertEqual(uploadBase64.label, 'Upload Base64')
51 | self.assertEqual(uploadBase64.type, 'file')
52 | self.assertEqual(uploadBase64.storage, 'base64')
53 | self.assertEqual(uploadBase64.value, self.image_value_base64)
54 |
55 | # i18n translations
56 | def test_get_label_i18n_nl(self):
57 | uploadBase64 = self.builder_i18n_nl.input_components['uploadBase64']
58 | self.assertEqual(uploadBase64.label, 'Upload binair naar ASCII')
59 |
60 | def test_get_form_data_i18n_nl(self):
61 | self.assertEqual(self.form_i18n_nl.input.uploadBase64.label, 'Upload binair naar ASCII')
62 |
--------------------------------------------------------------------------------
/formiodata/components/grid_base.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from collections import OrderedDict
5 |
6 | from .component import Component
7 |
8 |
9 | class baseGridComponent(Component):
10 |
11 | class gridRow:
12 | """Not *really* a component, but it implements the same
13 | partial interface with input_components and components.
14 | TODO: Consider if there should be a shared base component for
15 | this (ComponentOwner?)
16 | """
17 | def __init__(self, grid, data):
18 | self.grid = grid
19 | self.builder = grid.builder
20 | self.builder_path = None
21 | self.input_components = {}
22 | self.components = OrderedDict()
23 | self.form = grid.form
24 | self.row = data
25 | self.html_component = ''
26 | grid.create_component_objects(self, data)
27 |
28 | def render(self):
29 | html_components = []
30 | for component in self.components.values():
31 | if component.is_visible:
32 | component.render()
33 | else:
34 | component.html_component = ''
35 | html_components.append(''+component.html_component+' | ')
36 | self.html_component = ''+(''.join(html_components))+'
'
37 |
38 | def __init__(self, raw, builder, **kwargs):
39 | # TODO when adding other data/grid components, create new
40 | # dataComponent class these can inherit from.
41 | self.input_components = {}
42 | self.rows = []
43 | super().__init__(raw, builder, **kwargs)
44 | self.form = {'value': []}
45 |
46 | def create_component_objects(self, parent, data):
47 | """This is a weird one, it creates component object for the
48 | "blueprint" inside the Builder, with parent = grid, and in
49 | a form on each grid row with parent = gridRow
50 | """
51 | for component in self.raw.get('components', []):
52 | # Only determine and load class if component type.
53 | if 'type' in component:
54 | component_obj = parent.builder.get_component_object(component)
55 | component_obj.load(component_owner=parent, parent=parent, data=data, all_data=self._all_data)
56 | parent.components[component_obj.key] = component_obj
57 |
58 | def load_data(self, data, is_form=False):
59 | # Always instantiate child components, even if no data.
60 | # This makes it exist both in the builder and in the form.
61 | self.create_component_objects(self, data)
62 |
63 | # TODO: Make sure data is always a dict here?
64 | if data and data.get(self.key):
65 | self._load_rows(data[self.key])
66 | self.value = data[self.key]
67 | self.raw_value = data[self.key]
68 | elif not self.initEmpty and not is_form:
69 | self.rows = [self.gridRow(self, None)]
70 |
71 | def _load_rows(self, data):
72 | rows = []
73 |
74 | for row in data:
75 | # EXAMPLE row (which is an entry in the data list):
76 | # {'email': 'personal@example.com', 'typeOfEmail': 'personal'}
77 | new_row = self.gridRow(self, row)
78 |
79 | if new_row:
80 | rows.append(new_row)
81 | self.rows = rows
82 |
83 | @property
84 | def labels(self):
85 | labels = OrderedDict()
86 | for comp in self.raw['components']:
87 | if self.i18n.get(self.language):
88 | label = self.i18n[self.language].get(comp['label'], comp['label'])
89 | else:
90 | label = comp['label']
91 | labels[comp['key']] = label
92 | return labels
93 |
94 | @property
95 | def is_form_component(self):
96 | # NOTE: A grid is not _really_ a form component, but it
97 | # has a key in the JSON for loading the form, so it acts as
98 | # such, and it will create an entry in the "input_components"
99 | # property of its owner.
100 | return True
101 |
102 | @property
103 | def child_component_owner(self):
104 | return self
105 |
106 | @property
107 | def initEmpty(self):
108 | return self.raw.get('initEmpty')
109 |
110 | def validation_errors(self):
111 | errors = []
112 | for row_idx, row in enumerate(self.rows):
113 | row_errors = {}
114 | for component_key, component in row.input_components.items():
115 | component_errors = component.validation_errors()
116 | if bool(component_errors):
117 | # scalar (not grid) components retrieve a Dict
118 | # from method validation_errors()
119 | row_errors[component_key] = component_errors
120 | if bool(row_errors):
121 | errors.append(row_errors)
122 | else:
123 | errors.append({})
124 | return errors
125 |
126 | def render(self):
127 | for row in self.rows:
128 | row.render()
129 | self.html_component = ''+(''.join([row.html_component for row in self.rows]))+'
'
130 |
--------------------------------------------------------------------------------
/formiodata/builder.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | import json
5 | import logging
6 |
7 | from collections import OrderedDict
8 | from copy import deepcopy
9 |
10 | from formiodata.components.component import Component
11 |
12 | logger = logging.getLogger(__name__)
13 |
14 |
15 | class Builder:
16 |
17 | def __init__(
18 | self,
19 | schema_json,
20 | language='en',
21 | i18n={},
22 | resources={},
23 | load_path_objects=True,
24 | component_class_mapping={},
25 | **kwargs
26 | ):
27 | """
28 | @param schema_json
29 | @param language
30 | @param i18n
31 | @param resources
32 | @param resources
33 | @param load_path_objects
34 | @param component_class_mapping
35 | """
36 |
37 | if isinstance(schema_json, dict):
38 | self.schema = schema_json
39 | else:
40 | self.schema = json.loads(schema_json)
41 |
42 | self.language = language
43 | self.i18n = i18n
44 | self.resources = resources
45 | self.load_path_objects = load_path_objects
46 | self.component_class_mapping = component_class_mapping
47 |
48 | # Raw components from the schema
49 | self._raw_components = []
50 |
51 | # Raw components enriched with Component(object) API.
52 | self.raw_components = []
53 |
54 | # Key/value dictionay of all components for instant access.
55 | self.components = OrderedDict()
56 | self.components_path_key = OrderedDict()
57 | self.component_ids = OrderedDict()
58 |
59 | # Key/value dictionay of Form input-only components (i.e., no layout components) for instant access.
60 | self.input_components = {}
61 |
62 | # Set/load component attrs intialized above.
63 | self.load_components()
64 |
65 | # TODO kwargs['component_cls']
66 | # Custom component classes
67 | self._component_cls = []
68 |
69 | def load_components(self):
70 | self._raw_components = self.schema.get('components')
71 | self.raw_components = deepcopy(self.schema.get('components'))
72 | if self.raw_components:
73 | self._load_components(self.raw_components)
74 |
75 | def _load_components(self, components, parent=None):
76 | """
77 | @param components
78 | """
79 | for component in components:
80 | # Only determine and load class if component type.
81 | if 'type' in component:
82 | component_obj = self.get_component_object(component)
83 | # start and traverse from toplevel
84 | component_obj.load(component_owner=self, parent=None, data=None, is_form=False)
85 | self.components[component_obj.key] = component_obj
86 |
87 | def get_component_class(self, component):
88 | component_type = component.get('type')
89 | try:
90 | mapping_value = self.component_class_mapping[component_type]
91 | if isinstance(mapping_value, str):
92 | cls_name = '%sComponent' % mapping_value
93 | import_path = 'formiodata.components.%s' % mapping_value
94 | module = __import__(import_path, fromlist=[cls_name])
95 | cls = getattr(module, cls_name)
96 | else:
97 | cls = self.component_class_mapping[component_type]
98 | except KeyError:
99 | cls_name = '%sComponent' % component_type
100 | import_path = 'formiodata.components.%s' % component_type
101 | module = __import__(import_path, fromlist=[cls_name])
102 | cls = getattr(module, cls_name)
103 | return cls
104 |
105 | def get_component_object(self, component):
106 | """
107 | @param component
108 | """
109 | component_type = component.get('type')
110 | if component_type:
111 | try:
112 | cls = self.get_component_class(component)
113 | component_obj = cls(component, self, language=self.language, i18n=self.i18n, resources=self.resources)
114 | return component_obj
115 | except (AttributeError, ModuleNotFoundError) as e:
116 | # TODO try to find/load first from self._component_cls else
117 | # re-raise exception or silence (log error and return False)
118 | logger.warning(e)
119 | logger.warning("Falling back to the base Component object.")
120 | # TODO: implement property (by kwargs) whether to return
121 | # (raw) Component object or throw exception,
122 | return Component(component, self)
123 | else:
124 | msg = "Can't instantiate a (raw) component without a type.\n\n" \
125 | "Component raw data\n" \
126 | "==================\n" \
127 | "%s\n"
128 | logging.warning(msg % component)
129 | return False
130 |
131 | @property
132 | def form(self):
133 | """
134 | Placeholder form dict, always empty. Useful in contexts where the component owner's form
135 | is requested because there is a need for form data.
136 | """
137 | return {}
138 |
--------------------------------------------------------------------------------
/tests/test_component_editgrid.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from test_component import ComponentTestCase
5 | from formiodata.components.editgrid import editgridComponent
6 |
7 |
8 | class editgridComponentTestCase(ComponentTestCase):
9 |
10 | def test_object(self):
11 | editgrid = self.builder.components['editGrid']
12 | self.assertIsInstance(editgrid, editgridComponent)
13 | email = self.builder.input_components['email']
14 | self.assertNotIsInstance(email, editgridComponent)
15 |
16 | def test_get_key(self):
17 | editGrid = self.builder.components['editGrid']
18 | self.assertEqual(editGrid.key, 'editGrid')
19 |
20 | def test_get_type(self):
21 | editGrid = self.builder.components['editGrid']
22 | self.assertEqual(editGrid.type, 'editgrid')
23 |
24 | def test_get_label(self):
25 | editGrid = self.builder.components['editGrid']
26 | self.assertEqual(editGrid.label, 'Edit Grid')
27 |
28 | def test_paths(self):
29 | editGrid = self.builder.components['editGrid']
30 | # editgrid
31 | self.assertEqual(editGrid.builder_path_key, ['editGrid'])
32 | self.assertEqual(editGrid.builder_path_label, ['Edit Grid'])
33 | self.assertEqual(editGrid.builder_input_path_key, ['editGrid'])
34 | self.assertEqual(editGrid.builder_input_path_label, ['Edit Grid'])
35 | # editgrid inputs
36 | for pos, row_with_components in enumerate(editGrid.rows):
37 | for component in row_with_components.input_components.values():
38 | if component.key == 'textfield':
39 | self.assertEqual(component.builder_path_key, ['editGrid', 'textField'])
40 | self.assertEqual(component.builder_path_label, ['Edit Grid', 'Text Field'])
41 | self.assertEqual(component.builder_input_path_key, ['editGrid', 'textField'])
42 | self.assertEqual(component.builder_input_path_label, ['Edit Grid', 'Text Field'])
43 | if component.key == 'date':
44 | self.assertEqual(component.builder_path_key, ['editGrid', 'datetime'])
45 | self.assertEqual(component.builder_path_label, ['Edit Grid', 'Date'])
46 | self.assertEqual(component.builder_input_path_key, ['editGrid', 'datetime'])
47 | self.assertEqual(component.builder_input_path_label, ['Edit Grid', 'Date'])
48 | if component.key == 'textArea':
49 | self.assertEqual(component.builder_path_key, ['editGrid', 'textArea'])
50 | self.assertEqual(component.builder_path_label, ['Edit Grid', 'Text Area'])
51 | self.assertEqual(component.builder_input_path_key, ['editGrid', 'textArea'])
52 | self.assertEqual(component.builder_input_path_label, ['Edit Grid', 'Text Area'])
53 | if component.key == 'number':
54 | self.assertEqual(component.builder_path_key, ['editGrid', 'number'])
55 | self.assertEqual(component.builder_path_label, ['Edit Grid', 'Number'])
56 | self.assertEqual(component.builder_input_path_key, ['editGrid', 'number'])
57 | self.assertEqual(component.builder_input_path_label, ['Edit Grid', 'Number'])
58 | if component.key == 'checkbox':
59 | self.assertEqual(component.builder_path_key, ['editGrid', 'checkbox'])
60 | self.assertEqual(component.builder_path_label, ['Edit Grid', 'Checkbox'])
61 | self.assertEqual(component.builder_input_path_key, ['editGrid', 'checkbox'])
62 | self.assertEqual(component.builder_input_path_label, ['Edit Grid', 'Checkbox'])
63 |
64 | def test_get_row_labels(self):
65 | builder_editGrid = self.builder.components['editGrid']
66 | editGrid = self.form.input_components[builder_editGrid.key]
67 |
68 | self.assertEqual(len(editGrid.rows), 3)
69 |
70 | labels = ['Text Field', 'Text Area', 'Date', 'Select', 'Number', 'Checkbox']
71 | for key, label in editGrid.labels.items():
72 | self.assertIn(label , labels)
73 |
74 | def test_get_rows_values(self):
75 | builder_editGrid = self.builder.components['editGrid']
76 | editGrid = self.form.input_components[builder_editGrid.key]
77 |
78 | self.assertEqual(len(editGrid.rows), 3)
79 |
80 | textField_values = ['This is the FIRST row', 'This is the SECOND row', 'This is the THIRD row']
81 | date_values = ['2023-01-02', '2023-05-26', '2023-07-03']
82 | number_values = [123, 789, 456]
83 | checkbox_values = [True, False, False]
84 | textArea_values = ['The first textarea.\nSuch nice!', '', '']
85 | for pos, row_with_components in enumerate(editGrid.rows):
86 | for component in row_with_components.input_components.values():
87 | if component.key == 'textField':
88 | self.assertEqual(textField_values[pos], component.value)
89 | if component.key == 'date':
90 | self.assertIn(date_values[pos], component.value)
91 | if component.key == 'textArea':
92 | self.assertIn(textArea_values[pos], component.value)
93 | if component.key == 'number':
94 | self.assertIn(number_values[pos], component.value)
95 | if component.key == 'checkbox':
96 | self.assertIn(checkbox_values[pos], component.value)
97 |
--------------------------------------------------------------------------------
/formiodata/form.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | import json
5 | import logging
6 | import re
7 |
8 | from collections import defaultdict, OrderedDict
9 |
10 | from formiodata.builder import Builder
11 |
12 |
13 | class Form:
14 |
15 | def __init__(
16 | self,
17 | form_json,
18 | builder=None,
19 | builder_schema_json=None,
20 | lang="en",
21 | component_class_mapping={},
22 | **kwargs
23 | ):
24 | """
25 | @param form_json
26 | @param builder Builder
27 | @param builder_schema_json
28 | @param lang
29 | """
30 | if isinstance(form_json, dict):
31 | self.form = form_json
32 | else:
33 | self.form = json.loads(form_json)
34 |
35 | self.builder = builder
36 | self.builder_schema_json = builder_schema_json
37 | self.lang = lang
38 | self.component_class_mapping = component_class_mapping
39 |
40 | if self.builder and self.builder_schema_json:
41 | raise Exception("Constructor accepts either builder or builder_schema_json.")
42 |
43 | if self.builder:
44 | assert isinstance(self.builder, Builder)
45 | elif self.builder_schema_json:
46 | assert isinstance(self.builder_schema_json, str)
47 | else:
48 | raise Exception("Provide either the argument: builder or builder_schema_json.")
49 |
50 | if self.builder is None and self.builder_schema_json:
51 | self.set_builder_by_builder_schema_json()
52 |
53 | # defaults to English (en) date/time format
54 | self.date_format = kwargs.get('date_format', '%m/%d/%Y')
55 | self.time_format = kwargs.get('time_format', '%H:%M:%S')
56 |
57 | self.input_components = {}
58 |
59 | self.components = OrderedDict()
60 | self.component_ids = {}
61 |
62 | self.load_components()
63 | self._input = self._data = FormInput(self)
64 |
65 | def set_builder_by_builder_schema_json(self):
66 | self.builder = Builder(
67 | self.builder_schema_json,
68 | language=self.lang,
69 | component_class_mapping=self.component_class_mapping
70 | )
71 |
72 | def load_components(self):
73 | for key, component in self.builder.components.items():
74 | # New object, don't affect the Builder component
75 | component_obj = self.builder.get_component_object(component.raw)
76 | component_obj.load(
77 | component_owner=self,
78 | parent=None,
79 | data=self.form,
80 | all_data=self.form,
81 | is_form=True,
82 | )
83 | self.components[key] = component_obj
84 | self.component_ids[component_obj.id] = component_obj
85 |
86 | @property
87 | def input(self):
88 | return self._input
89 |
90 | @property
91 | def data(self):
92 | logging.warning('DEPRECATION WARNING: data attr/property shall be deleted in a future version.')
93 | return self._data
94 |
95 | def get_component_by_path(self, component_path):
96 | """
97 | Get component object by path
98 |
99 | (Eg provided by the Formio.js JS/API).
100 | Especially handy for data Components eg datagridComponent.
101 |
102 | Example path:
103 | dataGrid[0].lastname => lastname in the first row [0] of a datagrid
104 |
105 | # Example path_nodes:
106 | # dataGrid[0].lastname => ['dataGrid[0]', 'lastname']
107 |
108 | @param component_path: the Formio.js JS/API path
109 | @return component: a Component object
110 | """
111 | path_nodes = component_path.split('.')
112 | # Example path_nodes:
113 | # dataGrid[0].lastname => ['dataGrid[0]', 'lastname']
114 | components = self.input_components
115 | for path_node in path_nodes:
116 | # eg: regex search '[0]' in 'dataGrid[0]'
117 | m = re.search(r"\[([A-Za-z0-9_]+)\]", path_node)
118 | if m:
119 | idx_notation = m.group(0) # eg: '[0]', '[1]', etc
120 | idx = int(m.group(1)) # eg: 0, 1, etc
121 | key = path_node.replace(idx_notation, '')
122 | component = components[key]
123 | if hasattr(component, 'rows') and idx < len(component.rows):
124 | components = component.rows[idx].input_components
125 | elif path_node in components:
126 | component = components[path_node]
127 | else:
128 | component = None
129 | return component
130 |
131 | def validation_errors(self):
132 | """
133 | @return errors dict: Dictionary where key is component key and
134 | value is a Dictionary with errors.
135 | """
136 | errors = defaultdict(dict)
137 | for component_key, component in self.input_components.items():
138 | component_errors = component.validation_errors()
139 | if isinstance(component_errors, dict):
140 | for error_type, val in component_errors.items():
141 | vals = {error_type: val}
142 | errors[component_key].update(vals)
143 | elif isinstance(component_errors, list):
144 | errors[component_key] = component_errors
145 | return errors
146 |
147 | def render_components(self, force=False):
148 | for key, component in self.input_components.items():
149 | if force or component.html_component == "":
150 | if component.is_visible:
151 | component.render()
152 | else:
153 | component.html_component = ""
154 |
155 |
156 | class FormInput:
157 |
158 | def __init__(self, form):
159 | self._form = form
160 |
161 | def __getattr__(self, key):
162 | return self._form.input_components.get(key)
163 |
--------------------------------------------------------------------------------
/tests/test_component_day.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from collections import OrderedDict
5 |
6 | from test_component import ComponentTestCase
7 | from formiodata.components.day import dayComponent
8 |
9 |
10 | class dayComponentTestCase(ComponentTestCase):
11 |
12 | def test_object(self):
13 | # dayComponent
14 | monthDayYear = self.builder.input_components['monthDayYear']
15 | self.assertIsInstance(monthDayYear, dayComponent)
16 |
17 | monthYear = self.builder.input_components['monthYear']
18 | self.assertIsInstance(monthYear, dayComponent)
19 |
20 | dayMonthYear = self.builder.input_components['dayMonthYear']
21 | self.assertIsInstance(dayMonthYear, dayComponent)
22 |
23 | dayMonth = self.builder.input_components['dayMonth']
24 | self.assertIsInstance(dayMonth, dayComponent)
25 |
26 | day = self.builder.input_components['day']
27 | self.assertIsInstance(day, dayComponent)
28 |
29 | month = self.builder.input_components['month']
30 | self.assertIsInstance(month, dayComponent)
31 |
32 | year = self.builder.input_components['year']
33 | self.assertIsInstance(year, dayComponent)
34 |
35 | # Not dayComponent
36 | email = self.builder.input_components['email']
37 | self.assertNotIsInstance(email, dayComponent)
38 |
39 | def test_get_form_empty_monthDayYear(self):
40 | monthDayYear = self.form_empty.input_components['monthDayYear']
41 | self.assertEqual(monthDayYear.type, 'day')
42 | self.assertEqual(monthDayYear.label, 'Month Day Year')
43 | self.assertIsInstance(monthDayYear.value, OrderedDict)
44 | self.assertEqual(monthDayYear.value, {'month': None, 'day': None, 'year': None})
45 | # parts
46 | self.assertIsNone(monthDayYear.day)
47 | self.assertIsNone(monthDayYear.month)
48 | self.assertIsNone(monthDayYear.year)
49 |
50 | def test_get_form_monthDayYear(self):
51 | monthDayYear = self.form.input_components['monthDayYear']
52 | self.assertEqual(monthDayYear.type, 'day')
53 | self.assertEqual(monthDayYear.label, 'Month Day Year')
54 | self.assertEqual(monthDayYear.value, {'month': 5, 'day': 16, 'year': 2021})
55 | # parts
56 | self.assertEqual(monthDayYear.day, 16)
57 | self.assertEqual(monthDayYear.month, 5)
58 | self.assertEqual(monthDayYear.month_name, 'May')
59 | self.assertEqual(monthDayYear.year, 2021)
60 | # extra test
61 | self.assertIsInstance(monthDayYear.value, OrderedDict)
62 |
63 | def test_get_form_monthYear(self):
64 | monthYear = self.form.input_components['monthYear']
65 | self.assertEqual(monthYear.type, 'day')
66 | self.assertEqual(monthYear.label, 'Month Year')
67 | self.assertEqual(monthYear.value, {'month': 5, 'month': 5, 'year': 2021})
68 | # parts
69 | self.assertIsNone(monthYear.day)
70 | self.assertEqual(monthYear.month, 5)
71 | self.assertEqual(monthYear.month_name, 'May')
72 | self.assertEqual(monthYear.year, 2021)
73 |
74 | def test_get_form_dayMonthYear(self):
75 | dayMonthYear = self.form.input_components['dayMonthYear']
76 | self.assertEqual(dayMonthYear.type, 'day')
77 | self.assertEqual(dayMonthYear.label, 'Day Month Year')
78 | self.assertEqual(dayMonthYear.value, {'day': 16, 'month': 5, 'year': 2021})
79 | # parts
80 | self.assertEqual(dayMonthYear.day, 16)
81 | self.assertEqual(dayMonthYear.month, 5)
82 | self.assertEqual(dayMonthYear.month_name, 'May')
83 | self.assertEqual(dayMonthYear.year, 2021)
84 |
85 | def test_get_form_dayMonth(self):
86 | dayMonth = self.form.input_components['dayMonth']
87 | self.assertEqual(dayMonth.type, 'day')
88 | self.assertEqual(dayMonth.label, 'Day Month')
89 | self.assertEqual(dayMonth.value, {'day': 16, 'month': 5})
90 | # parts
91 | self.assertEqual(dayMonth.day, 16)
92 | self.assertEqual(dayMonth.month, 5)
93 | self.assertEqual(dayMonth.month_name, 'May')
94 | self.assertIsNone(dayMonth.year)
95 |
96 | def test_get_form_day(self):
97 | day = self.form.input_components['day']
98 | self.assertEqual(day.type, 'day')
99 | self.assertEqual(day.label, 'Day')
100 | self.assertEqual(day.value, {'day': 16})
101 | # parts
102 | self.assertEqual(day.day, 16)
103 | self.assertIsNone(day.month)
104 | self.assertIsNone(day.year)
105 |
106 | def test_get_form_month(self):
107 | month = self.form.input_components['month']
108 | self.assertEqual(month.type, 'day')
109 | self.assertEqual(month.label, 'Month')
110 | self.assertEqual(month.value, {'month': 5})
111 | # parts
112 | self.assertIsNone(month.day)
113 | self.assertEqual(month.month, 5)
114 | self.assertEqual(month.month_name, 'May')
115 | self.assertIsNone(month.year)
116 |
117 | def test_get_form_year(self):
118 | year = self.form.input_components['year']
119 | self.assertEqual(year.type, 'day')
120 | self.assertEqual(year.label, 'Year')
121 | self.assertEqual(year.value, {'year': 2021})
122 | # parts
123 | self.assertIsNone(year.day)
124 | self.assertIsNone(year.month)
125 | self.assertEqual(year.year, 2021)
126 |
127 | # i18n translations
128 | def test_get_label_i18n_nl(self):
129 | # We won't test all here
130 | monthDayYear = self.builder_i18n_nl.input_components['monthDayYear']
131 | self.assertEqual(monthDayYear.label, 'Maand dag jaar')
132 |
133 | dayMonthYear = self.builder_i18n_nl.input_components['dayMonthYear']
134 | self.assertEqual(dayMonthYear.label, 'Dag maand jaar')
135 |
136 | def test_get_form_data_i18n_nl(self):
137 | self.assertEqual(self.form_i18n_nl.input.dayMonthYear.label, 'Dag maand jaar')
138 |
139 | monthDayYear = self.form_i18n_nl.input_components['monthDayYear']
140 | self.assertEqual(monthDayYear.month_name, 'Mei')
141 |
142 | dayMonthYear = self.form_i18n_nl.input_components['dayMonthYear']
143 | self.assertEqual(dayMonthYear.month_name, 'Mei')
144 |
145 | dayMonth = self.form_i18n_nl.input_components['dayMonth']
146 | self.assertEqual(dayMonth.month_name, 'Mei')
147 |
148 | monthYear = self.form_i18n_nl.input_components['monthYear']
149 | self.assertEqual(monthYear.month_name, 'Mei')
150 |
151 | month = self.form_i18n_nl.input_components['month']
152 | self.assertEqual(month.month_name, 'Mei')
153 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 2.1.6
4 |
5 | Fixes:
6 | - Fix for `selectComponent` with data source URL (`"dataSrc": "url"`), when no value is set.
7 | - Fix `get_component_by_path` method to properly handle components with no value set.
8 |
9 | ## 2.1.5
10 |
11 | Update to Nixpkgs 25.05 (development nix-shell), which includes poetry 2.1.3
12 |
13 | ## 2.1.4
14 |
15 | Fix `pyproject.toml`.
16 |
17 | ## 2.1.3
18 |
19 | Update `pyproject.toml`.
20 |
21 | # 2.1.2
22 | Add method `datetime_fromisoformat` in utils and refactor `datetimeComponent` to use it.
23 |
24 | ## 2.1.1
25 |
26 | Fix import error in `get_component_class` to log a warning instead of error.
27 |
28 | ## 2.1.0
29 |
30 | Fix `value_label` property (getter) in `selectComponent`, if no value is set.
31 |
32 | **WARNING - Backward incompatibility**\
33 | This also changes `value_label` return value from `False` to `None` if no value is set.\
34 | So check whether to update any usage (code).\
35 | However no need when `None` is used in comparisons (`if` etc), which converts to boolean `False`.
36 |
37 | ## 2.0.3
38 |
39 | Add `dataSrc` property (getter) in `selectboxesComponent`.
40 |
41 | ## 2.0.2
42 |
43 | Improve the `selectComponent` class to support the property Data Source Type (`dataSrc`) with URL.\
44 | This applies to the methods `value_label` and `value_labels`, which use the stored label(s).\
45 | So this doesn't execute a request to the URL, because we can't take any control (e.g. due to authentication) over the host/server/service,
46 |
47 | ## 2.0.1
48 |
49 | Improve the `Component` class `conditional_visible_when` method, to also obtain a Dictionary for the value of triggering component.\
50 | Remove the now redundant `conditional_visible_when` method from the `selectboxesComponent`, because a generic implementation is now in the `Component` class.
51 |
52 | ## 2.0.0
53 |
54 | Fix `datetimeComponent` which is always stored as ISO with combined date, time and timezone.\
55 | The previous implementation was wrong and now backwards imcompatible.\
56 |
57 | **Warning**:
58 |
59 | Test all `datetimeComponent` setters and their impact on implementations.
60 |
61 | ## 1.2.7
62 |
63 | Fix ValueError in `datetimeComponent` value setter exception handler:\
64 | `ValueError: : "fromisoformat: argument must be str" while evaluating`
65 |
66 | ## 1.2.6
67 |
68 | - Fix `datetimeComponent` value setter, to properly parse when the
69 | `enableTime` property is `False`.\
70 | This fixes a bug/regression in version 1.2.3.
71 | - Update README concerning the datetime component value.
72 |
73 | ## 1.2.5
74 |
75 | Improve the load methods for components and `gridRow`, by passing
76 | whether it applies to a Form `is_form`, otherwise it's obtained as a
77 | Builder.
78 |
79 | Ensure an empty form `gridRow` doesn't appear in a grid's `rows`
80 | property, made possible by the other `is_form` change.
81 |
82 | ## 1.2.4
83 |
84 | Implementation of "simple" validation required.
85 |
86 | For a Form object the validation errors can be retrieved by the new
87 | `validation_errors()` method.
88 |
89 | The new component method `validation_errors()` can be extended and
90 | returns either a dictionary or a list (for grid components) with the
91 | validation errors.
92 |
93 | ## 1.2.3
94 |
95 | Improve the `datetimeComponent` value setter, to properly parse a date
96 | with a custom format, when the `enableTime` (new property) is `False`.
97 |
98 | Provide the `component_class_mapping` (interface) in the keyword arguments of the Form (class) instantiation.
99 |
100 | ## 1.2.2
101 |
102 | Refactored the `Component` class `conditionally_visible` method, to
103 | call the following 2 methods which can be extended in component
104 | subclasses:
105 | - `conditional_visible_json_when`
106 | - `conditional_visible_json_logic`
107 |
108 | Implemented the `conditional_visible_json_when` method for the `selectboxesComponent`.\
109 | Extended the unittest `ConditionalVisibilitySimpleTestCase` with simple conditional visibility for the `selectboxesComponent`.
110 |
111 | ## 1.2.1
112 |
113 | Fix `get_component_object` (Builder) method to handle `ModuleNotFoundError`.\
114 | Therefor implemented the `get_component_class` method to determine the class with a fallback to the base `Component` class.
115 |
116 | ## 1.2.0
117 |
118 | New "component class mapping feature" for the Builder instantiation:\
119 | Map a custom component type to an implemented component class, which is then loaded.
120 |
121 | An example is available in the unittests of file: `tests/test_component_class_mapping.py`
122 |
123 | Also refactored the Builder constructor, from some `kwargs` to keyword arguments.
124 |
125 | ## 1.1.0
126 |
127 | Put component classes as files in the new `components` directory.\
128 | Change the instantiation of a component in the `get_component_object` method of the `Builder` class.
129 |
130 | **Warning**:
131 |
132 | This changes the `import` declaration (path) of how components should be imported.
133 |
134 | **Old style import:**:
135 |
136 | ```python
137 | from formiodata.components import textfieldComponent
138 | ```
139 |
140 | **New style import:**
141 |
142 | ```python
143 | from formiodata.components.textfield import textfieldComponent
144 | ```
145 |
146 | Also some additional minor improvements, e.g. remove unused imports and newlines.
147 |
148 | ## 1.0.5
149 |
150 | Add Component properties:
151 | - `tableView`: Display // Table View
152 | - `disabled`: Display // Disabled
153 |
154 | ## 1.0.4
155 |
156 | Add Component properties:
157 | - `conditional`: Conditional // Simple Conditional
158 | - `custom_conditional`: Conditional // Custom Conditional
159 | - `templates`: Templates (eg templates for layout and (data) grids.)
160 | - `logic`: Logic (trigger/action pairs).
161 |
162 | ## 1.0.3
163 |
164 | Add the country_code property in the addressComponent.
165 |
166 | ## 1.0.2
167 |
168 | Refactor builder component path properties, to store objects, with key and label getters.
169 |
170 | ## 1.0.1
171 |
172 | Fix error in `get_component_object` (`builder.py`) => `NameError: name 'logging is not defined'`\
173 | Accidentally removed the `import logging` in previous version 1.0.0
174 |
175 | ## 1.0.0
176 |
177 | Implement builder component path properties (keys, labels).
178 |
179 | `builder_path_key`
180 | List of complete path components with keys. This includes layout components.
181 |
182 | `builder_path_label`
183 | List of complete path components with labels. This includes layout components.
184 |
185 | `builder_input_path_key`
186 | List of input components in path with keys. This only includes input components, so no layout components.
187 |
188 | `builder_input_path_label`
189 | List of input components in path with labels. This only includes input components, so no layout components.
190 |
191 | Also propagate this as first official release 1.0.0
192 |
193 | ## 0.5.1
194 |
195 | Fix `initEmpty` in `editgridComponent`, bug in previous version 0.5.0
196 |
197 | ## 0.5.0
198 |
199 | Implement `initEmpty` in `editgridComponent`, which obtains a different key (`openWhenEmpty`) in the form builder schema.
200 |
201 | ## 0.4.11
202 |
203 | Improvements:
204 | - Add support for editGrid component (#33).
205 | - Breaking change: in a dataGrid, renamed `gridRow` object's `datagrid` property to `grid`.
206 |
--------------------------------------------------------------------------------
/tests/test_component_file_storage_url.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | from test_component import ComponentTestCase
5 | from formiodata.components.file import fileComponent
6 |
7 |
8 | class fileComponentStorageUrlTestCase(ComponentTestCase):
9 |
10 | def setUp(self):
11 | super(fileComponentStorageUrlTestCase, self).setUp()
12 |
13 | self.image_url = 'https://avatars1.githubusercontent.com/u/31220867?s=50'
14 | self.image_value_base64 = 'data:image/png;base64,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'
15 |
16 | def test_object(self):
17 | uploadUrl = self.builder.input_components['uploadUrl']
18 | self.assertIsInstance(uploadUrl, fileComponent)
19 |
20 | # Not fileComponent
21 | email = self.builder.input_components['email']
22 | self.assertNotIsInstance(email, fileComponent)
23 |
24 | def test_get_key(self):
25 | uploadUrl = self.builder.input_components['uploadUrl']
26 | self.assertEqual(uploadUrl.key, 'uploadUrl')
27 |
28 | def test_get_type(self):
29 | uploadUrl = self.builder.input_components['uploadUrl']
30 | self.assertEqual(uploadUrl.type, 'file')
31 |
32 | def test_get_label(self):
33 | uploadUrl = self.builder.input_components['uploadUrl']
34 | self.assertEqual(uploadUrl.label, 'Upload Url')
35 |
36 | def test_set_label(self):
37 | uploadUrl = self.builder.input_components['uploadUrl']
38 | self.assertEqual(uploadUrl.label, 'Upload Url')
39 | uploadUrl.label = 'RAW Image'
40 | self.assertEqual(uploadUrl.label, 'RAW Image')
41 |
42 | def test_get_form(self):
43 | uploadUrl = self.form.components['uploadUrl']
44 | self.assertEqual(uploadUrl.label, 'Upload Url')
45 | self.assertEqual(uploadUrl.type, 'file')
46 | self.assertEqual(uploadUrl.storage, 'url')
47 | self.assertUrlBase64(uploadUrl, self.image_value_base64)
48 |
49 |
50 | def test_get_form_data(self):
51 | uploadUrl = self.form.input.uploadUrl
52 | self.assertEqual(uploadUrl.label, 'Upload Url')
53 | self.assertEqual(uploadUrl.type, 'file')
54 | self.assertEqual(uploadUrl.storage, 'url')
55 | self.assertUrlBase64(uploadUrl, self.image_value_base64)
56 |
57 | # i18n translations
58 | def test_get_label_i18n_nl(self):
59 | uploadUrl = self.builder_i18n_nl.input_components['uploadUrl']
60 | self.assertEqual(uploadUrl.label, 'Upload naar locatie')
61 |
62 | def test_get_form_data_i18n_nl(self):
63 | self.assertEqual(self.form_i18n_nl.input.uploadUrl.label, 'Upload naar locatie')
64 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # formio-data (Python)
2 |
3 | formio.js (JSON Form Builder) data API for Python.
4 |
5 | For information about the formio.js project, see https://github.com/formio/formio.js
6 |
7 | ## Introduction
8 |
9 | **python-formio-data** is a Python package, which loads and transforms
10 | formio.js **Builder JSON** and **Form JSON** into **usable Python objects**.\
11 | It's main aim is to provide easy access to a Form its components/fields, also
12 | captured as **Python objects**, which makes this API very versatile and usable.
13 |
14 | **Notes about terms:**
15 | - **Builder:** The Form Builder which is the design/blueprint of a Form.
16 | - **Form:** A filled-in Form, aka Form submission.
17 | - **Component:** Input (field) or layout component in the Form Builder and Form.
18 |
19 | ## Features
20 |
21 | - Compatible with Python 3.6 and later
22 | - Constructor of the **Builder** and **Form** class, only requires
23 | the JSON and an optional language code for translations.
24 | - Get a Form object its Components as a usable object e.g. datetime, boolean, dict (for select component) etc.
25 | - Open source (MIT License)
26 |
27 | ## Installation
28 |
29 | The source code is currently hosted on GitHub at:
30 | https://github.com/novacode-nl/python-formio-data
31 |
32 | ### PyPI - Python Package Index
33 |
34 | Binary installers for the latest released version are available at the [Python
35 | Package Index](https://pypi.python.org/pypi/formio-data)
36 |
37 | ```sh
38 | pip(3) install formio-data
39 | ```
40 |
41 | #### Optional dependencies
42 |
43 | To support conditional visibility using JSON logic, you can install
44 | the `json-logic-qubit` package (the `json-logic` package it is forked
45 | off of is currently unmaintained). It's also possible to install it
46 | via the pip feature `json_logic` like so:
47 |
48 | ```sh
49 | pip(3) install -U formio-data[json_logic]
50 | ```
51 |
52 | ### Source Install with Poetry (recommended)
53 |
54 | Convenient for developers. Also useful for running the (unit)tests.
55 |
56 | ```sh
57 | git clone git@github.com:novacode-nl/python-formio-data.git
58 | poetry install
59 | ```
60 |
61 | #### Optional dependencies
62 |
63 | When working in the project itself, use
64 |
65 | ```sh
66 | poetry install -E json_logic
67 | ```
68 |
69 | ### Source Install with pip
70 |
71 | Optional dependencies need to be installed separately.
72 |
73 | ```sh
74 | pip(3) install -U -e python-formio-data
75 | ```
76 |
77 | ## Using direnv
78 |
79 | You can use [nixpkgs](https://nixos.org/) to run a self-contained
80 | Python environment without any additional setup. Once you've
81 | installed nixpkgs, switch into the directory and type "nix-shell" to
82 | get a shell from which the correct Python with packages is available.
83 |
84 | If you're using [direnv](https://direnv.net/), use `direnv allow`
85 | after changing into the project directory and you're good to go. Also
86 | consider [nix-direnv](https://github.com/nix-community/nix-direnv) to
87 | speed up the experience (it can re-use a cached local installation).
88 |
89 | ## License
90 | [MIT](LICENSE)
91 |
92 | ## Contributing
93 | All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome.
94 |
95 | ## Usage examples
96 |
97 | For more examples of usage, see the unit-tests.
98 |
99 | ``` python
100 | >> from formiodata import Builder, Form
101 | >>
102 | # builder_json is a formio.js Builder JSON document (text/string)
103 | # form_json is a formio.js Form JSON document (text/string)
104 | >>
105 | >> builder = Builder(builder_json)
106 | >> form = Form(builder, form_json)
107 |
108 | ##################
109 | # input components
110 | ##################
111 |
112 | # textfield label
113 | >> print(form.input_components['firstname'].label)
114 | 'First Name'
115 |
116 | # textfield value
117 | >> print(form.input_components['firstname'].value)
118 | 'Bob'
119 |
120 | # datetime label
121 | >> print(form.input_components['birthday'].label)
122 | 'Birthday'
123 |
124 | # datetime value
125 | >> print(form.input_components['birthday'].value)
126 | '2009-10-16'
127 |
128 | >> print(form.input_components['birthday'].to_date())
129 | datetime.date(2009 10 16)
130 |
131 | # datagrid (rows property)
132 | >> print(form.input_components['datagridMeasurements'].rows)
133 | [
134 | {'measurementDatetime': , 'measurementFahrenheit': },
135 | {'measurementDatetime': , 'measurementFahrenheit': }
136 | ]
137 |
138 | >> for row in form.input_components['datagridMeasurements'].rows:
139 | >> dtime = row['measurementDatetime']
140 | >> fahrenheit = row['measurementFahrenheit']
141 | >> print(%s: %s, %s: %s' % (dt.label, dt.to_datetime(), fahrenheit.label, fahrenheit.value))
142 |
143 | Datetime: datetime.datetime(2021, 5, 8, 11, 39, 0, 296487), Fahrenheit: 122
144 | Datetime: datetime.datetime(2021, 5, 8, 11, 41, 5, 919943), Fahrenheit: 131
145 |
146 | # alternative example, by getattr
147 | >> print(form.data.firstname.label)
148 | 'First Name'
149 |
150 | >> print(form.data.firstname.value)
151 | 'Bob'
152 |
153 | ###################
154 | # validation errors
155 | ###################
156 |
157 | >> print(form.validation_errors())
158 | {
159 | 'companyName': 'Company Name is required',
160 | 'editgridActivities': [
161 | {'description': 'Description is required'},
162 | {}, # no validation error (row 2)
163 | {}, # no validation error (row 3)
164 | {'description': 'Description is required', 'startDate': 'Start Date is required'}
165 | ]
166 | }
167 |
168 | #############################
169 | # component path (properties)
170 | #############################
171 |
172 | # datagrid input
173 | >> datagridMeasurements = builder.components['datagridMeasurements']
174 |
175 | # builder_path
176 | >> [
177 | >> print(row.input_components['measurementDatetime'].builder_path)
178 | >> for row in datagridMeasurements.rows
179 | >> ]
180 | [, , , ]
181 |
182 | # builder_path_key
183 | >> [
184 | >> print(row.input_components['measurementDatetime'].builder_path_key)
185 | >> for row in datagridMeasurements.rows
186 | >> ]
187 | ['pageMeasurements', 'columnsExternal', 'datagridMeasurements', 'measurementDatetime']
188 |
189 | # builder_path_labels
190 | >> [
191 | >> print(row.input_components['measurementDatetime'].builder_path_labels)
192 | >> for row in datagridMeasurements.rows
193 | >> ]
194 | ['Page Measurements', 'Columns External', 'Data Grid Measurements', 'Measurement Datetime']
195 |
196 | # builder_input_path
197 | >> [
198 | >> print(row.input_components['measurementDatetime'].builder_input_path)
199 | >> for row in datagridMeasurements.rows
200 | >> ]
201 | [, ]
202 |
203 | # builder_input_path_key
204 | >> [
205 | >> print(row.input_components['measurementDatetime'].builder_input_path_key)
206 | >> for row in datagridMeasurements.rows
207 | >> ]
208 | ['datagridMeasurements', 'measurementDatetime']
209 |
210 | # builder_input_path_labels
211 | >> [
212 | >> print(row.input_components['measurementDatetime'].builder_input_path_labels)
213 | >> for row in datagridMeasurements.rows
214 | >> ]
215 | ['Data Grid Measurements', 'Measurement Datetime']
216 |
217 | #################################
218 | # components (layout, input etc.)
219 | #################################
220 |
221 | # columns
222 | >> print(form.components['addressColumns'])
223 |
224 |
225 | >> print(form.components['addressColumns'].rows)
226 | [
227 | {'firstName': , 'lastName: },
228 | {'email': , 'companyName: }
229 | ]
230 |
231 | ##########################
232 | # components class mapping
233 | ##########################
234 |
235 | # Below an example which verbosely shows the feature:
236 | # - First set a custom component type 'custom_editgrid' in the Builder JSON schema.
237 | # - Check (assert) whether the component object is an instance of the mapped editgridComponent.
238 | # This code is also present in the unittest (file): tests/test_component_class_mapping.py
239 |
240 | schema_dict = json.loads(self.builder_json)
241 |
242 | # change 'editgrid' type to 'custom_editgrid'
243 | for comp in schema_dict['components']:
244 | if comp['key'] == 'editGrid':
245 | comp['type'] = 'custom_editgrid'
246 |
247 | component_class_mapping = {'custom_editgrid': editgridComponent}
248 | builder = Builder(
249 | schema_json,
250 | component_class_mapping=component_class_mapping,
251 | )
252 |
253 | custom_editgrid = builder.components['editGrid']
254 | self.assertIsInstance(custom_editgrid, editgridComponent)
255 | self.assertEqual(custom_editgrid.type, 'custom_editgrid')
256 | ```
257 |
258 | ## Unit tests
259 |
260 | **Note:**
261 |
262 | Internet access is recommended for running the `fileStorageUrlComponentTestCase`, because this also tests the URL Storage (type).\
263 | If no internet access, this test won't fail and a WARNING shall be logged regarding a ConnectionError.
264 |
265 | ### Run all unittests
266 |
267 | From toplevel directory:
268 |
269 | ```
270 | poetry install -E json_logic # if you haven't already
271 | poetry run python -m unittest
272 | ```
273 |
274 | ### Run component unittests
275 |
276 | All Components, from toplevel directory:
277 |
278 | ```
279 | poetry run python -m unittest tests/test_component_*.py
280 | ```
281 |
282 | Nested components (complexity), from toplevel directory:
283 |
284 | ```
285 | poetry run python -m unittest tests/test_nested_components.py
286 | ```
287 |
288 | ### Run specific component unittest
289 |
290 | ```
291 | poetry run python -m unittest tests.test_component_day.dayComponentTestCase.test_get_form_dayMonthYear
292 | ```
293 |
--------------------------------------------------------------------------------
/tests/data/test_conditional_visibility_json_logic_builder.json:
--------------------------------------------------------------------------------
1 | {
2 | "components": [
3 | {
4 | "defaultValue": "",
5 | "id": "edr3y4q",
6 | "inputType": "text",
7 | "dataGridLabel": false,
8 | "refreshOn": "",
9 | "input": true,
10 | "type": "textfield",
11 | "overlay": {
12 | "height": "",
13 | "width": "",
14 | "top": "",
15 | "left": "",
16 | "page": "",
17 | "style": ""
18 | },
19 | "attributes": {
20 | },
21 | "logic": [],
22 | "customConditional": "",
23 | "conditional": {
24 | "json": "",
25 | "eq": "",
26 | "when": null,
27 | "show": null
28 | },
29 | "properties": {
30 | },
31 | "tags": [],
32 | "key": "username",
33 | "errorLabel": "",
34 | "unique": false,
35 | "validate": {
36 | "unique": false,
37 | "multiple": false,
38 | "strictDateValidation": false,
39 | "maxLength": "",
40 | "minLength": "",
41 | "json": "",
42 | "customPrivate": false,
43 | "custom": "",
44 | "customMessage": "",
45 | "pattern": "",
46 | "required": false
47 | },
48 | "validateOn": "change",
49 | "allowCalculateOverride": false,
50 | "calculateServer": false,
51 | "calculateValue": "",
52 | "customDefaultValue": "",
53 | "clearOnHide": true,
54 | "redrawOn": "",
55 | "encrypted": false,
56 | "case": "",
57 | "dbIndex": false,
58 | "protected": false,
59 | "inputFormat": "plain",
60 | "persistent": true,
61 | "multiple": false,
62 | "modalEdit": false,
63 | "tableView": true,
64 | "disabled": false,
65 | "spellcheck": true,
66 | "autofocus": false,
67 | "mask": false,
68 | "showCharCount": false,
69 | "showWordCount": false,
70 | "hideLabel": false,
71 | "hidden": false,
72 | "autocomplete": "",
73 | "tabindex": "",
74 | "customClass": "",
75 | "allowMultipleMasks": false,
76 | "inputMask": "",
77 | "widget": {
78 | "type": "input"
79 | },
80 | "suffix": "",
81 | "prefix": "",
82 | "tooltip": "",
83 | "description": "",
84 | "placeholder": "",
85 | "labelPosition": "top",
86 | "label": "Username"
87 | },
88 | {
89 | "id": "essijy",
90 | "inputMask": "",
91 | "inputFormat": "plain",
92 | "inputType": "text",
93 | "allowMultipleMasks": false,
94 | "encrypted": false,
95 | "calculateValue": "",
96 | "customDefaultValue": "",
97 | "dbIndex": false,
98 | "dataGridLabel": false,
99 | "refreshOn": "",
100 | "persistent": true,
101 | "unique": false,
102 | "protected": true,
103 | "defaultValue": null,
104 | "multiple": false,
105 | "input": true,
106 | "type": "password",
107 | "overlay": {
108 | "height": "",
109 | "width": "",
110 | "top": "",
111 | "left": "",
112 | "page": "",
113 | "style": ""
114 | },
115 | "attributes": {
116 | },
117 | "logic": [],
118 | "customConditional": "",
119 | "conditional": {
120 | "json": "",
121 | "eq": "",
122 | "when": null,
123 | "show": null
124 | },
125 | "properties": {
126 | },
127 | "tags": [],
128 | "key": "password",
129 | "errorLabel": "",
130 | "validate": {
131 | "unique": false,
132 | "multiple": false,
133 | "strictDateValidation": false,
134 | "maxLength": "",
135 | "minLength": "",
136 | "json": "",
137 | "customPrivate": false,
138 | "custom": "",
139 | "customMessage": "",
140 | "pattern": "",
141 | "required": false
142 | },
143 | "validateOn": "change",
144 | "allowCalculateOverride": false,
145 | "calculateServer": false,
146 | "clearOnHide": true,
147 | "redrawOn": "",
148 | "case": "",
149 | "modalEdit": false,
150 | "tableView": false,
151 | "disabled": false,
152 | "spellcheck": true,
153 | "autofocus": false,
154 | "mask": false,
155 | "showCharCount": false,
156 | "showWordCount": false,
157 | "hideLabel": false,
158 | "hidden": false,
159 | "autocomplete": "",
160 | "tabindex": "",
161 | "customClass": "",
162 | "widget": {
163 | "type": "input"
164 | },
165 | "suffix": "",
166 | "prefix": "",
167 | "tooltip": "",
168 | "description": "",
169 | "placeholder": "",
170 | "labelPosition": "top",
171 | "label": "Password"
172 | },
173 | {
174 | "defaultValue": "Secret message",
175 | "id": "eyx9rms",
176 | "inputType": "text",
177 | "dataGridLabel": false,
178 | "refreshOn": "",
179 | "input": true,
180 | "type": "textfield",
181 | "overlay": {
182 | "height": "",
183 | "width": "",
184 | "top": "",
185 | "left": "",
186 | "page": "",
187 | "style": ""
188 | },
189 | "attributes": {
190 | },
191 | "logic": [],
192 | "customConditional": "",
193 | "conditional": {
194 | "json": {
195 | "and": [
196 | {
197 | "===": [
198 | {
199 | "var": "data.username"
200 | },
201 | "user"
202 | ]
203 | },
204 | {
205 | "===": [
206 | {
207 | "var": "data.password"
208 | },
209 | "secret"
210 | ]
211 | }
212 | ]
213 | },
214 | "eq": "",
215 | "when": null,
216 | "show": null
217 | },
218 | "properties": {
219 | },
220 | "tags": [],
221 | "key": "secret",
222 | "errorLabel": "",
223 | "unique": false,
224 | "validate": {
225 | "unique": false,
226 | "multiple": false,
227 | "strictDateValidation": false,
228 | "maxLength": "",
229 | "minLength": "",
230 | "json": "",
231 | "customPrivate": false,
232 | "custom": "",
233 | "customMessage": "",
234 | "pattern": "",
235 | "required": false
236 | },
237 | "validateOn": "change",
238 | "allowCalculateOverride": false,
239 | "calculateServer": false,
240 | "calculateValue": "",
241 | "customDefaultValue": "",
242 | "clearOnHide": true,
243 | "redrawOn": "",
244 | "encrypted": false,
245 | "case": "",
246 | "dbIndex": false,
247 | "protected": false,
248 | "inputFormat": "plain",
249 | "persistent": true,
250 | "multiple": false,
251 | "modalEdit": false,
252 | "tableView": true,
253 | "disabled": false,
254 | "spellcheck": true,
255 | "autofocus": false,
256 | "mask": false,
257 | "showCharCount": false,
258 | "showWordCount": false,
259 | "hideLabel": false,
260 | "hidden": false,
261 | "autocomplete": "",
262 | "tabindex": "",
263 | "customClass": "",
264 | "allowMultipleMasks": false,
265 | "inputMask": "",
266 | "widget": {
267 | "type": "input"
268 | },
269 | "suffix": "",
270 | "prefix": "",
271 | "tooltip": "",
272 | "description": "",
273 | "placeholder": "",
274 | "labelPosition": "top",
275 | "label": "Secret"
276 | },
277 | {
278 | "id": "eo7g67",
279 | "rightIcon": "",
280 | "leftIcon": "",
281 | "allowMultipleMasks": false,
282 | "properties": {
283 | },
284 | "showWordCount": false,
285 | "showCharCount": false,
286 | "encrypted": false,
287 | "allowCalculateOverride": false,
288 | "overlay": {
289 | "height": "",
290 | "width": "",
291 | "top": "",
292 | "left": "",
293 | "style": ""
294 | },
295 | "conditional": {
296 | "eq": "",
297 | "when": null,
298 | "show": null
299 | },
300 | "validate": {
301 | "unique": false,
302 | "multiple": false,
303 | "strictDateValidation": false,
304 | "customPrivate": false,
305 | "custom": "",
306 | "required": false
307 | },
308 | "validateOn": "change",
309 | "attributes": {
310 | },
311 | "widget": {
312 | "type": "input"
313 | },
314 | "calculateServer": false,
315 | "calculateValue": "",
316 | "customDefaultValue": "",
317 | "dbIndex": false,
318 | "autofocus": false,
319 | "disabled": false,
320 | "tabindex": "",
321 | "hideLabel": false,
322 | "tooltip": "",
323 | "errorLabel": "",
324 | "description": "",
325 | "labelPosition": "top",
326 | "dataGridLabel": true,
327 | "modalEdit": false,
328 | "tableView": false,
329 | "redrawOn": "",
330 | "refreshOn": "",
331 | "clearOnHide": true,
332 | "hidden": false,
333 | "persistent": false,
334 | "unique": false,
335 | "protected": false,
336 | "defaultValue": null,
337 | "multiple": false,
338 | "suffix": "",
339 | "customClass": "",
340 | "prefix": "",
341 | "placeholder": "",
342 | "input": true,
343 | "theme": "primary",
344 | "disableOnInvalid": true,
345 | "action": "submit",
346 | "block": false,
347 | "size": "md",
348 | "key": "submit",
349 | "label": "Submit",
350 | "type": "button"
351 | }
352 | ]
353 | }
354 |
--------------------------------------------------------------------------------
/formiodata/components/component.py:
--------------------------------------------------------------------------------
1 | # Copyright Nova Code (http://www.novacode.nl)
2 | # See LICENSE file for full licensing details.
3 |
4 | import json
5 | import logging
6 | import uuid
7 |
8 | from collections import OrderedDict
9 |
10 | logger = logging.getLogger(__name__)
11 |
12 |
13 | class Component:
14 |
15 | _none_value = None
16 |
17 | def __init__(self, raw, builder, **kwargs):
18 | self.raw = raw
19 | self.builder = builder
20 |
21 | self._parent = None
22 | self._component_owner = None
23 | # components can also be seen as children
24 | self.components = OrderedDict()
25 |
26 | # List of complete path components. This includes layout
27 | # components.
28 | self.builder_path = []
29 | # includes input components, so no layout components.
30 | self.builder_input_path = []
31 |
32 | # XXX uuid to ensure (hope this won't break anything)
33 | self.id = self.raw.get('id', str(uuid.uuid4()))
34 |
35 | # submission at this level {key: value, ...}
36 | # This includes a pseudo 'value' key which always encodes the current
37 | # component's value. NOTE: This should be refactored away for conditionals
38 | # to work 100% correct when there's another element with a "value" key.
39 | self.form = {}
40 | # Full raw data from the root on up {key: value, ...}
41 | self._all_data = {}
42 |
43 | # i18n (language, translations)
44 | self.language = kwargs.get('language', 'en')
45 | self.i18n = kwargs.get('i18n', {})
46 | self.resources = kwargs.get('resources', {})
47 | if self.resources and isinstance(self.resources, str):
48 | self.resources = json.loads(self.resources)
49 | self.html_component = ""
50 | self.defaultValue = self.raw.get('defaultValue')
51 |
52 | def load(self, component_owner, parent=None, data=None, all_data=None, is_form=False):
53 | self.component_owner = component_owner
54 |
55 | if parent:
56 | self.parent = parent
57 |
58 | self._all_data = all_data
59 | self.load_data(data, is_form=is_form)
60 |
61 | self.builder.component_ids[self.id] = self
62 |
63 | # path
64 | self.set_builder_paths()
65 | builder_path_keys = [p.key for p in self.builder_path]
66 | builder_path_key = '.'.join(builder_path_keys)
67 | self.builder.components_path_key[builder_path_key] = self
68 |
69 | def load_data(self, data, is_form=False):
70 | if self.input and data:
71 | try:
72 | self.value = data[self.key]
73 | self.raw_value = data[self.key]
74 | except KeyError:
75 | # NOTE: getter will read out defaultValue if it's missing in self.form
76 | # TODO: Is this the right approach?
77 | pass
78 |
79 | @property
80 | def id(self):
81 | return self._id
82 |
83 | @id.setter
84 | def id(self, id=False):
85 | if not id:
86 | id = str(uuid.uuid4())
87 | self._id = id
88 |
89 | @property
90 | def key(self):
91 | return self.raw.get('key')
92 |
93 | @property
94 | def type(self):
95 | return self.raw.get('type')
96 |
97 | @property
98 | def input(self):
99 | return self.raw.get('input')
100 |
101 | @property
102 | def parent(self):
103 | return self._parent
104 |
105 | @parent.setter
106 | def parent(self, parent):
107 | if parent:
108 | self._parent = parent
109 | self._parent.components[self.key] = self
110 |
111 | @property
112 | def is_form_component(self):
113 | return bool(self.input)
114 |
115 | @property
116 | def component_owner(self):
117 | """The component's "owner". This is usually the Builder class which
118 | created it. But if this component is inside a datagrid
119 | component which may clone the form element, then the datagrid
120 | is the owner. Each component adds itself to the `input_components`
121 | property its owner.
122 | """
123 | return self._component_owner
124 |
125 | @component_owner.setter
126 | def component_owner(self, component_owner):
127 | self._component_owner = component_owner
128 | if self.is_form_component:
129 | self._component_owner.input_components[self.key] = self
130 |
131 | @property
132 | def child_component_owner(self):
133 | """The owner object for child components, to use in the recursion"""
134 | return self.component_owner
135 |
136 | @property
137 | def builder_path_key(self):
138 | return [p.key for p in self.builder_path]
139 |
140 | @property
141 | def builder_path_label(self):
142 | return [p.label for p in self.builder_path]
143 |
144 | @property
145 | def builder_input_path_key(self):
146 | return [p.key for p in self.builder_input_path]
147 |
148 | @property
149 | def builder_input_path_label(self):
150 | return [p.label for p in self.builder_input_path]
151 |
152 | def set_builder_paths(self):
153 | builder_path = [self]
154 | builder_input_path = []
155 | if self.is_form_component:
156 | if self.builder.load_path_objects:
157 | builder_input_path.append(self)
158 | parent = self.parent
159 | while parent:
160 | if hasattr(parent, 'key'):
161 | if self.builder.load_path_objects:
162 | builder_path.append(parent)
163 | if parent.is_form_component:
164 | if self.builder.load_path_objects:
165 | builder_input_path.append(parent)
166 | parent = parent.parent
167 | elif parent.__class__.__name__ == 'gridRow':
168 | parent = parent.grid
169 | else:
170 | parent = parent.component_owner
171 | builder_path.reverse()
172 | self.builder_path = builder_path
173 | # input path
174 | builder_input_path.reverse()
175 | self.builder_input_path = builder_input_path
176 |
177 | @property
178 | def validate(self):
179 | return self.raw.get('validate')
180 |
181 | @property
182 | def required(self):
183 | return self.raw.get('validate', {}).get('required')
184 |
185 | @property
186 | def properties(self):
187 | return self.raw.get('properties')
188 |
189 | @property
190 | def clearOnHide(self):
191 | if 'clearOnHide' in self.raw:
192 | return self.raw.get('clearOnHide')
193 | else:
194 | return None
195 |
196 | @property
197 | def label(self):
198 | label = self.raw.get('label')
199 | if self.i18n.get(self.language):
200 | return self.i18n[self.language].get(label, label)
201 | else:
202 | return label
203 |
204 | @label.setter
205 | def label(self, value):
206 | if self.raw.get('label'):
207 | self.raw['label'] = value
208 |
209 | @property
210 | def value(self):
211 | if self.clearOnHide is not None and not self.clearOnHide:
212 | default = self.defaultValue
213 | else:
214 | default = self._none_value
215 | return self.form.get('value', default)
216 |
217 | @value.setter
218 | def value(self, value):
219 | self.set_value(value)
220 |
221 | def _set_value(self, value):
222 | self.form['value'] = self._encode_value(value)
223 |
224 | @property
225 | def raw_value(self):
226 | return self.form['raw_value']
227 |
228 | @raw_value.setter
229 | def raw_value(self, value):
230 | self._set_raw_value(value)
231 |
232 | def _set_raw_value(self, value):
233 | self.form['raw_value'] = value
234 |
235 | def set_value(self, value):
236 | """ Set raw_value and value at once! """
237 | self._set_raw_value(value)
238 | self._set_value(value)
239 |
240 | @property
241 | def hidden(self):
242 | return self.raw.get('hidden')
243 |
244 | @property
245 | def tableView(self):
246 | return self.raw.get('tableView')
247 |
248 | @property
249 | def disabled(self):
250 | return self.raw.get('disabled')
251 |
252 | @property
253 | def conditional(self):
254 | return self.raw.get('conditional')
255 |
256 | @property
257 | def customConditional(self):
258 | return self.raw.get('customConditional')
259 |
260 | @property
261 | def templates(self):
262 | return self.raw.get('templates')
263 |
264 | @property
265 | def logic(self):
266 | return self.raw.get('logic')
267 |
268 | def _encode_value(self, value):
269 | return value
270 |
271 | def render(self):
272 | if self.value is not None:
273 | self.html_component = '%s
' % self.value
274 |
275 | @property
276 | def conditionally_visible(self):
277 | """
278 | If conditional visibility applies, evaluate to see if it is visible.
279 | Note that the component can also be hidden, which is a separate concept.
280 |
281 | IMPORTANT
282 | =========
283 | Currently JSONLogic (json) precedes the Simple (when).
284 | This causes backward compatibility issues when changing the priority order.
285 | """
286 | try:
287 | cond = self.raw['conditional']
288 | if cond.get('json'):
289 | return self.conditional_visible_json_logic()
290 | elif cond.get('when'):
291 | return self.conditional_visible_when()
292 | except KeyError:
293 | # Unknown component or no 'when', 'eq' or 'show' property
294 | pass
295 |
296 | # By default, it's visible
297 | return True
298 |
299 | def conditional_visible_when(self):
300 | cond = self.raw['conditional']
301 | triggering_component = self.component_owner.input_components[cond['when']]
302 | triggering_value = cond['eq']
303 | if isinstance(
304 | triggering_component.value, dict
305 | ) and triggering_component.value.get(triggering_value):
306 | # E.g. triggering_component like selectboxesComponent
307 | return cond["show"]
308 | elif triggering_component.value == triggering_value:
309 | return cond['show']
310 | else:
311 | return not cond['show']
312 |
313 | def conditional_visible_json_logic(self):
314 | # Optional package
315 | try:
316 | from json_logic import jsonLogic
317 | context = {'data': self._all_data}
318 | try:
319 | context['row'] = self.component_owner.row
320 | except AttributeError:
321 | pass # only datagrid rows have a "row" attribute
322 | cond = self.raw['conditional']
323 | return jsonLogic(cond['json'], context)
324 | except ImportError:
325 | logger.warning(f'Could not load json logic extension; will not evaluate visibility of {self.__class__.__name__} {self.id} ("{self.key}")')
326 | return True
327 |
328 | @property
329 | def is_visible(self):
330 | conditional = self.raw.get('conditional')
331 | if conditional and (conditional.get('json') or conditional.get('when')):
332 | # Not implement (JavaScript):
333 | # conditional_show = self.raw.get('show')
334 | return self.conditionally_visible
335 | else:
336 | return not self.hidden
337 |
338 | def validation_errors(self):
339 | errors = {}
340 | if self.required and not self.value:
341 | msg_tmpl = '{{field}} is required'
342 | if self.i18n.get(self.language):
343 | msg_tmpl = self.i18n[self.language].get(msg_tmpl, msg_tmpl)
344 | errors['required'] = msg_tmpl.replace('{{field}}', self.label)
345 | return errors
346 |
--------------------------------------------------------------------------------
/tests/data/test_example_form_check_default.json:
--------------------------------------------------------------------------------
1 | {
2 | "firstName": "Bob",
3 | "signature": "data:image/png;base64,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",
4 | "phoneNumber": "(069) 999-9999",
5 | "survey": {
6 | "overallExperience": "excellent",
7 | "howWasCustomerSupport": "great",
8 | "howWouldYouRateTheFormIoPlatform": "excellent"
9 | },
10 | "lastName": "Leers",
11 | "favouriteSeason": "autumn",
12 | "favouriteFood": ["mexican", "chinese"],
13 | "cardinalDirection": "south",
14 | "dataGrid": [{"textField": "abc", "checkbox": true}, {"textField": "def", "checkbox": false}],
15 | "uploadBase64": "data:image/gif;base64,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",
16 | "uploadUrl": [{"storage": "url", "name": "31220867-68a92e01-294d-43ae-91b5-5b91f01d9aa1.png", "url": "https://avatars1.githubusercontent.com/u/31220867?s=50&baseUrl=https%3A%2F%2Fapi.form.io&project=&form=/31220867-68a92e01-294d-43ae-91b5-5b91f01d9aa1.png", "size": 847807, "type": "image/png", "data": {"id": 1212, "name": "31220867-68a92e01-294d-43ae-91b5-5b91f01d9aa1.png", "mimetype": "image/png", "file_size": 847807, "access_token": false, "baseUrl": "https://api.form.io", "project": "", "form": ""}, "originalName": "31220867.png"}],
17 | "submit": true
18 | }
19 |
--------------------------------------------------------------------------------