surveyjs 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
surveyjs/__init__.py ADDED
@@ -0,0 +1,21 @@
1
+ # Copyright 2026 Nova Code (https://www.novacode.nl)
2
+ """
3
+ SurveyJS Python Package
4
+
5
+ This module serves as the entry point for the surveyjs package, providing
6
+ access to the core components for working with SurveyJS forms and the
7
+ survey creator.
8
+
9
+ It imports and exposes the following classes:
10
+ - SurveyForm: Handles the rendering and processing of SurveyJS forms.
11
+ - SurveyCreator: Provides functionality for creating and managing surveys.
12
+
13
+ Copyright 2026 Nova Code (https://www.novacode.nl)
14
+ See LICENSE file for full licensing details.
15
+ """
16
+ # See LICENSE file for full licensing details.
17
+
18
+ from surveyjs.creator import SurveyCreator
19
+ from surveyjs.form import SurveyForm
20
+
21
+ __all__ = ['SurveyCreator', 'SurveyForm']
surveyjs/creator.py ADDED
@@ -0,0 +1,165 @@
1
+ # Copyright 2026 Nova Code (https://www.novacode.nl)
2
+ # See LICENSE file for full licensing details.
3
+
4
+ import json
5
+ import logging
6
+ from collections import OrderedDict
7
+ from copy import deepcopy
8
+
9
+ from surveyjs.questions.question import Question
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class SurveyCreator:
15
+ """
16
+ Represents a SurveyJS Creator schema (the survey blueprint/design).
17
+
18
+ Parses the survey JSON schema and creates Question objects for each
19
+ question element found in the schema.
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ schema_json,
25
+ language='en',
26
+ i18n=None,
27
+ question_class_mapping={},
28
+ **kwargs
29
+ ):
30
+ """
31
+ @param schema_json: SurveyJS Creator JSON schema (str or dict)
32
+ @param language: Language code for translations (default: 'en')
33
+ @param i18n: Translations dict
34
+ """
35
+ if isinstance(schema_json, dict):
36
+ self.schema = schema_json
37
+ else:
38
+ self.schema = json.loads(schema_json)
39
+
40
+ self.language = language
41
+ self.i18n = i18n or {}
42
+
43
+ self.question_class_mapping = question_class_mapping
44
+
45
+ # All questions (input + layout) keyed by name
46
+ self.questions = OrderedDict()
47
+
48
+ # Input-only questions keyed by name (no panels, html, image)
49
+ self.input_questions = OrderedDict()
50
+
51
+ # Questions keyed by internal id
52
+ self.question_ids = OrderedDict()
53
+
54
+ # Load all questions from the schema
55
+ self.load_questions()
56
+
57
+ @property
58
+ def title(self):
59
+ return self.schema.get('title', '')
60
+
61
+ @property
62
+ def description(self):
63
+ return self.schema.get('description', '')
64
+
65
+ @property
66
+ def pages(self):
67
+ return self.schema.get('pages', [])
68
+
69
+ @property
70
+ def components(self):
71
+ return self.questions
72
+
73
+ @property
74
+ def input_components(self):
75
+ return self.input_questions
76
+
77
+ def load_questions(self):
78
+ """Load questions from the schema, handling both pages-based and
79
+ flat elements-based schemas."""
80
+ pages = self.schema.get('pages')
81
+ if pages:
82
+ for page in pages:
83
+ elements = page.get('elements', [])
84
+ self._load_elements(elements)
85
+ else:
86
+ # Single page: elements at top level
87
+ elements = self.schema.get('elements', [])
88
+ self._load_elements(elements)
89
+
90
+ def _load_elements(self, elements, parent=None):
91
+ """Recursively load elements from the schema."""
92
+ for element in elements:
93
+ if 'type' not in element:
94
+ continue
95
+
96
+ question_obj = self.get_question_object(element)
97
+ if not question_obj:
98
+ continue
99
+
100
+ question_obj.load(
101
+ question_owner=self,
102
+ parent=parent,
103
+ data=None,
104
+ is_form=False,
105
+ )
106
+ self.questions[question_obj.name] = question_obj
107
+
108
+ if question_obj.id:
109
+ self.question_ids[question_obj.id] = question_obj
110
+
111
+ # Recurse into panel/page elements
112
+ if element['type'] in ('panel', 'paneldynamic'):
113
+ nested = element.get('elements', [])
114
+ self._load_elements(nested, parent=question_obj)
115
+
116
+ def get_question_class(self, element):
117
+ """Dynamically load the question class based on the element type."""
118
+ element_type = element.get('type')
119
+ element_type_cap = element_type.capitalize() if element_type else None
120
+ if not element_type:
121
+ return None
122
+
123
+ cls_mapping = self.question_class_mapping.get(element_type)
124
+ if cls_mapping:
125
+ cls_mapping = cls_mapping.capitalize() if isinstance(cls_mapping, str) else cls_mapping
126
+ if isinstance(cls_mapping, str):
127
+ cls_name = f"Question{cls_mapping}"
128
+ import_path = f"surveyjs.questions.{element_type}.{cls_mapping}"
129
+ try:
130
+ module = __import__(import_path, fromlist=[cls_name])
131
+ cls = getattr(module, cls_name)
132
+ return cls
133
+ except (AttributeError, ModuleNotFoundError) as e:
134
+ logger.warning(
135
+ "Could not load question class for type '%s': %s. "
136
+ "Falling back to base Question.", element_type, e
137
+ )
138
+ return Question
139
+ else:
140
+ return cls_mapping
141
+ else:
142
+ cls_name = f"Question{element_type_cap}"
143
+ import_path = 'surveyjs.questions.%s' % element_type
144
+ try:
145
+ module = __import__(import_path, fromlist=[cls_name])
146
+ return getattr(module, cls_name)
147
+ except (AttributeError, ModuleNotFoundError) as e:
148
+ logger.warning(
149
+ "Could not load question class for type '%s': %s. "
150
+ "Falling back to base Question.", element_type, e
151
+ )
152
+ return Question
153
+
154
+ def get_question_object(self, element):
155
+ """Create a question object from an element dict."""
156
+ cls = self.get_question_class(element)
157
+ if cls is None:
158
+ return None
159
+ return cls(element, self, language=self.language, i18n=self.i18n)
160
+
161
+ @property
162
+ def form(self):
163
+ """Placeholder form dict (always empty). Useful in contexts where
164
+ a question owner's form is requested."""
165
+ return {}
surveyjs/form.py ADDED
@@ -0,0 +1,147 @@
1
+ # Copyright 2026 Nova Code (https://www.novacode.nl)
2
+ # See LICENSE file for full licensing details.
3
+
4
+ import json
5
+ import logging
6
+ from collections import OrderedDict
7
+
8
+ from surveyjs import SurveyCreator
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class SurveyForm:
14
+ """
15
+ Represents a filled-in SurveyJS form (submission data).
16
+
17
+ Parses the form submission JSON against a SurveyCreator schema and creates
18
+ Question objects with values populated from the submission.
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ form_json,
24
+ creator=None,
25
+ creator_schema_json=None,
26
+ lang="en",
27
+ **kwargs
28
+ ):
29
+ """
30
+ @param form_json: SurveyJS Form submission JSON (str or dict)
31
+ @param creator: A SurveyCreator instance
32
+ @param creator_schema_json: SurveyCreator schema JSON (str or dict)
33
+ Alternative to providing a SurveyCreator instance.
34
+ @param lang: Language code for translations (default: 'en')
35
+ """
36
+
37
+ if creator and creator_schema_json:
38
+ raise Exception(
39
+ "Constructor accepts either creator or creator_schema_json, not both."
40
+ )
41
+
42
+ if isinstance(form_json, dict):
43
+ self.form = form_json
44
+ else:
45
+ self.form = json.loads(form_json)
46
+
47
+ self.creator = creator
48
+ self.creator_schema_json = creator_schema_json
49
+
50
+ if self.creator is None and self.creator_schema_json:
51
+ self.creator = SurveyCreator(self.creator_schema_json)
52
+
53
+ if self.creator:
54
+ if not isinstance(self.creator, SurveyCreator):
55
+ raise TypeError("creator must be a SurveyCreator instance")
56
+ elif self.creator_schema_json:
57
+ assert isinstance(self.creator_schema_json, str)
58
+
59
+ else:
60
+ raise Exception(
61
+ "Provide either the argument: creator or creator_schema_json."
62
+ )
63
+
64
+ self.lang = lang
65
+ # defaults to English (en) date/time format
66
+ self.date_format = kwargs.get('date_format', '%m/%d/%Y')
67
+ self.time_format = kwargs.get('time_format', '%H:%M:%S')
68
+
69
+ # All questions (input + layout) keyed by name
70
+ self.questions = OrderedDict()
71
+
72
+ # Input-only questions keyed by name
73
+ self.input_questions = OrderedDict()
74
+
75
+ # Questions keyed by id
76
+ self.question_ids = {}
77
+
78
+ # Load questions from survey schema + populate values from form data
79
+ self.load_questions()
80
+
81
+ # Create attribute-style accessor
82
+ self._data = FormData(self)
83
+
84
+ def set_creator_by_creator_schema_json(self):
85
+ self.creator = SurveyCreator(
86
+ self.creator_schema_json,
87
+ language=self.lang,
88
+ )
89
+
90
+ @property
91
+ def data(self):
92
+ """Attribute-style access to input questions."""
93
+ return self._data
94
+
95
+ @property
96
+ def components(self):
97
+ """ Alias for questions, including both input and layout questions."""
98
+ return self.questions
99
+
100
+ @property
101
+ def input_components(self):
102
+ """ Alias for questions, including input queastions."""
103
+ return self.input_questions
104
+
105
+ def load_questions(self):
106
+ """Load questions from the survey schema and populate values from
107
+ form submission data."""
108
+ for key, creator_question in self.creator.questions.items():
109
+ # Create a new question object (don't affect the Survey's question)
110
+ question_obj = self.creator.get_question_object(creator_question.raw)
111
+ question_obj.load(
112
+ question_owner=self,
113
+ parent=None,
114
+ data=self.form,
115
+ is_form=True,
116
+ )
117
+ self.questions[key] = question_obj
118
+
119
+ if question_obj.id:
120
+ self.question_ids[question_obj.id] = question_obj
121
+
122
+ def get_question_by_name(self, name):
123
+ """Get a question by its name."""
124
+ return self.input_questions.get(name)
125
+
126
+ def get_value(self, name):
127
+ """Get the value of a question by name."""
128
+ question = self.input_questions.get(name)
129
+ if question:
130
+ return question.value
131
+ return None
132
+
133
+
134
+ class FormData:
135
+ """Provides attribute-style access to form input questions.
136
+
137
+ Example:
138
+ form.data.firstName.value # => 'Bob'
139
+ """
140
+
141
+ def __init__(self, form):
142
+ self._form = form
143
+
144
+ def __getattr__(self, key):
145
+ if key.startswith('_'):
146
+ return super().__getattribute__(key)
147
+ return self._form.input_questions.get(key)
@@ -0,0 +1,2 @@
1
+ # Copyright 2026 Nova Code (https://www.novacode.nl)
2
+ # See LICENSE file for full licensing details.
@@ -0,0 +1,46 @@
1
+ # Copyright 2026 Nova Code (https://www.novacode.nl)
2
+ # See LICENSE file for full licensing details.
3
+
4
+ from .question import Question
5
+
6
+
7
+ class QuestionBoolean(Question):
8
+ """SurveyJS Boolean (Yes/No) question.
9
+
10
+ Value is typically True/False.
11
+ """
12
+
13
+ @property
14
+ def label_true(self):
15
+ return self.raw.get('labelTrue', 'Yes')
16
+
17
+ @property
18
+ def label_false(self):
19
+ return self.raw.get('labelFalse', 'No')
20
+
21
+ @property
22
+ def value_true(self):
23
+ return self.raw.get('valueTrue', True)
24
+
25
+ @property
26
+ def value_false(self):
27
+ return self.raw.get('valueFalse', False)
28
+
29
+ @property
30
+ def render_as(self):
31
+ """Render mode: 'default' (toggle) or 'checkbox' or 'radio'."""
32
+ return self.raw.get('renderAs', 'default')
33
+
34
+ def to_bool(self):
35
+ """Convert the value to a Python bool."""
36
+ val = self.value
37
+ if val is None:
38
+ return None
39
+ if isinstance(val, bool):
40
+ return val
41
+ if val == self.value_true:
42
+ return True
43
+ if val == self.value_false:
44
+ return False
45
+ # Fallback
46
+ return bool(val)
@@ -0,0 +1,65 @@
1
+ # Copyright 2026 Nova Code (https://www.novacode.nl)
2
+ # See LICENSE file for full licensing details.
3
+
4
+ from .question import Question
5
+
6
+
7
+ class QuestionCheckbox(Question):
8
+ """SurveyJS Checkbox (multiple selection) question.
9
+
10
+ Value is typically a list of selected choice values.
11
+ """
12
+
13
+ @property
14
+ def choices(self):
15
+ return self.raw.get('choices', [])
16
+
17
+ @property
18
+ def choices_values(self):
19
+ """Get normalized choice values as a list of dicts."""
20
+ result = []
21
+ for choice in self.choices:
22
+ if isinstance(choice, dict):
23
+ result.append({
24
+ 'value': choice.get('value'),
25
+ 'text': choice.get('text', str(choice.get('value', '')))
26
+ })
27
+ else:
28
+ result.append({'value': choice, 'text': str(choice)})
29
+ return result
30
+
31
+ @property
32
+ def value_texts(self):
33
+ """Get the display texts for the currently selected values."""
34
+ val = self.value
35
+ if not val or not isinstance(val, list):
36
+ return []
37
+ texts = []
38
+ choice_map = {c['value']: c['text'] for c in self.choices_values}
39
+ for v in val:
40
+ texts.append(choice_map.get(v, str(v)))
41
+ return texts
42
+
43
+ @property
44
+ def has_other(self):
45
+ return self.raw.get('showOtherItem', self.raw.get('hasOther', False))
46
+
47
+ @property
48
+ def has_none(self):
49
+ return self.raw.get('showNoneItem', self.raw.get('hasNone', False))
50
+
51
+ @property
52
+ def has_select_all(self):
53
+ return self.raw.get('showSelectAllItem', self.raw.get('hasSelectAll', False))
54
+
55
+ @property
56
+ def max_selected_choices(self):
57
+ return self.raw.get('maxSelectedChoices', 0)
58
+
59
+ @property
60
+ def min_selected_choices(self):
61
+ return self.raw.get('minSelectedChoices', 0)
62
+
63
+ @property
64
+ def col_count(self):
65
+ return self.raw.get('colCount', 0)
@@ -0,0 +1,25 @@
1
+ # Copyright 2026 Nova Code (https://www.novacode.nl)
2
+ # See LICENSE file for full licensing details.
3
+
4
+ from .question import Question
5
+
6
+
7
+ class QuestionComment(Question):
8
+ """SurveyJS Comment (Long Text / Multi-line) question."""
9
+
10
+ @property
11
+ def max_length(self):
12
+ return self.raw.get('maxLength', 0)
13
+
14
+ @property
15
+ def rows(self):
16
+ """Number of visible rows in the text area."""
17
+ return self.raw.get('rows', 4)
18
+
19
+ @property
20
+ def auto_grow(self):
21
+ return self.raw.get('autoGrow', False)
22
+
23
+ @property
24
+ def allow_resize(self):
25
+ return self.raw.get('allowResize', True)
@@ -0,0 +1,59 @@
1
+ # Copyright 2026 Nova Code (https://www.novacode.nl)
2
+ # See LICENSE file for full licensing details.
3
+
4
+ from .question import Question
5
+
6
+
7
+ class QuestionDropdown(Question):
8
+ """SurveyJS Dropdown (single-select) question."""
9
+
10
+ @property
11
+ def choices(self):
12
+ return self.raw.get('choices', [])
13
+
14
+ @property
15
+ def choices_values(self):
16
+ result = []
17
+ for choice in self.choices:
18
+ if isinstance(choice, dict):
19
+ result.append({
20
+ 'value': choice.get('value'),
21
+ 'text': choice.get('text', str(choice.get('value', '')))
22
+ })
23
+ else:
24
+ result.append({'value': choice, 'text': str(choice)})
25
+ return result
26
+
27
+ @property
28
+ def value_text(self):
29
+ """Get the display text for the current value."""
30
+ val = self.value
31
+ for choice in self.choices_values:
32
+ if choice['value'] == val:
33
+ return choice['text']
34
+ return str(val) if val is not None else None
35
+
36
+ @property
37
+ def has_other(self):
38
+ return self.raw.get('showOtherItem', self.raw.get('hasOther', False))
39
+
40
+ @property
41
+ def other_text(self):
42
+ return self.raw.get('otherText', 'Other')
43
+
44
+ @property
45
+ def choices_by_url(self):
46
+ """Get choices loaded from a URL configuration."""
47
+ return self.raw.get('choicesByUrl', None)
48
+
49
+ @property
50
+ def search_enabled(self):
51
+ return self.raw.get('searchEnabled', True)
52
+
53
+ @property
54
+ def placeholder_text(self):
55
+ return self.raw.get('placeholder', '')
56
+
57
+ @property
58
+ def allow_clear(self):
59
+ return self.raw.get('allowClear', True)
@@ -0,0 +1,49 @@
1
+ # Copyright 2026 Nova Code (https://www.novacode.nl)
2
+ # See LICENSE file for full licensing details.
3
+
4
+ from .question import Question
5
+
6
+
7
+ class QuestionExpression(Question):
8
+ """SurveyJS Expression question.
9
+
10
+ A read-only calculated field. Value is computed from an expression.
11
+ """
12
+
13
+ @property
14
+ def expression(self):
15
+ """The calculation expression."""
16
+ return self.raw.get('expression', '')
17
+
18
+ @property
19
+ def display_style(self):
20
+ """'none', 'decimal', 'currency', or 'percent'."""
21
+ return self.raw.get('displayStyle', 'none')
22
+
23
+ @property
24
+ def currency(self):
25
+ return self.raw.get('currency', 'USD')
26
+
27
+ @property
28
+ def maximum_fraction_digits(self):
29
+ return self.raw.get('maximumFractionDigits', -1)
30
+
31
+ @property
32
+ def minimum_fraction_digits(self):
33
+ return self.raw.get('minimumFractionDigits', -1)
34
+
35
+ @property
36
+ def use_grouping(self):
37
+ return self.raw.get('useGrouping', True)
38
+
39
+ def to_number(self):
40
+ """Try to convert the expression value to a number."""
41
+ val = self.value
42
+ if val is None:
43
+ return None
44
+ try:
45
+ if isinstance(val, (int, float)):
46
+ return val
47
+ return float(val)
48
+ except (ValueError, TypeError):
49
+ return None
@@ -0,0 +1,53 @@
1
+ # Copyright 2026 Nova Code (https://www.novacode.nl)
2
+ # See LICENSE file for full licensing details.
3
+
4
+ from .question import Question
5
+
6
+
7
+ class QuestionFile(Question):
8
+ """SurveyJS File Upload question.
9
+
10
+ Value is typically a list of file metadata objects or base64 data.
11
+ """
12
+
13
+ @property
14
+ def store_data_as_text(self):
15
+ """Whether files are stored as base64 text."""
16
+ return self.raw.get('storeDataAsText', True)
17
+
18
+ @property
19
+ def allow_multiple(self):
20
+ return self.raw.get('allowMultiple', False)
21
+
22
+ @property
23
+ def max_size(self):
24
+ """Maximum file size in bytes (0 = unlimited)."""
25
+ return self.raw.get('maxSize', 0)
26
+
27
+ @property
28
+ def accepted_types(self):
29
+ """Accepted file types (MIME types or extensions)."""
30
+ return self.raw.get('acceptedTypes', '')
31
+
32
+ @property
33
+ def allow_camera_access(self):
34
+ return self.raw.get('allowCameraAccess', False)
35
+
36
+ @property
37
+ def source_type(self):
38
+ return self.raw.get('sourceType', 'file')
39
+
40
+ @property
41
+ def files(self):
42
+ """Get the list of file objects from the value."""
43
+ val = self.value
44
+ if val is None:
45
+ return []
46
+ if isinstance(val, list):
47
+ return val
48
+ return [val]
49
+
50
+ @property
51
+ def file_count(self):
52
+ """Number of uploaded files."""
53
+ return len(self.files)
@@ -0,0 +1,21 @@
1
+ # Copyright 2026 Nova Code (https://www.novacode.nl)
2
+ # See LICENSE file for full licensing details.
3
+
4
+ from .question import Question
5
+
6
+
7
+ class QuestionHtml(Question):
8
+ """SurveyJS HTML question.
9
+
10
+ Displays static HTML content. Not an input element.
11
+ """
12
+
13
+ @property
14
+ def is_input(self):
15
+ """HTML elements are display-only, not input questions."""
16
+ return False
17
+
18
+ @property
19
+ def html(self):
20
+ """Get the HTML content."""
21
+ return self.raw.get('html', '')