textual-wtf 0.8.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.
- textual_wtf/__init__.py +67 -0
- textual_wtf/bound_fields.py +245 -0
- textual_wtf/demo/__init__.py +0 -0
- textual_wtf/demo/advanced_form.py +139 -0
- textual_wtf/demo/basic_form.py +92 -0
- textual_wtf/demo/basic_form.tcss +7 -0
- textual_wtf/demo/launcher.py +124 -0
- textual_wtf/demo/nested_once_form.py +165 -0
- textual_wtf/demo/nested_twice_form.py +170 -0
- textual_wtf/demo/results_screen.py +88 -0
- textual_wtf/demo/user_registration.py +134 -0
- textual_wtf/exceptions.py +25 -0
- textual_wtf/fields.py +264 -0
- textual_wtf/forms.py +433 -0
- textual_wtf/py.typed +0 -0
- textual_wtf/validators.py +40 -0
- textual_wtf/version.py +1 -0
- textual_wtf/widgets.py +165 -0
- textual_wtf-0.8.0.dist-info/METADATA +223 -0
- textual_wtf-0.8.0.dist-info/RECORD +23 -0
- textual_wtf-0.8.0.dist-info/WHEEL +4 -0
- textual_wtf-0.8.0.dist-info/entry_points.txt +2 -0
- textual_wtf-0.8.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Form Composition Example with results display"""
|
|
2
|
+
|
|
3
|
+
from textual.app import App, ComposeResult
|
|
4
|
+
from textual.containers import Container
|
|
5
|
+
from textual.screen import Screen
|
|
6
|
+
from textual.widgets import Static
|
|
7
|
+
from textual_wtf import Form, StringField
|
|
8
|
+
from textual_wtf.demo.results_screen import ResultsDisplayScreen
|
|
9
|
+
|
|
10
|
+
# Define reusable forms
|
|
11
|
+
class AddressForm(Form):
|
|
12
|
+
"""Reusable address form"""
|
|
13
|
+
|
|
14
|
+
street = StringField(label="Street Address", required=True)
|
|
15
|
+
city = StringField(label="City", required=True)
|
|
16
|
+
state = StringField(label="State/Province", required=True)
|
|
17
|
+
postal_code = StringField(label="Postal Code", required=True)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PersonalInfoForm(Form):
|
|
21
|
+
"""Personal information form"""
|
|
22
|
+
|
|
23
|
+
first_name = StringField(label="First Name", required=True)
|
|
24
|
+
last_name = StringField(label="Last Name", required=True)
|
|
25
|
+
email = StringField(label="Email", required=True)
|
|
26
|
+
phone = StringField(label="Phone")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Compose forms together
|
|
30
|
+
class OrderForm(Form):
|
|
31
|
+
"""Order form composed from reusable forms"""
|
|
32
|
+
|
|
33
|
+
# Personal information (no prefix - fields added directly)
|
|
34
|
+
personal = PersonalInfoForm.compose()
|
|
35
|
+
|
|
36
|
+
# Billing address (prefixed - fields added with prefix)
|
|
37
|
+
billing = AddressForm.compose(prefix="billing")
|
|
38
|
+
|
|
39
|
+
# Additional fields
|
|
40
|
+
notes = StringField(label="Order Notes")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ResultsScreen(ResultsDisplayScreen):
|
|
44
|
+
"""Utility Screen to display form results with SQL-style field lookup demo"""
|
|
45
|
+
|
|
46
|
+
def compose(self) -> ComposeResult:
|
|
47
|
+
with Container(id="results-container"):
|
|
48
|
+
yield Static(self.result_title, id="results-title")
|
|
49
|
+
|
|
50
|
+
if self.form:
|
|
51
|
+
lookup_lines = [
|
|
52
|
+
"Field Attribute Access Examples:",
|
|
53
|
+
f" form.street: {self.form.street.label}:",
|
|
54
|
+
f" [{self.form.street.value}]",
|
|
55
|
+
f" form.billing_street: {self.form.billing_street.label}:",
|
|
56
|
+
f" [{self.form.billing_street.value}]",
|
|
57
|
+
f" form.email: {self.form.email.label}",
|
|
58
|
+
f" [{self.form.email.value}]" "",
|
|
59
|
+
"Even though a composed form has a prefix, its fields can be accessed",
|
|
60
|
+
"without the prefix so long as they are unique.",
|
|
61
|
+
]
|
|
62
|
+
yield Static("\n".join(lookup_lines), id="field-lookup")
|
|
63
|
+
|
|
64
|
+
yield from self.show_data()
|
|
65
|
+
yield from self.buttons()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ShopScreen(Screen):
|
|
69
|
+
"""Example application using composed forms"""
|
|
70
|
+
|
|
71
|
+
CSS = """
|
|
72
|
+
Screen {
|
|
73
|
+
align: center middle;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
RenderedForm {
|
|
77
|
+
width: 80;
|
|
78
|
+
height: 100%;
|
|
79
|
+
max-height: 100%;
|
|
80
|
+
border: heavy $accent;
|
|
81
|
+
}
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def compose(self):
|
|
85
|
+
"""Compose the application"""
|
|
86
|
+
initial_data = {
|
|
87
|
+
# Personal info fields (no prefix)
|
|
88
|
+
'first_name': 'John',
|
|
89
|
+
'last_name': 'Doe',
|
|
90
|
+
'email': 'john@example.com',
|
|
91
|
+
'phone': '555-1234',
|
|
92
|
+
# Billing address (prefixed)
|
|
93
|
+
'billing_street': '123 Main St',
|
|
94
|
+
'billing_city': 'Springfield',
|
|
95
|
+
'billing_state': 'IL',
|
|
96
|
+
'billing_postal_code': '62701',
|
|
97
|
+
# Shipping address (prefixed)
|
|
98
|
+
'shipping_street': '456 Oak Ave',
|
|
99
|
+
'shipping_city': 'Chicago',
|
|
100
|
+
'shipping_state': 'IL',
|
|
101
|
+
'shipping_postal_code': '60601',
|
|
102
|
+
# Additional fields
|
|
103
|
+
'same_as_billing': False,
|
|
104
|
+
'notes': 'Please ring doorbell',
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
self.form = OrderForm(
|
|
108
|
+
title="Order Form - Composed from Reusable Forms", data=initial_data
|
|
109
|
+
)
|
|
110
|
+
yield self.form.render()
|
|
111
|
+
|
|
112
|
+
def on_form_submitted(self, event):
|
|
113
|
+
"""Handle form submission"""
|
|
114
|
+
data = event.form.get_data()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def check_reset(cont):
|
|
118
|
+
if cont:
|
|
119
|
+
self.reset_form()
|
|
120
|
+
else:
|
|
121
|
+
self.dismiss(cont)
|
|
122
|
+
|
|
123
|
+
# Pass form for field lookup demo
|
|
124
|
+
self.app.push_screen(
|
|
125
|
+
ResultsScreen("Order Submitted!", data, event.form.form), check_reset
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def on_form_cancelled(self, event):
|
|
129
|
+
"""Handle form cancellation"""
|
|
130
|
+
|
|
131
|
+
def check_reset(cont):
|
|
132
|
+
if cont:
|
|
133
|
+
self.reset_form()
|
|
134
|
+
else:
|
|
135
|
+
self.dismiss(cont)
|
|
136
|
+
|
|
137
|
+
self.app.push_screen(ResultsScreen("Order Cancelled", None), check_reset)
|
|
138
|
+
|
|
139
|
+
def reset_form(self):
|
|
140
|
+
"""Clear form and create fresh one"""
|
|
141
|
+
old_form = self.query_one("RenderedForm")
|
|
142
|
+
old_form.remove()
|
|
143
|
+
|
|
144
|
+
self.form = OrderForm(title="Order Form - Composed from Reusable Forms")
|
|
145
|
+
self.mount(self.form.render())
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def ShopApp(App):
|
|
149
|
+
|
|
150
|
+
def on_mount(self):
|
|
151
|
+
self.app.push_screen(ShopScreen(), callback=self.exit_app)
|
|
152
|
+
|
|
153
|
+
def exit_app(self, result=None) -> None:
|
|
154
|
+
"""Called when BasicFormScreen is dismissed."""
|
|
155
|
+
self.exit(result)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def main():
|
|
159
|
+
"""Run the example"""
|
|
160
|
+
app = ShopApp()
|
|
161
|
+
app.run()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
if __name__ == "__main__":
|
|
165
|
+
main()
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Form Composition Example with results display"""
|
|
2
|
+
from textual.app import App, ComposeResult
|
|
3
|
+
from textual.containers import Container
|
|
4
|
+
from textual.screen import Screen
|
|
5
|
+
from textual.widgets import Static
|
|
6
|
+
from textual_wtf import Form, StringField, BooleanField
|
|
7
|
+
from textual_wtf.demo.results_screen import ResultsDisplayScreen
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Define reusable forms
|
|
11
|
+
class AddressForm(Form):
|
|
12
|
+
"""Reusable address form"""
|
|
13
|
+
street = StringField(label="Street Address", required=True)
|
|
14
|
+
city = StringField(label="City", required=True)
|
|
15
|
+
state = StringField(label="State/Province", required=True)
|
|
16
|
+
postal_code = StringField(label="Postal Code", required=True)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PersonalInfoForm(Form):
|
|
20
|
+
"""Personal information form"""
|
|
21
|
+
first_name = StringField(label="First Name", required=True)
|
|
22
|
+
last_name = StringField(label="Last Name", required=True)
|
|
23
|
+
email = StringField(label="Email", required=True)
|
|
24
|
+
phone = StringField(label="Phone")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Compose forms together
|
|
28
|
+
class OrderForm(Form):
|
|
29
|
+
"""Order form composed from reusable forms"""
|
|
30
|
+
|
|
31
|
+
# Personal information (no prefix - fields added directly)
|
|
32
|
+
personal = PersonalInfoForm.compose()
|
|
33
|
+
|
|
34
|
+
# Billing address (prefixed)
|
|
35
|
+
billing = AddressForm.compose(prefix='billing')
|
|
36
|
+
|
|
37
|
+
# Shipping address (prefixed)
|
|
38
|
+
shipping = AddressForm.compose(prefix='shipping')
|
|
39
|
+
|
|
40
|
+
# Additional fields
|
|
41
|
+
same_as_billing = BooleanField(label="Shipping same as billing")
|
|
42
|
+
notes = StringField(label="Order Notes")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ResultsScreen(ResultsDisplayScreen):
|
|
46
|
+
"""Utility Screen to display form results with SQL-style field lookup demo"""
|
|
47
|
+
|
|
48
|
+
def compose(self) -> ComposeResult:
|
|
49
|
+
with Container(id="results-container"):
|
|
50
|
+
yield Static(self.result_title, id="results-title")
|
|
51
|
+
|
|
52
|
+
yield from self.show_data()
|
|
53
|
+
|
|
54
|
+
# Demonstrate attribute-style field access
|
|
55
|
+
if self.form:
|
|
56
|
+
lookup_lines = [
|
|
57
|
+
"Field Attribute Access Examples:",
|
|
58
|
+
" form.billing_street: "
|
|
59
|
+
+ self.form.billing_street.label,
|
|
60
|
+
f" [{self.form.billing_street.value}]",
|
|
61
|
+
" form.email: "
|
|
62
|
+
+ self.form.email.label
|
|
63
|
+
+ " (unqualified)",
|
|
64
|
+
f" [{self.form.email.value}]",
|
|
65
|
+
"",
|
|
66
|
+
"Note: In this case form.street would raise AmbiguousFieldError",
|
|
67
|
+
" because both billing_street and shipping_street exist",
|
|
68
|
+
]
|
|
69
|
+
yield Static("\n".join(lookup_lines), id="field-lookup")
|
|
70
|
+
|
|
71
|
+
yield from self.buttons()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ShopScreen(Screen):
|
|
75
|
+
"""Example application using composed forms"""
|
|
76
|
+
|
|
77
|
+
CSS = """
|
|
78
|
+
Screen {
|
|
79
|
+
align: center middle;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
RenderedForm {
|
|
83
|
+
width: 80;
|
|
84
|
+
height: 100%;
|
|
85
|
+
max-height: 100%;
|
|
86
|
+
border: heavy $accent;
|
|
87
|
+
}
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def compose(self):
|
|
91
|
+
"""Compose the application"""
|
|
92
|
+
initial_data = {
|
|
93
|
+
# Personal info fields (no prefix)
|
|
94
|
+
'first_name': 'John',
|
|
95
|
+
'last_name': 'Doe',
|
|
96
|
+
'email': 'john@example.com',
|
|
97
|
+
'phone': '555-1234',
|
|
98
|
+
|
|
99
|
+
# Billing address (prefixed)
|
|
100
|
+
'billing_street': '123 Main St',
|
|
101
|
+
'billing_city': 'Springfield',
|
|
102
|
+
'billing_state': 'IL',
|
|
103
|
+
'billing_postal_code': '62701',
|
|
104
|
+
|
|
105
|
+
# Shipping address (prefixed)
|
|
106
|
+
'shipping_street': '456 Oak Ave',
|
|
107
|
+
'shipping_city': 'Chicago',
|
|
108
|
+
'shipping_state': 'IL',
|
|
109
|
+
'shipping_postal_code': '60601',
|
|
110
|
+
|
|
111
|
+
# Additional fields
|
|
112
|
+
'same_as_billing': False,
|
|
113
|
+
'notes': 'Please ring doorbell'
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
self.form = OrderForm(
|
|
117
|
+
title="Order Form - Composed from Reusable Forms",
|
|
118
|
+
data=initial_data
|
|
119
|
+
)
|
|
120
|
+
yield self.form.render()
|
|
121
|
+
|
|
122
|
+
def on_form_submitted(self, event):
|
|
123
|
+
"""Handle form submission"""
|
|
124
|
+
data = event.form.get_data()
|
|
125
|
+
|
|
126
|
+
def check_reset(cont):
|
|
127
|
+
if cont:
|
|
128
|
+
self.reset_form()
|
|
129
|
+
else:
|
|
130
|
+
self.dismiss(cont)
|
|
131
|
+
|
|
132
|
+
# Pass form for field lookup demo
|
|
133
|
+
self.app.push_screen(
|
|
134
|
+
ResultsScreen("Order Submitted!", data, event.form.form), check_reset
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def on_form_cancelled(self, event):
|
|
138
|
+
"""Handle form cancellation"""
|
|
139
|
+
|
|
140
|
+
def check_reset(cont):
|
|
141
|
+
if cont:
|
|
142
|
+
self.reset_form()
|
|
143
|
+
else:
|
|
144
|
+
self.dismiss(cont)
|
|
145
|
+
|
|
146
|
+
self.app.push_screen(ResultsScreen("Order Cancelled", None), check_reset)
|
|
147
|
+
|
|
148
|
+
def reset_form(self):
|
|
149
|
+
"""Clear form and create fresh one"""
|
|
150
|
+
old_form = self.query_one("RenderedForm")
|
|
151
|
+
old_form.remove()
|
|
152
|
+
|
|
153
|
+
self.form = OrderForm(title="Order Form - Composed from Reusable Forms")
|
|
154
|
+
self.mount(self.form.render())
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class ShopApp(App):
|
|
158
|
+
|
|
159
|
+
def compose(self):
|
|
160
|
+
yield ShopScreen()
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def main():
|
|
164
|
+
"""Run the example"""
|
|
165
|
+
app = ShopApp()
|
|
166
|
+
app.run()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
if __name__ == "__main__":
|
|
170
|
+
main()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from textual.app import ComposeResult, on
|
|
2
|
+
from textual.containers import Center
|
|
3
|
+
from textual.screen import Screen
|
|
4
|
+
from textual.widgets import Button, Static
|
|
5
|
+
|
|
6
|
+
class ResultsDisplayScreen(Screen):
|
|
7
|
+
"""
|
|
8
|
+
Screen to display form results.
|
|
9
|
+
|
|
10
|
+
Subclasses must add a compose() method to implement the visual rendering.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
CSS = """
|
|
14
|
+
ResultsScreen {
|
|
15
|
+
align: center middle;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#results-container {
|
|
19
|
+
width: 80%;
|
|
20
|
+
height: auto;
|
|
21
|
+
border: heavy green;
|
|
22
|
+
padding: 1;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#results-title {
|
|
26
|
+
background: green;
|
|
27
|
+
color: white;
|
|
28
|
+
width: 100%;
|
|
29
|
+
height: 1;
|
|
30
|
+
content-align: center middle;
|
|
31
|
+
margin-bottom: 1;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#results-data {
|
|
35
|
+
background: $panel;
|
|
36
|
+
padding: 1;
|
|
37
|
+
margin: 1 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#field-lookup {
|
|
41
|
+
background: $panel;
|
|
42
|
+
padding: 1;
|
|
43
|
+
margin: 1 0;
|
|
44
|
+
border: solid blue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#buttons {
|
|
48
|
+
align: center middle;
|
|
49
|
+
margin-top: 1;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
Button {
|
|
53
|
+
margin: 0 1;
|
|
54
|
+
}
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, title: str, data: dict = None, form=None):
|
|
58
|
+
super().__init__()
|
|
59
|
+
self.result_title = title
|
|
60
|
+
self.data = data
|
|
61
|
+
self.form = form
|
|
62
|
+
|
|
63
|
+
def compose(self) -> ComposeResult:
|
|
64
|
+
raise NotImplementedError("ResultsScreen subclasses must define a compose() method")
|
|
65
|
+
|
|
66
|
+
@on(Button.Pressed, "#new")
|
|
67
|
+
def new_pressed(self, event: Button.Pressed):
|
|
68
|
+
self.dismiss(True)
|
|
69
|
+
|
|
70
|
+
@on(Button.Pressed, "#exit")
|
|
71
|
+
def exit_pressed(self, event: Button.Pressed):
|
|
72
|
+
self.dismiss(False)
|
|
73
|
+
|
|
74
|
+
def show_data(self):
|
|
75
|
+
|
|
76
|
+
if self.data:
|
|
77
|
+
# Format data nicely
|
|
78
|
+
lines = []
|
|
79
|
+
for key, value in self.data.items():
|
|
80
|
+
lines.append(f"{key}: {value}")
|
|
81
|
+
yield Static("\n".join(lines), id="results-data")
|
|
82
|
+
|
|
83
|
+
def buttons(self) -> ComposeResult:
|
|
84
|
+
with Center(id="buttons"):
|
|
85
|
+
yield Button("New Form", variant="primary", id="new")
|
|
86
|
+
yield Button("Exit", id="exit")
|
|
87
|
+
|
|
88
|
+
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""User registration form example with results display"""
|
|
2
|
+
from textual.app import App, ComposeResult
|
|
3
|
+
from textual.containers import Container, VerticalScroll
|
|
4
|
+
from textual.screen import Screen
|
|
5
|
+
from textual.widgets import Static
|
|
6
|
+
from textual_wtf import Form, StringField, BooleanField
|
|
7
|
+
from textual_wtf.validators import EmailValidator
|
|
8
|
+
from textual_wtf.demo.results_screen import ResultsDisplayScreen
|
|
9
|
+
|
|
10
|
+
class RegistrationForm(Form):
|
|
11
|
+
"""User registration form"""
|
|
12
|
+
username = StringField(
|
|
13
|
+
label="Username",
|
|
14
|
+
required=True,
|
|
15
|
+
help_text="Choose a unique username"
|
|
16
|
+
)
|
|
17
|
+
email = StringField(
|
|
18
|
+
label="Email Address",
|
|
19
|
+
required=True,
|
|
20
|
+
validators=[EmailValidator()]
|
|
21
|
+
)
|
|
22
|
+
full_name = StringField(
|
|
23
|
+
label="Full Name",
|
|
24
|
+
required=True
|
|
25
|
+
)
|
|
26
|
+
agree_terms = BooleanField(
|
|
27
|
+
label="I agree to the terms and conditions",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ResultsScreen(ResultsDisplayScreen):
|
|
32
|
+
"""Utility Screen to display form results"""
|
|
33
|
+
|
|
34
|
+
def compose(self) -> ComposeResult:
|
|
35
|
+
with Container(id="results-container"):
|
|
36
|
+
yield Static(self.result_title, id="results-title")
|
|
37
|
+
|
|
38
|
+
yield from self.show_data()
|
|
39
|
+
yield from self.buttons()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class RegistrationScreen(Screen):
|
|
43
|
+
"""User registration application"""
|
|
44
|
+
|
|
45
|
+
TITLE = "User Registration"
|
|
46
|
+
BINDINGS = [
|
|
47
|
+
("q", "quit", "Quit"),
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
CSS = """
|
|
51
|
+
Container {
|
|
52
|
+
align: center middle;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#info {
|
|
56
|
+
width: 60;
|
|
57
|
+
height: auto;
|
|
58
|
+
margin: 1 0;
|
|
59
|
+
padding: 1;
|
|
60
|
+
border: solid blue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
RenderedForm {
|
|
64
|
+
width: 60;
|
|
65
|
+
height: auto;
|
|
66
|
+
}
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def compose(self) -> ComposeResult:
|
|
70
|
+
with Container():
|
|
71
|
+
with VerticalScroll():
|
|
72
|
+
yield Static(
|
|
73
|
+
"Welcome! Please fill out the registration form below.",
|
|
74
|
+
id="info"
|
|
75
|
+
)
|
|
76
|
+
self.form = RegistrationForm(title="Create Account")
|
|
77
|
+
yield self.form.render()
|
|
78
|
+
|
|
79
|
+
def on_form_submitted(self, event: Form.Submitted):
|
|
80
|
+
"""Handle successful registration"""
|
|
81
|
+
data = event.form.get_data()
|
|
82
|
+
|
|
83
|
+
if not data['agree_terms']:
|
|
84
|
+
self.notify(
|
|
85
|
+
"You must agree to the terms and conditions",
|
|
86
|
+
severity="error"
|
|
87
|
+
)
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
def check_reset(cont):
|
|
91
|
+
if cont:
|
|
92
|
+
self.reset_form()
|
|
93
|
+
else:
|
|
94
|
+
self.dismiss(cont)
|
|
95
|
+
|
|
96
|
+
self.app.push_screen(
|
|
97
|
+
ResultsScreen("Registration Successful!", data), check_reset
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def on_form_cancelled(self, event: Form.Cancelled):
|
|
101
|
+
"""Handle registration cancellation"""
|
|
102
|
+
|
|
103
|
+
def check_reset(cont):
|
|
104
|
+
if cont:
|
|
105
|
+
self.reset_form()
|
|
106
|
+
else:
|
|
107
|
+
self.dismiss(cont)
|
|
108
|
+
|
|
109
|
+
self.app.push_screen(ResultsScreen("Registration Cancelled", None), check_reset)
|
|
110
|
+
|
|
111
|
+
def reset_form(self):
|
|
112
|
+
"""Clear form and create fresh one"""
|
|
113
|
+
old_form = self.query_one("RenderedForm")
|
|
114
|
+
old_form.remove()
|
|
115
|
+
self.form = RegistrationForm(title="Create Account")
|
|
116
|
+
self.query_one("VerticalScroll").mount(self.form.render())
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class RegistrationApp(App):
|
|
120
|
+
|
|
121
|
+
def on_mount(self):
|
|
122
|
+
self.app.push_screen(RegistrationScreen(), callback=self.exit_app)
|
|
123
|
+
|
|
124
|
+
def exit_app(self, result=None) -> None:
|
|
125
|
+
"""Called when Screen is dismissed."""
|
|
126
|
+
self.exit(result)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def main():
|
|
130
|
+
RegistrationApp().run()
|
|
131
|
+
|
|
132
|
+
if __name__ == "__main__":
|
|
133
|
+
main()
|
|
134
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Exceptions for textual-wtf"""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ValidationError(Exception):
|
|
5
|
+
"""Raised when field validation fails"""
|
|
6
|
+
|
|
7
|
+
def __init__(self, message: str, code: str = None):
|
|
8
|
+
self.message = message
|
|
9
|
+
self.code = code
|
|
10
|
+
super().__init__(self.message)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FieldError(Exception):
|
|
14
|
+
"""Raised when there's an error with field configuration"""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FormError(Exception):
|
|
19
|
+
"""Raised when there's an error with form configuration"""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AmbiguousFieldError(FieldError):
|
|
24
|
+
"""Raised when an unqualified field name matches multiple fields"""
|
|
25
|
+
pass
|