Skip to content

Commit

Permalink
refactor: question_state and form_data handling
Browse files Browse the repository at this point in the history
  • Loading branch information
alexanderschmitz committed Aug 3, 2023
1 parent e29103b commit 9afc623
Show file tree
Hide file tree
Showing 6 changed files with 275 additions and 300 deletions.
423 changes: 205 additions & 218 deletions poetry.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ aiohttp = "^3.8.1"
pydantic = "^1.10.5"
PyYAML = "^6.0"
questionpy-common = { git = "https://github.com/questionpy-org/questionpy-common.git", rev = "9b3bedd711b2b699b26882ba187002a97bf8a057" }
questionpy-server = { git = "https://github.com/questionpy-org/questionpy-server.git", rev = "c77a7c3a68403848410421633a1f1a756c9edb86" }
questionpy-server = { git = "https://github.com/questionpy-org/questionpy-server.git", rev = "ddae12f3cdbc034e99d16ee58eee757a12173fc8" }
jinja2 = "^3.1.2"
aiohttp-jinja2 = "^1.5"

Expand Down
45 changes: 25 additions & 20 deletions questionpy_sdk/webserver/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import json
from pathlib import Path
from typing import Optional

import aiohttp_jinja2
from aiohttp import web
Expand All @@ -14,7 +15,7 @@
from questionpy_server.worker.exception import WorkerUnknownError
from questionpy_server.worker.worker.thread import ThreadWorker

from questionpy_sdk.webserver.state_storage import QuestionStateStorage, add_repetition
from questionpy_sdk.webserver.state_storage import QuestionStateStorage, add_repetition, parse_form_data

routes = web.RouteTableDef()

Expand Down Expand Up @@ -43,17 +44,19 @@ def start_server(self) -> None:

@routes.get('/')
async def render_options(request: web.Request) -> web.Response:
"""Get the options form definition that allow a question creator to customize a question."""
"""Get the options form definition that allows a question creator to customize a question."""
webserver: 'WebServer' = request.app['sdk_webserver_app']
stored_state = webserver.state_storage.get(webserver.package)
old_state: Optional[bytes] = json.dumps(stored_state).encode() if stored_state else None

async with webserver.worker_pool.get_worker(webserver.package, 0, None) as worker:
manifest = await worker.get_manifest()
form_definition, _ = await worker.get_options_form(None)
form_definition, form_data = await worker.get_options_form(old_state)

context = {
'manifest': manifest,
'options': form_definition.dict(),
'form_data': webserver.state_storage.get(webserver.package)
'form_data': form_data
}

return aiohttp_jinja2.render_template('options.html.jinja2', request, context)
Expand All @@ -63,47 +66,49 @@ async def render_options(request: web.Request) -> web.Response:
async def submit_form(request: web.Request) -> web.Response:
"""Store the form_data from the Options Form in the StateStorage."""
webserver: 'WebServer' = request.app['sdk_webserver_app']
form_data = await request.json()
data = await request.json()
parsed_form_data = parse_form_data(data)
stored_state = webserver.state_storage.get(webserver.package)
old_state: Optional[bytes] = json.dumps(stored_state).encode() if stored_state else None

async with webserver.worker_pool.get_worker(webserver.package, 0, None) as worker:
form_definition, _ = await worker.get_options_form(None)
parsed_form_data = webserver.state_storage.parse_form_data(form_definition, form_data)
try:
question = await worker.create_question_from_options(old_state=None, form_data=parsed_form_data)
question = await worker.create_question_from_options(old_state, form_data=parsed_form_data)
except WorkerUnknownError:
return HTTPBadRequest()

question_state = json.loads(question.question_state)
webserver.state_storage.insert(webserver.package, question_state)
new_state = question.question_state
webserver.state_storage.insert(webserver.package, json.loads(new_state))

return web.json_response(question.question_state)
return web.json_response(new_state)


@routes.post('/repeat')
async def repeat_element(request: web.Request) -> web.Response:
"""Add Repetitions to the referenced RepetitionElement and store the form_data in the StateStorage."""
webserver: 'WebServer' = request.app['sdk_webserver_app']
data = await request.json()
form_data = data['form_data']
repetition_reference = data['element-name'].replace(']', '').split('[')
old_form_data = add_repetition(form_data=parse_form_data(data['form_data']),
reference=data['element-name'].replace(']', '').split('['),
increment=int(data['increment']))
stored_state = webserver.state_storage.get(webserver.package)
old_state: Optional[bytes] = json.dumps(stored_state).encode() if stored_state else None

async with webserver.worker_pool.get_worker(webserver.package, 0, None) as worker:
manifest = await worker.get_manifest()
form_definition, _ = await worker.get_options_form(None)
parsed_form_data = webserver.state_storage.parse_form_data(form_definition, form_data)
parsed_form_data = add_repetition(parsed_form_data, form_definition, repetition_reference)
try:
question = await worker.create_question_from_options(old_state=None, form_data=parsed_form_data)
question = await worker.create_question_from_options(old_state, form_data=old_form_data)
except WorkerUnknownError:
return HTTPBadRequest()

question_state = json.loads(question.question_state)
webserver.state_storage.insert(webserver.package, question_state)
new_state = question.question_state
webserver.state_storage.insert(webserver.package, json.loads(new_state))
form_definition, form_data = await worker.get_options_form(new_state.encode())

context = {
'manifest': manifest,
'options': form_definition.dict(),
'form_data': webserver.state_storage.get(webserver.package)
'form_data': form_data
}

return aiohttp_jinja2.render_template('options.html.jinja2', request, context)

This file was deleted.

94 changes: 36 additions & 58 deletions questionpy_sdk/webserver/state_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@

import json
from pathlib import Path
from typing import Optional, Union, List, Any

from questionpy_common.elements import StaticTextElement, SelectElement, GroupElement, OptionsFormDefinition, \
FormElement, RepetitionElement
from typing import Optional, Any, Union


def _unflatten(flat_form_data: dict[str, str]) -> dict[str, Any]:
Expand All @@ -20,34 +17,52 @@ def _unflatten(flat_form_data: dict[str, str]) -> dict[str, Any]:
current_dict[k] = {}
current_dict = current_dict[k]
current_dict[key.split('[')[-1][:-1]] = value
return unflattened_dict

result = _convert_repetition_dict_to_list(unflattened_dict)
if isinstance(result, dict):
return result
else:
raise ValueError("The result is not a dictionary.")


def _convert_repetition_dict_to_list(dictionary: dict[str, Any]) -> Union[dict[str, Any], list[Any]]:
if not isinstance(dictionary, dict):
return dictionary

if all(key.isnumeric() for key in dictionary.keys()):
return list(dictionary.values())

for key, value in dictionary.items():
dictionary[key] = _convert_repetition_dict_to_list(value)

def add_repetition(form_data: dict[str, Any], form_definition: OptionsFormDefinition, reference: list) \
-> dict[str, Any]:
return dictionary


def parse_form_data(form_data: dict) -> dict:
unflattened_form_data = _unflatten(form_data)
options = unflattened_form_data['general']
for section_name, section in unflattened_form_data.items():
if not section_name == 'general':
options[section_name] = section
return options


def add_repetition(form_data: dict[str, Any], reference: list, increment: int) -> dict[str, Any]:
"""Adds repetitions of the referenced RepetitionElement to the form_data."""
# Find RepetitionElement in the FormDefinition.
definition_element = form_definition.general
form_data_element = form_data
current_element = form_data

if ref := reference.pop(0) != 'general':
section = next(filter(lambda s: s.name == ref, form_definition.sections))
definition_element = section.elements
form_data_element = form_data_element[section.name]
current_element = current_element[ref]
while reference:
ref = reference.pop(0)
element = next(filter(lambda e: (e.name == ref), definition_element))
if not (isinstance(element, GroupElement) or isinstance(element, RepetitionElement)):
return form_data
definition_element = element.elements
form_data_element = form_data_element[ref]
current_element = current_element[ref]

if not isinstance(element, RepetitionElement) or not isinstance(form_data_element, list):
if not isinstance(current_element, list):
return form_data

# Add "increment" number of repetitions.
for i in range(element.increment):
form_data_element.append(form_data_element[-1])
for i in range(increment):
current_element.append(current_element[-1])

return form_data

Expand Down Expand Up @@ -75,40 +90,3 @@ def get(self, key: Path) -> Optional[dict]:
return None
self.paths[key] = path
return json.loads(path.read_text())

def parse_form_data(self, form_definition: OptionsFormDefinition, form_data: dict) -> dict:
unflattened_form_data = _unflatten(form_data)
options = self._parse_section(form_definition.general, unflattened_form_data['general'])
for section in form_definition.sections:
options[section.name] = self._parse_section(section.elements, unflattened_form_data[section.name])
return options

def _parse_section(self, section: List[FormElement], section_form_data: dict) -> dict:
options = {}
for form_element in section:
if not isinstance(form_element, StaticTextElement) \
and (form_element.name in section_form_data or isinstance(form_element, GroupElement)):
parsed_element = self._parse_form_element(form_element, section_form_data)
if parsed_element:
options[form_element.name] = parsed_element
return options

def _parse_form_element(self, form_element: FormElement, form_data: dict) \
-> Optional[Union[str, int, list, dict, FormElement]]:
if isinstance(form_element, SelectElement):
return form_data[form_element.name]
elif isinstance(form_element, GroupElement):
if form_element.name not in form_data:
return None
group = {}
for child in form_element.elements:
if not isinstance(child, StaticTextElement) and child.name in form_data[form_element.name]:
group[child.name] = self._parse_form_element(child, form_data[form_element.name])
return group
elif isinstance(form_element, RepetitionElement):
repetition = []
for key, value in form_data[form_element.name].items():
repetition.append(self._parse_section(form_element.elements, value))
return repetition
else:
return form_data[form_element.name]
10 changes: 8 additions & 2 deletions questionpy_sdk/webserver/static/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -243,12 +243,18 @@ async function add_repetition_element(event) {

const json_form_data = {};
for (const pair of new FormData(form)) {
json_form_data[pair[0]] = pair[1];
if (json_form_data[pair[0]]) {
json_form_data[pair[0]] = [json_form_data[pair[0]]]
json_form_data[pair[0]].push(pair[1]);
} else {
json_form_data[pair[0]] = pair[1];
}
}

const data = {
'form_data': json_form_data,
'element-name': event.target.name
'element-name': event.target.name,
'increment': event.target.dataset['repetition_increment']
}

const headers = {'Content-Type': 'application/json'}
Expand Down

0 comments on commit 9afc623

Please sign in to comment.