2017-02-13 01:21:26 +00:00
|
|
|
# Copyright 2017 Canonical, Ltd.
|
|
|
|
#
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU Affero General Public License as
|
|
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
|
|
# License, or (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU Affero General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
2018-06-20 22:55:53 +00:00
|
|
|
import abc
|
2017-09-05 02:25:21 +00:00
|
|
|
import logging
|
2018-04-09 04:10:55 +00:00
|
|
|
from urllib.parse import urlparse
|
2017-09-05 02:25:21 +00:00
|
|
|
|
2017-02-13 01:21:26 +00:00
|
|
|
from urwid import (
|
2018-11-29 08:58:17 +00:00
|
|
|
CheckBox,
|
2017-10-05 03:19:27 +00:00
|
|
|
connect_signal,
|
2017-02-13 01:21:26 +00:00
|
|
|
delegate_to_widget_mixin,
|
|
|
|
emit_signal,
|
|
|
|
MetaSignals,
|
2018-11-29 08:58:17 +00:00
|
|
|
Padding as UrwidPadding,
|
2019-12-14 09:28:30 +00:00
|
|
|
RadioButton,
|
2017-02-13 01:21:26 +00:00
|
|
|
Text,
|
|
|
|
WidgetDecoration,
|
|
|
|
)
|
|
|
|
|
|
|
|
from subiquitycore.ui.buttons import cancel_btn, done_btn
|
2018-06-21 21:38:18 +00:00
|
|
|
from subiquitycore.ui.container import (
|
2019-12-12 09:22:49 +00:00
|
|
|
Pile,
|
2018-06-21 21:38:18 +00:00
|
|
|
WidgetWrap,
|
|
|
|
)
|
2017-03-21 01:39:23 +00:00
|
|
|
from subiquitycore.ui.interactive import (
|
|
|
|
PasswordEditor,
|
|
|
|
IntegerEditor,
|
|
|
|
StringEditor,
|
2019-08-07 20:35:35 +00:00
|
|
|
EmailEditor,
|
2017-03-21 01:39:23 +00:00
|
|
|
)
|
2018-03-14 01:20:53 +00:00
|
|
|
from subiquitycore.ui.selector import Selector
|
2018-06-21 08:56:59 +00:00
|
|
|
from subiquitycore.ui.table import (
|
|
|
|
ColSpec,
|
|
|
|
TablePile,
|
|
|
|
TableRow,
|
|
|
|
)
|
2018-06-20 04:20:59 +00:00
|
|
|
from subiquitycore.ui.utils import (
|
|
|
|
button_pile,
|
|
|
|
Color,
|
|
|
|
disabled,
|
|
|
|
screen,
|
|
|
|
)
|
2018-11-29 08:58:17 +00:00
|
|
|
from subiquitycore.ui.width import (
|
|
|
|
widget_width,
|
|
|
|
)
|
2017-02-13 01:21:26 +00:00
|
|
|
|
2018-11-29 09:35:31 +00:00
|
|
|
|
2017-09-05 02:25:21 +00:00
|
|
|
log = logging.getLogger("subiquitycore.ui.form")
|
|
|
|
|
2017-09-14 23:04:38 +00:00
|
|
|
|
2019-12-12 09:15:20 +00:00
|
|
|
# Passing NO_CAPTION as the caption of a field supresses the caption
|
|
|
|
# entirely so the field occupies the full width of the form.
|
|
|
|
NO_CAPTION = object()
|
|
|
|
|
2019-12-12 09:19:23 +00:00
|
|
|
# Passing NO_HELP as the help of a field supresses the gap under a
|
|
|
|
# field where the help would go. This means there is nowhere to put
|
|
|
|
# validation failures, so don't use this on fields that have any
|
|
|
|
# validation at all.
|
|
|
|
NO_HELP = object()
|
|
|
|
|
2019-12-12 09:15:20 +00:00
|
|
|
|
2018-05-22 16:11:32 +00:00
|
|
|
class Toggleable(delegate_to_widget_mixin('_original_widget'),
|
|
|
|
WidgetDecoration):
|
2017-02-13 01:21:26 +00:00
|
|
|
|
2018-06-25 21:59:00 +00:00
|
|
|
has_original_width = True
|
|
|
|
|
2017-09-14 23:04:38 +00:00
|
|
|
def __init__(self, original):
|
2017-02-13 01:21:26 +00:00
|
|
|
self.original = original
|
2018-07-03 23:43:35 +00:00
|
|
|
self._enabled = False
|
|
|
|
self.enabled = True
|
2017-02-13 01:21:26 +00:00
|
|
|
|
2018-07-03 23:43:35 +00:00
|
|
|
@property
|
|
|
|
def enabled(self):
|
|
|
|
return self._enabled
|
2017-02-13 01:21:26 +00:00
|
|
|
|
2018-07-03 23:43:35 +00:00
|
|
|
@enabled.setter
|
|
|
|
def enabled(self, val):
|
|
|
|
if val and not self._enabled:
|
|
|
|
self.original_widget = self.original
|
|
|
|
elif not val and self._enabled:
|
2018-06-20 04:20:59 +00:00
|
|
|
self.original_widget = disabled(self.original)
|
2018-07-03 23:43:35 +00:00
|
|
|
self._enabled = val
|
2017-02-13 01:21:26 +00:00
|
|
|
|
2017-09-14 23:04:38 +00:00
|
|
|
|
2017-02-13 01:21:26 +00:00
|
|
|
class _Validator(WidgetWrap):
|
|
|
|
|
|
|
|
def __init__(self, field, w):
|
|
|
|
self.field = field
|
|
|
|
super().__init__(w)
|
|
|
|
|
2018-11-29 08:58:17 +00:00
|
|
|
def get_natural_width(self):
|
|
|
|
return widget_width(self._w)
|
|
|
|
|
2017-02-13 01:21:26 +00:00
|
|
|
def lost_focus(self):
|
2017-10-05 03:16:31 +00:00
|
|
|
self.field.showing_extra = False
|
2018-07-16 00:34:18 +00:00
|
|
|
lf = getattr(self._w.base_widget, 'lost_focus', None)
|
2017-10-05 03:16:31 +00:00
|
|
|
if lf is not None:
|
|
|
|
lf()
|
2017-02-13 01:21:26 +00:00
|
|
|
self.field.validate()
|
|
|
|
|
|
|
|
|
2017-10-06 02:39:14 +00:00
|
|
|
class WantsToKnowFormField(object):
|
2017-10-05 09:14:44 +00:00
|
|
|
"""A marker class."""
|
2017-10-06 02:39:14 +00:00
|
|
|
def set_bound_form_field(self, bff):
|
|
|
|
self.bff = bff
|
2017-10-05 09:14:44 +00:00
|
|
|
|
2018-05-22 16:11:32 +00:00
|
|
|
|
2018-06-21 08:56:59 +00:00
|
|
|
form_colspecs = {1: ColSpec(pack=False)}
|
|
|
|
|
|
|
|
|
2017-02-13 01:21:26 +00:00
|
|
|
class BoundFormField(object):
|
|
|
|
|
2017-02-14 02:16:12 +00:00
|
|
|
def __init__(self, field, form, widget):
|
2017-02-13 01:21:26 +00:00
|
|
|
self.field = field
|
|
|
|
self.form = form
|
2018-06-20 23:33:48 +00:00
|
|
|
self.widget = widget
|
|
|
|
|
2017-02-13 01:21:26 +00:00
|
|
|
self.in_error = False
|
2017-02-13 02:12:04 +00:00
|
|
|
self._enabled = True
|
2018-06-20 23:33:48 +00:00
|
|
|
self._help = None
|
2017-02-13 02:50:39 +00:00
|
|
|
self.showing_extra = False
|
2018-06-20 23:33:48 +00:00
|
|
|
|
2018-06-21 08:56:59 +00:00
|
|
|
self._build_table()
|
2018-06-20 23:33:48 +00:00
|
|
|
|
2017-10-05 03:19:27 +00:00
|
|
|
if 'change' in getattr(widget, 'signals', []):
|
|
|
|
connect_signal(widget, 'change', self._change)
|
2017-10-06 02:39:14 +00:00
|
|
|
if isinstance(widget, WantsToKnowFormField):
|
|
|
|
widget.set_bound_form_field(self)
|
2017-02-13 01:21:26 +00:00
|
|
|
|
2022-06-08 08:28:44 +00:00
|
|
|
def is_in_error(self) -> bool:
|
2022-06-09 07:54:18 +00:00
|
|
|
""" Tells whether this field is in error. """
|
|
|
|
return self.in_error
|
2022-06-08 08:28:44 +00:00
|
|
|
|
2018-06-21 08:56:59 +00:00
|
|
|
def _build_table(self):
|
2018-06-20 23:33:48 +00:00
|
|
|
widget = self.widget
|
|
|
|
if self.field.takes_default_style:
|
|
|
|
widget = Color.string_input(widget)
|
|
|
|
|
2019-12-12 09:19:23 +00:00
|
|
|
if self.help is not NO_HELP:
|
|
|
|
self.under_text = Text(self.help)
|
|
|
|
else:
|
|
|
|
self.under_text = Text("")
|
2019-12-12 09:15:20 +00:00
|
|
|
if self.field.caption is NO_CAPTION:
|
|
|
|
first_row = [(2, _Validator(self, widget))]
|
|
|
|
second_row = [(2, self.under_text)]
|
2018-11-29 08:58:17 +00:00
|
|
|
else:
|
2020-05-06 09:07:03 +00:00
|
|
|
self.caption_text = Text(_(self.field.caption))
|
2019-12-12 09:15:20 +00:00
|
|
|
|
|
|
|
if self.field.caption_first:
|
|
|
|
self.caption_text.align = 'right'
|
|
|
|
first_row = [self.caption_text, _Validator(self, widget)]
|
|
|
|
else:
|
|
|
|
first_row = [
|
|
|
|
_Validator(
|
|
|
|
self,
|
|
|
|
UrwidPadding(
|
|
|
|
widget, align='right',
|
|
|
|
width=widget_width(widget))),
|
|
|
|
self.caption_text,
|
|
|
|
]
|
|
|
|
second_row = [Text(""), self.under_text]
|
2018-11-29 08:58:17 +00:00
|
|
|
|
2019-12-12 09:19:23 +00:00
|
|
|
rows = [first_row]
|
|
|
|
if self.help is not NO_HELP:
|
|
|
|
rows.append(second_row)
|
|
|
|
|
|
|
|
self._rows = [Toggleable(TableRow(row)) for row in rows]
|
2018-06-20 23:33:48 +00:00
|
|
|
|
2018-06-21 08:56:59 +00:00
|
|
|
self._table = TablePile(self._rows, spacing=2, colspecs=form_colspecs)
|
2018-06-20 23:33:48 +00:00
|
|
|
|
2017-02-13 01:21:26 +00:00
|
|
|
def clean(self, value):
|
|
|
|
cleaner = getattr(self.form, "clean_" + self.field.name, None)
|
|
|
|
if cleaner is not None:
|
|
|
|
value = cleaner(value)
|
|
|
|
return value
|
|
|
|
|
2017-10-05 03:19:27 +00:00
|
|
|
def _change(self, sender, new_val):
|
2019-07-24 00:27:59 +00:00
|
|
|
if self.in_error or self.showing_extra:
|
2017-10-05 03:19:27 +00:00
|
|
|
self.showing_extra = False
|
|
|
|
# the validator will likely inspect self.value to decide
|
|
|
|
# if the new input is valid. So self.value had better
|
|
|
|
# return the new value and we stuff it into tmpval to do
|
|
|
|
# this. It's a bit of a hack but oh well...
|
|
|
|
self.tmpval = new_val
|
|
|
|
r = self._validate()
|
|
|
|
del self.tmpval
|
|
|
|
if r is not None:
|
|
|
|
return
|
|
|
|
self.in_error = False
|
2019-12-12 09:19:23 +00:00
|
|
|
if not self.showing_extra and self.help is not NO_HELP:
|
2018-06-20 23:33:48 +00:00
|
|
|
self.under_text.set_text(self.help)
|
2017-10-05 03:19:27 +00:00
|
|
|
self.form.validated()
|
|
|
|
|
2017-02-13 01:21:26 +00:00
|
|
|
def _validate(self):
|
2017-02-13 02:12:04 +00:00
|
|
|
if not self._enabled:
|
2017-02-13 01:59:25 +00:00
|
|
|
return
|
2017-02-13 01:21:26 +00:00
|
|
|
try:
|
2017-10-05 02:45:16 +00:00
|
|
|
self.value
|
2017-02-13 01:21:26 +00:00
|
|
|
except ValueError as e:
|
|
|
|
return str(e)
|
|
|
|
validator = getattr(self.form, "validate_" + self.field.name, None)
|
|
|
|
if validator is not None:
|
|
|
|
return validator()
|
|
|
|
|
2018-06-20 23:33:48 +00:00
|
|
|
def validate(self, show_error=True):
|
2017-10-05 02:05:45 +00:00
|
|
|
# cleaning/validation can call show_extra to add an
|
|
|
|
# informative message. We record this by having show_extra to
|
|
|
|
# set showing_extra so we don't immediately replace this
|
|
|
|
# message with the widget's help in the case that validation
|
|
|
|
# succeeds.
|
2017-02-13 01:21:26 +00:00
|
|
|
r = self._validate()
|
|
|
|
if r is None:
|
|
|
|
self.in_error = False
|
2019-12-12 09:19:23 +00:00
|
|
|
if not self.showing_extra and self.help is not NO_HELP:
|
2018-06-20 23:33:48 +00:00
|
|
|
self.under_text.set_text(self.help)
|
2017-02-13 01:21:26 +00:00
|
|
|
else:
|
|
|
|
self.in_error = True
|
2018-06-20 23:33:48 +00:00
|
|
|
if show_error:
|
|
|
|
self.show_extra(('info_error', r))
|
2017-02-13 01:21:26 +00:00
|
|
|
self.form.validated()
|
|
|
|
|
2017-10-05 02:05:45 +00:00
|
|
|
def show_extra(self, extra_markup):
|
2017-02-13 02:50:39 +00:00
|
|
|
self.showing_extra = True
|
2018-06-20 23:33:48 +00:00
|
|
|
self.under_text.set_text(extra_markup)
|
2017-02-13 01:21:26 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def value(self):
|
2017-10-05 03:19:27 +00:00
|
|
|
return self.clean(getattr(self, 'tmpval', self.widget.value))
|
2017-02-13 01:21:26 +00:00
|
|
|
|
|
|
|
@value.setter
|
|
|
|
def value(self, val):
|
|
|
|
self.widget.value = val
|
|
|
|
|
|
|
|
@property
|
|
|
|
def help(self):
|
|
|
|
if self._help is not None:
|
|
|
|
return self._help
|
2017-10-05 02:05:45 +00:00
|
|
|
elif self.field.help is not None:
|
2020-05-06 09:07:03 +00:00
|
|
|
if isinstance(self.field.help, str):
|
|
|
|
return _(self.field.help)
|
|
|
|
else:
|
|
|
|
return self.field.help
|
2017-10-05 02:05:45 +00:00
|
|
|
else:
|
|
|
|
return ""
|
2017-02-13 01:21:26 +00:00
|
|
|
|
|
|
|
@help.setter
|
|
|
|
def help(self, val):
|
2018-06-20 23:33:48 +00:00
|
|
|
if val is None:
|
|
|
|
val = ""
|
2017-02-13 01:21:26 +00:00
|
|
|
self._help = val
|
2018-06-20 23:33:48 +00:00
|
|
|
self.under_text.set_text(val)
|
2017-02-13 01:21:26 +00:00
|
|
|
|
2017-02-13 01:48:53 +00:00
|
|
|
@property
|
|
|
|
def caption(self):
|
2018-06-20 23:33:48 +00:00
|
|
|
return self.caption_text.text
|
2017-02-13 01:48:53 +00:00
|
|
|
|
|
|
|
@caption.setter
|
|
|
|
def caption(self, val):
|
2018-06-20 23:33:48 +00:00
|
|
|
self.caption_text.set_text(val)
|
2017-02-13 01:59:25 +00:00
|
|
|
|
2017-02-13 02:12:04 +00:00
|
|
|
@property
|
|
|
|
def enabled(self):
|
|
|
|
return self._enabled
|
|
|
|
|
|
|
|
@enabled.setter
|
|
|
|
def enabled(self, val):
|
2018-07-03 23:43:35 +00:00
|
|
|
self._enabled = val
|
|
|
|
for row in self._rows:
|
|
|
|
row.enabled = val
|
2017-02-13 01:59:25 +00:00
|
|
|
|
2017-02-13 01:21:26 +00:00
|
|
|
|
2022-06-09 07:54:18 +00:00
|
|
|
class BoundSubFormField(BoundFormField):
|
|
|
|
def is_in_error(self):
|
|
|
|
""" Tells whether this field is in error. We will also check if the
|
|
|
|
subform (if enabled) reports an error.
|
|
|
|
"""
|
|
|
|
if super().is_in_error():
|
|
|
|
return True
|
|
|
|
|
|
|
|
if not self._enabled:
|
|
|
|
return False
|
|
|
|
|
|
|
|
return self.widget.form.has_validation_error()
|
|
|
|
|
|
|
|
|
|
|
|
class FormField(abc.ABC):
|
|
|
|
|
|
|
|
next_index = 0
|
|
|
|
takes_default_style = True
|
|
|
|
caption_first = True
|
|
|
|
bound_field_class = BoundFormField
|
|
|
|
|
|
|
|
def __init__(self, caption=None, help=None):
|
|
|
|
self.caption = caption
|
|
|
|
self.help = help
|
|
|
|
self.index = FormField.next_index
|
|
|
|
FormField.next_index += 1
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
def _make_widget(self, form):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def bind(self, form):
|
|
|
|
widget = self._make_widget(form)
|
|
|
|
return self.bound_field_class(self, form, widget)
|
|
|
|
|
|
|
|
|
2017-02-13 02:37:29 +00:00
|
|
|
def simple_field(widget_maker):
|
|
|
|
class Field(FormField):
|
2017-02-14 02:16:12 +00:00
|
|
|
def _make_widget(self, form):
|
|
|
|
return widget_maker()
|
2017-02-13 02:37:29 +00:00
|
|
|
return Field
|
2017-02-13 01:48:53 +00:00
|
|
|
|
|
|
|
|
2017-02-14 02:16:12 +00:00
|
|
|
StringField = simple_field(StringEditor)
|
2017-03-21 01:39:23 +00:00
|
|
|
PasswordField = simple_field(PasswordEditor)
|
2017-02-14 02:16:12 +00:00
|
|
|
IntegerField = simple_field(IntegerEditor)
|
2019-08-07 20:35:35 +00:00
|
|
|
EmailField = simple_field(EmailEditor)
|
2017-02-13 01:48:53 +00:00
|
|
|
|
2018-03-14 01:20:53 +00:00
|
|
|
|
2019-12-14 09:28:30 +00:00
|
|
|
class RadioButtonEditor(RadioButton):
|
|
|
|
|
|
|
|
reserve_columns = 3
|
|
|
|
|
|
|
|
@property
|
|
|
|
def value(self):
|
|
|
|
return self.state
|
|
|
|
|
|
|
|
@value.setter
|
|
|
|
def value(self, val):
|
|
|
|
self.state = val
|
|
|
|
|
|
|
|
|
|
|
|
class RadioButtonField(FormField):
|
|
|
|
|
|
|
|
caption_first = False
|
|
|
|
takes_default_style = False
|
|
|
|
|
|
|
|
def __init__(self, group, caption=None, help=None):
|
|
|
|
if group is None:
|
|
|
|
group = []
|
|
|
|
group.append(self)
|
|
|
|
self.group = group
|
|
|
|
super().__init__(caption, help)
|
|
|
|
|
|
|
|
def _make_widget(self, form):
|
|
|
|
for bf in form._fields:
|
|
|
|
if bf.field in self.group:
|
|
|
|
group = bf.widget.group
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
group = []
|
|
|
|
return RadioButtonEditor(group, "")
|
|
|
|
|
|
|
|
|
2018-04-09 02:59:53 +00:00
|
|
|
class URLEditor(StringEditor, WantsToKnowFormField):
|
2018-04-09 04:10:55 +00:00
|
|
|
def __init__(self, allowed_schemes=frozenset(['http', 'https'])):
|
|
|
|
self.allowed_schemes = allowed_schemes
|
|
|
|
super().__init__()
|
|
|
|
|
|
|
|
@StringEditor.value.getter
|
|
|
|
def value(self):
|
|
|
|
v = self.get_edit_text()
|
|
|
|
if v == "":
|
|
|
|
return v
|
|
|
|
parsed = urlparse(v)
|
|
|
|
if parsed.scheme not in self.allowed_schemes:
|
|
|
|
schemes = []
|
|
|
|
for s in sorted(self.allowed_schemes):
|
|
|
|
schemes.append(s)
|
|
|
|
if len(schemes) > 2:
|
|
|
|
schemes = ", ".join(schemes[:-1]) + _(", or ") + schemes[-1]
|
|
|
|
elif len(schemes) == 2:
|
|
|
|
schemes = schemes[0] + _(" or ") + schemes[1]
|
|
|
|
else:
|
|
|
|
schemes = schemes[0]
|
2020-05-08 03:40:59 +00:00
|
|
|
raise ValueError(
|
|
|
|
_("This field must be a {schemes} URL.").format(
|
|
|
|
schemes=schemes))
|
2018-04-09 04:10:55 +00:00
|
|
|
return v
|
2018-04-09 02:59:53 +00:00
|
|
|
|
2018-05-24 18:05:45 +00:00
|
|
|
|
2018-04-09 02:59:53 +00:00
|
|
|
URLField = simple_field(URLEditor)
|
|
|
|
|
|
|
|
|
2018-03-14 01:20:53 +00:00
|
|
|
class ChoiceField(FormField):
|
|
|
|
|
2018-06-25 11:34:44 +00:00
|
|
|
takes_default_style = False
|
|
|
|
|
2018-03-14 01:20:53 +00:00
|
|
|
def __init__(self, caption=None, help=None, choices=[]):
|
|
|
|
super().__init__(caption, help)
|
|
|
|
self.choices = choices
|
|
|
|
|
|
|
|
def _make_widget(self, form):
|
|
|
|
return Selector(self.choices)
|
|
|
|
|
|
|
|
|
2018-06-21 21:21:41 +00:00
|
|
|
class ReadOnlyWidget(Text):
|
|
|
|
|
|
|
|
@property
|
|
|
|
def value(self):
|
|
|
|
return self.text
|
|
|
|
|
|
|
|
@value.setter
|
|
|
|
def value(self, val):
|
|
|
|
self.set_text(val)
|
|
|
|
|
|
|
|
|
|
|
|
class ReadOnlyField(FormField):
|
|
|
|
|
|
|
|
takes_default_style = False
|
|
|
|
|
|
|
|
def _make_widget(self, form):
|
|
|
|
return ReadOnlyWidget("")
|
|
|
|
|
|
|
|
|
2018-11-29 08:58:17 +00:00
|
|
|
class CheckBoxEditor(CheckBox):
|
|
|
|
|
|
|
|
reserve_columns = 3
|
|
|
|
|
|
|
|
@property
|
|
|
|
def value(self):
|
|
|
|
return self.state
|
|
|
|
|
|
|
|
@value.setter
|
|
|
|
def value(self, val):
|
|
|
|
self.state = val
|
|
|
|
|
|
|
|
|
|
|
|
class BooleanField(FormField):
|
|
|
|
|
|
|
|
caption_first = False
|
|
|
|
takes_default_style = False
|
|
|
|
|
|
|
|
def _make_widget(self, form):
|
|
|
|
return CheckBoxEditor('')
|
|
|
|
|
|
|
|
|
2017-02-13 01:21:26 +00:00
|
|
|
class MetaForm(MetaSignals):
|
|
|
|
|
|
|
|
def __init__(self, name, bases, attrs):
|
|
|
|
super().__init__(name, bases, attrs)
|
|
|
|
_unbound_fields = []
|
|
|
|
for k, v in attrs.items():
|
|
|
|
if isinstance(v, FormField):
|
|
|
|
v.name = k
|
|
|
|
if v.caption is None:
|
|
|
|
v.caption = k + ":"
|
|
|
|
_unbound_fields.append(v)
|
2018-05-22 16:11:32 +00:00
|
|
|
_unbound_fields.sort(key=lambda f: f.index)
|
2017-02-13 01:21:26 +00:00
|
|
|
self._unbound_fields = _unbound_fields
|
|
|
|
|
|
|
|
|
|
|
|
class Form(object, metaclass=MetaForm):
|
|
|
|
|
|
|
|
signals = ['submit', 'cancel']
|
|
|
|
|
2017-09-15 00:18:50 +00:00
|
|
|
ok_label = _("Done")
|
2018-01-11 23:56:24 +00:00
|
|
|
cancel_label = _("Cancel")
|
2017-02-13 01:21:26 +00:00
|
|
|
|
2017-09-04 23:58:49 +00:00
|
|
|
def __init__(self, initial={}):
|
2018-05-22 16:11:32 +00:00
|
|
|
self.done_btn = Toggleable(done_btn(_(self.ok_label),
|
|
|
|
on_press=self._click_done))
|
|
|
|
self.cancel_btn = Toggleable(cancel_btn(_(self.cancel_label),
|
|
|
|
on_press=self._click_cancel))
|
2017-09-20 03:20:32 +00:00
|
|
|
self.buttons = button_pile([self.done_btn, self.cancel_btn])
|
2017-02-13 01:21:26 +00:00
|
|
|
self._fields = []
|
|
|
|
for field in self._unbound_fields:
|
|
|
|
bf = field.bind(self)
|
|
|
|
setattr(self, bf.field.name, bf)
|
|
|
|
self._fields.append(bf)
|
2017-09-04 23:58:49 +00:00
|
|
|
if field.name in initial:
|
|
|
|
bf.value = initial[field.name]
|
2017-11-07 22:59:13 +00:00
|
|
|
for bf in self._fields:
|
2018-06-20 23:33:48 +00:00
|
|
|
bf.validate(show_error=False)
|
2017-11-07 22:59:13 +00:00
|
|
|
self.validated()
|
2017-02-13 01:21:26 +00:00
|
|
|
|
2018-06-26 22:32:38 +00:00
|
|
|
def enter_data(self, data):
|
|
|
|
for bf in self._fields:
|
|
|
|
if bf.field.name in data:
|
|
|
|
bf.field.value = data[bf.field.name]
|
|
|
|
|
2017-02-13 01:21:26 +00:00
|
|
|
def _click_done(self, sender):
|
|
|
|
emit_signal(self, 'submit', self)
|
|
|
|
|
|
|
|
def _click_cancel(self, sender):
|
|
|
|
emit_signal(self, 'cancel', self)
|
|
|
|
|
2017-03-10 02:46:21 +00:00
|
|
|
def remove_field(self, field_name):
|
|
|
|
new_fields = []
|
|
|
|
for bf in self._fields:
|
|
|
|
if bf.field.name != field_name:
|
|
|
|
new_fields.append(bf)
|
|
|
|
self._fields[:] = new_fields
|
|
|
|
|
2018-05-06 22:57:35 +00:00
|
|
|
def as_rows(self):
|
2018-06-21 08:56:59 +00:00
|
|
|
if len(self._fields) == 0:
|
|
|
|
return []
|
|
|
|
t0 = self._fields[0]._table
|
|
|
|
rows = [t0]
|
|
|
|
for field in self._fields[1:]:
|
2018-02-20 01:31:40 +00:00
|
|
|
rows.append(Text(""))
|
2018-06-21 08:56:59 +00:00
|
|
|
t = field._table
|
|
|
|
t0.bind(t)
|
|
|
|
rows.append(t)
|
2018-03-12 08:43:56 +00:00
|
|
|
return rows
|
2017-02-13 01:21:26 +00:00
|
|
|
|
2019-12-12 09:15:20 +00:00
|
|
|
def as_screen(self, focus_buttons=True, excerpt=None, narrow_rows=False):
|
2018-05-18 03:55:43 +00:00
|
|
|
return screen(
|
|
|
|
self.as_rows(), self.buttons,
|
2019-12-12 09:15:20 +00:00
|
|
|
focus_buttons=focus_buttons, excerpt=excerpt,
|
|
|
|
narrow_rows=narrow_rows)
|
2018-04-09 02:37:26 +00:00
|
|
|
|
2022-06-08 08:28:44 +00:00
|
|
|
def has_validation_error(self) -> bool:
|
2022-06-09 07:54:18 +00:00
|
|
|
""" Tells if any field is in error. """
|
2022-06-08 08:28:44 +00:00
|
|
|
return any(map(lambda f: f.is_in_error(), self._fields))
|
|
|
|
|
2017-02-13 01:21:26 +00:00
|
|
|
def validated(self):
|
2022-06-08 08:28:44 +00:00
|
|
|
if self.has_validation_error():
|
2018-07-03 23:43:35 +00:00
|
|
|
self.buttons.base_widget.contents[0][0].enabled = False
|
2017-09-20 03:20:32 +00:00
|
|
|
self.buttons.base_widget.focus_position = 1
|
2017-02-13 01:21:26 +00:00
|
|
|
else:
|
2018-07-03 23:43:35 +00:00
|
|
|
self.buttons.base_widget.contents[0][0].enabled = True
|
2017-09-04 23:58:49 +00:00
|
|
|
|
|
|
|
def as_data(self):
|
|
|
|
data = {}
|
|
|
|
for field in self._fields:
|
2018-06-26 03:57:00 +00:00
|
|
|
if field.enabled:
|
|
|
|
data[field.field.name] = field.value
|
2017-09-04 23:58:49 +00:00
|
|
|
return data
|
2019-12-12 09:22:49 +00:00
|
|
|
|
|
|
|
|
2019-12-16 02:02:07 +00:00
|
|
|
class SubFormWidget(WidgetWrap):
|
2019-12-12 09:22:49 +00:00
|
|
|
|
2019-12-16 02:02:07 +00:00
|
|
|
def __init__(self, form):
|
|
|
|
self.form = form
|
|
|
|
super().__init__(Pile(form.as_rows()))
|
2019-12-12 09:22:49 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def value(self):
|
|
|
|
return self.form.as_data()
|
|
|
|
|
|
|
|
@value.setter
|
|
|
|
def value(self, data):
|
|
|
|
for k, v in data.items():
|
|
|
|
getattr(self.form, k).value = v
|
|
|
|
|
|
|
|
|
|
|
|
class SubFormField(FormField):
|
|
|
|
|
|
|
|
takes_default_style = False
|
2022-06-09 07:54:18 +00:00
|
|
|
bound_field_class = BoundSubFormField
|
2019-12-12 09:22:49 +00:00
|
|
|
|
|
|
|
def __init__(self, form_cls, caption=None, help=None):
|
|
|
|
super().__init__(caption=caption, help=help)
|
|
|
|
self.form_cls = form_cls
|
|
|
|
|
|
|
|
def _make_widget(self, form):
|
2019-12-16 02:02:07 +00:00
|
|
|
form = self.form_cls(form)
|
|
|
|
return SubFormWidget(form)
|
2019-12-12 09:22:49 +00:00
|
|
|
|
|
|
|
|
|
|
|
class SubForm(Form):
|
|
|
|
|
2020-05-13 04:34:35 +00:00
|
|
|
def __init__(self, parent, **kw):
|
2019-12-12 09:22:49 +00:00
|
|
|
self.parent = parent
|
2020-05-13 04:34:35 +00:00
|
|
|
super().__init__(**kw)
|
2022-06-08 08:28:44 +00:00
|
|
|
|
|
|
|
def validated(self):
|
|
|
|
""" Propagate the validation to the parent. """
|
|
|
|
self.parent.validated()
|
|
|
|
super().validated()
|