Skip to content

Commit b381cf3

Browse files
committed
Switch configuration to YAML
1 parent a669ae7 commit b381cf3

14 files changed

Lines changed: 353 additions & 450 deletions

app/forms/config/__init__.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import importlib
2+
import os.path
3+
from pathlib import Path
4+
5+
import app.forms.parts as form_parts
6+
import yaml
7+
from app.forms.models import FormFlow
8+
9+
10+
def load_config(form_slug: str) -> FormFlow:
11+
if not form_slug:
12+
raise ValueError("Form slug must be provided")
13+
14+
config_path = os.path.join("/", "app", "app", "forms", "config", f"{form_slug}.yml")
15+
16+
form_config = Path(config_path)
17+
if not form_config.is_file():
18+
raise FileNotFoundError(
19+
f"Form configuration file not found for form: {form_config}"
20+
)
21+
22+
try:
23+
with open(config_path) as stream:
24+
return yaml.safe_load(stream)
25+
except yaml.YAMLError as e:
26+
raise ValueError(f"Error loading YAML configuration for form {form_slug}: {e}")
27+
28+
29+
def form_flow_from_config(config: dict, slug: str) -> FormFlow:
30+
if not config:
31+
raise ValueError("Configuration cannot be empty")
32+
33+
form_flow = FormFlow(slug=slug)
34+
35+
if not "startingPage" in config:
36+
raise ValueError("Configuration must contain 'startingPage'")
37+
38+
starting_page_config = config.get("startingPage", {})
39+
starting_page_id = starting_page_config.get("id", "")
40+
form_flow.create_starting_page(
41+
id=starting_page_id,
42+
name=starting_page_config.get("name", ""),
43+
slug=starting_page_config.get("slug", "/"),
44+
description=starting_page_config.get("description", ""),
45+
template=starting_page_config.get("template", ""),
46+
form=(
47+
getattr(importlib.import_module(
48+
f"app.forms.parts.{starting_page_config.get('form')}"
49+
), starting_page_config.get('form'))
50+
if starting_page_config.get("form")
51+
else None
52+
),
53+
yaml_config=starting_page_config,
54+
)
55+
56+
pages_config = {starting_page_id: starting_page_config}
57+
58+
for page in config.get("pages", []):
59+
id = page.get("id", "")
60+
if not id or id in pages_config:
61+
raise ValueError("Each page must have a unique 'id'")
62+
pages_config.update({id: page})
63+
form_flow.create_page(
64+
id=id,
65+
name=page.get("name", ""),
66+
slug=page.get("slug", ""),
67+
description=page.get("description", ""),
68+
template=page.get("template", ""),
69+
form=(
70+
getattr(importlib.import_module(
71+
f"app.forms.parts.{page.get('form')}"
72+
), page.get('form'))
73+
if page.get("form")
74+
else None
75+
),
76+
yaml_config=page,
77+
)
78+
79+
for page_id, page in form_flow.get_all_pages():
80+
page_config = page.yaml_config
81+
82+
for redirection in page_config.get("redirectWhenComplete", []):
83+
redirect_page = form_flow.get_page_by_id(redirection.get("page", ""))
84+
redirect_url = redirection.get("url", "")
85+
redirect_flask_method = redirection.get("flaskMethod", "")
86+
if not (redirect_page or redirect_url or redirect_flask_method):
87+
raise ValueError(
88+
f"Redirect target page or URL/flaskMethod must be provided for page '{page.slug}'."
89+
)
90+
when = None
91+
if when_data := redirection.get("when", {}):
92+
key = when_data.get("key", "")
93+
value = when_data.get("value", "")
94+
if key and value:
95+
when = (key, value)
96+
page.redirect_when_complete(
97+
page=redirect_page,
98+
flask_method=redirect_flask_method,
99+
url=redirect_url,
100+
when=when,
101+
# condition=TODO
102+
)
103+
104+
for requirement in page_config.get("requireResponse", []):
105+
required_page = form_flow.get_page_by_id(requirement.get("page"))
106+
if not required_page:
107+
raise ValueError(
108+
f"Required page '{requirement.get('page')}' not found in form flow as a prerequisite to '{page.slug}'."
109+
)
110+
page.require_response(
111+
page=required_page,
112+
key=requirement.get("key"),
113+
response=requirement.get("value", None),
114+
)
115+
116+
if require_completion_of := page_config.get("requireCompletionOf", []):
117+
required_pages = [
118+
form_flow.get_page_by_id(id) for id in require_completion_of
119+
]
120+
if any([page is None for page in required_pages]):
121+
raise ValueError(
122+
f"One or more required pages for 'requireCompletionOf' of '{page.slug}' not found in form flow."
123+
)
124+
page.require_completion_of(*required_pages)
125+
126+
if require_completion_of_any := page_config.get("requireCompletionOfAny", []):
127+
required_pages = [
128+
form_flow.get_page_by_id(id) for id in require_completion_of_any
129+
]
130+
if any([page is None for page in required_pages]):
131+
raise ValueError(
132+
f"One or more required pages for 'requireCompletionOfAny' of '{page.slug}' not found in form flow."
133+
)
134+
fallback_page = form_flow.get_page_by_id(
135+
page_config.get("redirectIfNotComplete", None)
136+
)
137+
page.require_completion_of_any(
138+
pages=required_pages, fallback_page=fallback_page
139+
)
140+
141+
return form_flow

app/forms/config/example.py

Lines changed: 0 additions & 85 deletions
This file was deleted.

app/forms/config/example.yml

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
startingPage:
2+
id: pizza_or_chocolate
3+
name: Pizza or chocolate
4+
slug: pizza-or-chocolate
5+
description: >
6+
Do you prefer pizza or chocolate? This is a simple example form to demonstrate the structure.
7+
form: PizzaOrChocolateForm
8+
redirectWhenComplete:
9+
- page: pizza_topping
10+
when:
11+
key: food
12+
value: pizza
13+
- page: type_of_chocolate
14+
when:
15+
key: food
16+
value: chocolate
17+
- page: neither_page
18+
when:
19+
key: food
20+
value: neither
21+
- url: https://www.reddit.com/r/catpictures/
22+
when:
23+
key: food
24+
value: cats
25+
26+
pages:
27+
- id: neither_page
28+
name: Neither
29+
slug: neither
30+
description: You selected neither pizza nor chocolate.
31+
template: forms/example/neither.html
32+
33+
- id: pizza_topping
34+
name: Pizza toppings
35+
slug: pizza-topping
36+
form: PizzaToppingsForm
37+
requireResponse:
38+
- page: pizza_or_chocolate
39+
key: food
40+
value: pizza
41+
redirectWhenComplete:
42+
- page: pizza_brand
43+
44+
- id: pizza_brand
45+
name: Pizza brand
46+
slug: pizza-brand
47+
form: PizzaBrandForm
48+
requireCompletionOf:
49+
- pizza_topping
50+
redirectWhenComplete:
51+
- page: address
52+
53+
- id: type_of_chocolate
54+
name: Type of Chocolate
55+
slug: type-of-chocolate
56+
form: TypeOfChocolateForm
57+
requireResponse:
58+
- page: pizza_or_chocolate
59+
key: food
60+
value: chocolate
61+
redirectWhenComplete:
62+
- page: address
63+
64+
- id: address
65+
name: Enter your address
66+
slug: address
67+
description: We need this to deliver your pizza or chocolate.
68+
form: AddressForm
69+
requireCompletionOfAny:
70+
- pizza_brand
71+
- type_of_chocolate
72+
redirectIfNotComplete: pizza_or_chocolate
73+
redirectWhenComplete:
74+
- page: final_page
75+
76+
- id: final_page
77+
name: Final Page
78+
slug: final-page
79+
description: This is the final page of the flow.
80+
template: forms/final.html
81+
requireCompletionOfAny:
82+
- pizza_brand
83+
- type_of_chocolate
84+
redirectIfNotComplete: pizza_or_chocolate

0 commit comments

Comments
 (0)