-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- ref: questionpy-org/moodle-qtype_questionpy#38 - removes as much logic from the jinja forms as possible - allows {qpy:repno} to be replaced with the current repetition number
- Loading branch information
1 parent
05c3e93
commit d5bb595
Showing
13 changed files
with
373 additions
and
119 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
# This file is part of QuestionPy. (https://questionpy.org) | ||
# QuestionPy is free software released under terms of the MIT license. See LICENSE.md. | ||
# (c) Technische Universität Berlin, innoCampus <[email protected]> | ||
|
||
from typing import List, Any, Optional, Dict | ||
|
||
from questionpy_common.elements import StaticTextElement, CheckboxElement, TextInputElement, FormElement, \ | ||
GroupElement, RepetitionElement, OptionsFormDefinition, CheckboxGroupElement, RadioGroupElement, SelectElement, \ | ||
HiddenElement | ||
|
||
from questionpy_sdk.webserver.elements import CxdOptionsFormDefinition, CxdFormSection, CxdFormElement, \ | ||
CxdGroupElement, CxdRepetitionElement, CxdStaticTextElement, CxdTextInputElement, \ | ||
CxdCheckboxElement, CxdCheckboxGroupElement, CxdRadioGroupElement, CxdSelectElement, CxdHiddenElement | ||
|
||
element_mapping: Dict[type, type] = { | ||
StaticTextElement: CxdStaticTextElement, | ||
TextInputElement: CxdTextInputElement, | ||
CheckboxElement: CxdCheckboxElement, | ||
CheckboxGroupElement: CxdCheckboxGroupElement, | ||
RadioGroupElement: CxdRadioGroupElement, | ||
SelectElement: CxdSelectElement, | ||
HiddenElement: CxdHiddenElement | ||
} | ||
|
||
|
||
def _contextualize_element(element: FormElement, form_data: Optional[dict[str, Any]], path: List[str], | ||
context: Optional[dict] = None) -> CxdFormElement: | ||
path.append(element.name) | ||
element_form_data = None | ||
if form_data: | ||
element_form_data = form_data.get(element.name) | ||
|
||
if isinstance(element, GroupElement): | ||
cxd_gr_element = CxdGroupElement(path=path.copy(), **element.model_dump(exclude={'elements'})) | ||
cxd_gr_element.cxd_elements = _contextualize_element_list(element.elements, element_form_data, path) | ||
path.pop() | ||
return cxd_gr_element | ||
|
||
if isinstance(element, RepetitionElement): | ||
cxd_rep_element = CxdRepetitionElement(path=path.copy(), **element.model_dump(exclude={'elements'})) | ||
if not element_form_data: | ||
for i in range(element.initial_repetitions): | ||
path.append(str(i)) | ||
element_list = _contextualize_element_list(element.elements, None, path, {'repno': i}) | ||
cxd_rep_element.cxd_elements.append(element_list) | ||
path.pop() | ||
path.pop() | ||
return cxd_rep_element | ||
for i, repetition in enumerate(element_form_data): | ||
path.append(str(i)) | ||
element_list = _contextualize_element_list(element.elements, repetition, path, {'repno': i}) | ||
cxd_rep_element.cxd_elements.append(element_list) | ||
path.pop() | ||
path.pop() | ||
return cxd_rep_element | ||
|
||
if element.__class__ not in element_mapping: | ||
raise ValueError(f"No corresponding CxdFormElement found for {element.__class__}") | ||
|
||
cxd_element_class = element_mapping[element.__class__] | ||
cxd_element = cxd_element_class(**element.model_dump(), path=path) | ||
if context: | ||
cxd_element.contextualize(context.get('repno')) | ||
cxd_element.add_form_data_value(element_form_data) | ||
|
||
path.pop() | ||
return cxd_element | ||
|
||
|
||
def _contextualize_element_list(element_list: List[FormElement], form_data: Optional[dict[str, Any]], | ||
path: list[str], context: Optional[dict] = None) -> List[CxdFormElement]: | ||
cxd_element_list: List[CxdFormElement] = [] | ||
for element in element_list: | ||
cxd_element = _contextualize_element(element, form_data, path, context) | ||
cxd_element_list.append(cxd_element) | ||
return cxd_element_list | ||
|
||
|
||
def contextualize(form_definition: OptionsFormDefinition, form_data: Optional[dict[str, Any]]) \ | ||
-> CxdOptionsFormDefinition: | ||
path: list[str] = ['general'] | ||
cxd_options_form = CxdOptionsFormDefinition() | ||
cxd_options_form.general = _contextualize_element_list(form_definition.general, form_data, path) | ||
for section in form_definition.sections: | ||
path = [section.name] | ||
cxd_section = CxdFormSection(name=section.name, header=section.header, path=path) | ||
section_data = None | ||
if form_data: | ||
section_data = form_data.get(section.name) | ||
cxd_section.cxd_elements = _contextualize_element_list(section.elements, section_data, path) | ||
cxd_options_form.sections.append(cxd_section) | ||
return cxd_options_form |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
# This file is part of QuestionPy. (https://questionpy.org) | ||
# QuestionPy is free software released under terms of the MIT license. See LICENSE.md. | ||
# (c) Technische Universität Berlin, innoCampus <[email protected]> | ||
|
||
import re | ||
from typing import List, Union, Annotated, Any, Optional | ||
from typing_extensions import TypeAlias | ||
from pydantic import Field, BaseModel | ||
|
||
from questionpy_common.elements import StaticTextElement, GroupElement, HiddenElement, RepetitionElement, \ | ||
SelectElement, RadioGroupElement, Option, CheckboxGroupElement, CheckboxElement, TextInputElement, FormSection | ||
# pylint: disable=unused-import | ||
from questionpy_common.elements import FormElement # noqa: F401 | ||
|
||
|
||
class _CxdFormElement(BaseModel): | ||
path: list[str] | ||
id: str = '' | ||
|
||
def contextualize(self, text: str) -> None: | ||
pass | ||
|
||
def add_form_data_value(self, element_form_data: Any) -> None: | ||
pass | ||
|
||
|
||
class CxdTextInputElement(TextInputElement, _CxdFormElement): | ||
value: Optional[str] = None | ||
|
||
def contextualize(self, text: str) -> None: | ||
self.label = re.sub(r'\{ qpy:repno }', f'{text}', self.label) | ||
self.default = re.sub(r'\{ qpy:repno }', f'{text}', self.default) | ||
self.placeholder = re.sub(r'\{ qpy:repno }', f'{text}', self.placeholder) | ||
|
||
def add_form_data_value(self, element_form_data: Any) -> None: | ||
if element_form_data: | ||
self.value = element_form_data | ||
|
||
|
||
class CxdStaticTextElement(StaticTextElement, _CxdFormElement): | ||
|
||
def contextualize(self, text: str) -> None: | ||
self.label = re.sub(r'\{ qpy:repno }', f'{text}', self.label) | ||
self.text = re.sub(r'\{ qpy:repno }', f'{text}', self.text) | ||
|
||
|
||
class CxdCheckboxElement(CheckboxElement, _CxdFormElement): | ||
def contextualize(self, text: str) -> None: | ||
self.left_label = re.sub(r'\{ qpy:repno }', f'{text}', self.left_label) | ||
self.right_label = re.sub(r'\{ qpy:repno }', f'{text}', self.right_label) | ||
|
||
def add_form_data_value(self, element_form_data: Any) -> None: | ||
if element_form_data: | ||
self.selected = element_form_data | ||
|
||
|
||
class CxdCheckboxGroupElement(CheckboxGroupElement, _CxdFormElement): | ||
cxd_checkboxes: List[CxdCheckboxElement] = [] | ||
|
||
def __init__(self, **data: Any): | ||
super().__init__(**data) | ||
for checkbox in self.checkboxes: | ||
path = data.get('path') | ||
if not isinstance(path, list): | ||
raise TypeError(f"Path should be of type list but is {type(path)}") | ||
path.append(checkbox.name) | ||
self.cxd_checkboxes.append(CxdCheckboxElement(**checkbox.model_dump(), path=path)) | ||
path.pop() | ||
self.checkboxes = [] | ||
|
||
def contextualize(self, text: str) -> None: | ||
for cxd_checkbox in self.cxd_checkboxes: | ||
cxd_checkbox.contextualize(text) | ||
|
||
def add_form_data_value(self, element_form_data: Any) -> None: | ||
if not element_form_data: | ||
return | ||
selected_checkboxes = [chk for chk in self.cxd_checkboxes if chk.name in element_form_data] | ||
|
||
for checkbox in self.cxd_checkboxes: | ||
checkbox.selected = checkbox in selected_checkboxes | ||
|
||
|
||
class CxdOption(Option, _CxdFormElement): | ||
def contextualize(self, text: str) -> None: | ||
self.label = re.sub(r'\{ qpy:repno }', f'{text}', self.label) | ||
|
||
|
||
class CxdRadioGroupElement(RadioGroupElement, _CxdFormElement): | ||
cxd_options: List[CxdOption] = [] | ||
|
||
def __init__(self, **data: Any): | ||
super().__init__(**data) | ||
for option in self.options: | ||
path = data.get('path') | ||
if not isinstance(path, list): | ||
raise TypeError(f"Path should be of type list but is {type(path)}") | ||
path.append(option.value) | ||
self.cxd_options.append(CxdOption(**option.model_dump(), path=path)) | ||
path.pop() | ||
self.options = [] | ||
|
||
def contextualize(self, text: str) -> None: | ||
self.label = re.sub(r'\{ qpy:repno }', f'{text}', self.label) | ||
for cxd_option in self.cxd_options: | ||
cxd_option.contextualize(text) | ||
|
||
def add_form_data_value(self, element_form_data: Any) -> None: | ||
if not element_form_data: | ||
return | ||
|
||
for option in self.cxd_options: | ||
option.selected = option.value == element_form_data | ||
|
||
|
||
class CxdSelectElement(SelectElement, _CxdFormElement): | ||
cxd_options: List[CxdOption] = [] | ||
|
||
def __init__(self, **data: Any): | ||
super().__init__(**data) | ||
for option in self.options: | ||
path = data.get('path') | ||
if not isinstance(path, list): | ||
raise TypeError(f"Path should be of type list but is {type(path)}") | ||
path.append(option.value) | ||
self.cxd_options.append(CxdOption(**option.model_dump(), path=path)) | ||
path.pop() | ||
self.options = [] | ||
|
||
def contextualize(self, text: str) -> None: | ||
self.label = re.sub(r'\{ qpy:repno }', f'{text}', self.label) | ||
|
||
def add_form_data_value(self, element_form_data: Any) -> None: | ||
if not element_form_data: | ||
return | ||
selected_options = [opt for opt in self.cxd_options if opt.value in element_form_data] | ||
|
||
for option in self.cxd_options: | ||
option.selected = option in selected_options | ||
|
||
|
||
class CxdHiddenElement(HiddenElement, _CxdFormElement): | ||
def contextualize(self, text: str) -> None: | ||
pass | ||
|
||
def add_form_data_value(self, element_form_data: Any) -> None: | ||
if element_form_data: | ||
self.value = element_form_data | ||
|
||
|
||
class CxdGroupElement(GroupElement, _CxdFormElement): | ||
cxd_elements: List["CxdFormElement"] = [] | ||
|
||
def __init__(self, **data: Any): | ||
super().__init__(**data, elements=[]) | ||
|
||
|
||
class CxdRepetitionElement(RepetitionElement, _CxdFormElement): | ||
cxd_elements: list[list["CxdFormElement"]] = [] | ||
|
||
def __init__(self, **data: Any): | ||
super().__init__(**data, elements=[]) | ||
|
||
|
||
CxdFormElement: TypeAlias = Annotated[Union[ | ||
CxdStaticTextElement, CxdTextInputElement, CxdCheckboxElement, CxdCheckboxGroupElement, | ||
CxdRadioGroupElement, CxdSelectElement, CxdHiddenElement, CxdGroupElement, CxdRepetitionElement | ||
], Field(discriminator="kind")] | ||
|
||
|
||
class GenericCxdFormElement(BaseModel): | ||
element: CxdFormElement | ||
|
||
|
||
class CxdFormSection(FormSection): | ||
cxd_elements: List[CxdFormElement] = [] | ||
"""Elements contained in the section.""" | ||
|
||
|
||
class CxdOptionsFormDefinition(BaseModel): | ||
general: List[CxdFormElement] = [] | ||
"""Elements to add to the main section, after the LMS' own elements.""" | ||
sections: List[CxdFormSection] = [] | ||
"""Sections to add after the main section.""" |
28 changes: 14 additions & 14 deletions
28
questionpy_sdk/webserver/templates/elements/checkbox.html.jinja2
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,28 +1,28 @@ | ||
{% extends "elements/element.html.jinja2" %} | ||
|
||
{% block info %} | ||
{% if element_options.left_label %} | ||
<p class="element-label">{{ element_options.left_label }}{{ '*' if element_options.required }}</p> | ||
{% if element.left_label %} | ||
<p class="element-label">{{ element.left_label }}{{ '*' if element.required }}</p> | ||
{% endif %} | ||
{% if element_options.help %} | ||
{% if element_options.left_label %} | ||
{{ macros.create_icon_container(element_options.left_label, element_options.help) }} | ||
{% elif element_options.right_label %} | ||
{{ macros.create_icon_container(element_options.right_label, element_options.help) }} | ||
{% if element.help %} | ||
{% if element.left_label %} | ||
{{ macros.create_icon_container(element.left_label, element.help) }} | ||
{% elif element.right_label %} | ||
{{ macros.create_icon_container(element.right_label, element.help) }} | ||
{% endif %} | ||
{% endif %} | ||
{% endblock %} | ||
|
||
{% block content %} | ||
<label class="answer-label"> | ||
<input class="answer-checkbox" type="checkbox" id="{{ section_name }}_{{ element_options.name }}" | ||
<input class="answer-checkbox" type="checkbox" id="{{ element.id }}" | ||
name="{{ element_reference }}" | ||
{{ {'data-absolute_path': absolute_path|tojson|forceescape}|xmlattr }} | ||
{{ 'required' if element_options.required }} | ||
{{ 'checked' if element_options.selected and not form_data | ||
or form_data is defined and form_data}}> | ||
{% if element_options.right_label %} | ||
{{ element_options.right_label }}{{ '*' if element_options.required }} | ||
{{ {'data-absolute_path': element.path|tojson|forceescape}|xmlattr }} | ||
{{ 'required' if element.required }} | ||
{{ 'checked' if element.selected and not element.value | ||
or element.value is defined and element.value}}> | ||
{% if element.right_label %} | ||
{{ element.right_label }}{{ '*' if element.required }} | ||
{% endif %} | ||
</label> | ||
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.