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 CheckBox, MetaSignals
|
2018-11-29 08:58:17 +00:00
|
|
|
from urwid import Padding as UrwidPadding
|
2017-02-13 01:21:26 +00:00
|
|
|
from urwid import (
|
2019-12-14 09:28:30 +00:00
|
|
|
RadioButton,
|
2017-02-13 01:21:26 +00:00
|
|
|
Text,
|
|
|
|
WidgetDecoration,
|
2017-10-05 03:19:27 +00:00
|
|
|
connect_signal,
|
2017-02-13 01:21:26 +00:00
|
|
|
delegate_to_widget_mixin,
|
|
|
|
emit_signal,
|
|
|
|
)
|
|
|
|
|
|
|
|
from subiquitycore.ui.buttons import cancel_btn, done_btn
|
2018-06-21 21:38:18 +00:00
|
|
|
from subiquitycore.ui.container import Pile, WidgetWrap
|
2017-03-21 01:39:23 +00:00
|
|
|
from subiquitycore.ui.interactive import (
|
|
|
|
EmailEditor,
|
|
|
|
IntegerEditor,
|
|
|
|
PasswordEditor,
|
|
|
|
StringEditor,
|
|
|
|
)
|
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 Color, button_pile, disabled, screen
|
2018-11-29 08:58:17 +00:00
|
|
|
from subiquitycore.ui.width import widget_width
|
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):
|
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."""
|
2023-07-25 21:26:25 +00:00
|
|
|
|
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)),
|
2023-07-25 21:26:25 +00:00
|
|
|
),
|
2019-12-12 09:15:20 +00:00
|
|
|
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):
|
2022-08-24 13:58:46 +00:00
|
|
|
return ("info_minor", _(self.field.help))
|
2020-05-06 09:07:03 +00:00
|
|
|
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
|
|
|
|
2022-06-24 09:18:47 +00:00
|
|
|
def use_as_confirmation(self, for_field: "BoundFormField", desc: str) -> None:
|
|
|
|
"""Mark this field as a confirmation field for another field.
|
|
|
|
This will automatically compare the value of both fields when this
|
|
|
|
field (a.k.a., the confirmation field) is changed."""
|
2023-07-25 21:26:25 +00:00
|
|
|
|
2022-06-24 09:18:47 +00:00
|
|
|
def _check_confirmation(sender, new_text):
|
|
|
|
if not for_field.value.startswith(new_text):
|
|
|
|
self.show_extra(("info_error", _(f"{desc} do not match")))
|
|
|
|
else:
|
|
|
|
self.show_extra("")
|
2023-07-25 21:26:25 +00:00
|
|
|
|
2022-06-24 09:18:47 +00:00
|
|
|
connect_signal(self.widget, "change", _check_confirmation)
|
|
|
|
|
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
|
2022-08-24 13:58:46 +00:00
|
|
|
# Allows styling at instantiation
|
2022-08-29 16:55:07 +00:00
|
|
|
if isinstance(help, str):
|
2022-08-24 13:58:46 +00:00
|
|
|
self.help = ("info_minor", help)
|
2022-08-29 16:55:07 +00:00
|
|
|
else:
|
|
|
|
self.help = help
|
2022-06-09 07:54:18 +00:00
|
|
|
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()
|
2023-07-25 21:26:25 +00:00
|
|
|
|
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:
|
filesystem: store the actual size in bytes alongside the human readable size
Currently, the partition form stores the size as a human readable value.
(e.g., 123456K, 1.1G, 1.876G, 100G). When we exit the size field (i.e., upon
losing focus), we convert the value to a number of bytes and then align
it up to the nearest MiB (or whatever the alignment requirement is).
Unfortunately, after computing the aligned value, we turn it back into a
human-readable string and store it as is. It is not okay because the
conversion does not ensure that the alignment requirement is still
honored.
For instance, if the user types in 1.1G, we do the following:
* convert it to a number of bytes -> 1181116006.4 (surprise, it is not
even an integer).
* round it up to the nearest MiB -> 1181745152 (this is the correct
value)
* transform it into a human readable string and store it as is -> 1.1G
- which actually corresponds to the original value.
This leads to an exception later when creating the partition:
File "subiquity/models/filesystem.py", line 1841, in add_partition
raise Exception(
Exception: ('size %s or offset %s not aligned to %s', 1181116006, 1048576, 1048576)
Fixed by storing the actual size as a number of bytes - alongside the
human readable size.
Signed-off-by: Olivier Gayot <olivier.gayot@canonical.com>
2023-08-09 11:28:07 +00:00
|
|
|
accurate_value = getattr(field.widget, "accurate_value", None)
|
|
|
|
if accurate_value is not None:
|
|
|
|
data[field.field.name] = accurate_value
|
|
|
|
else:
|
|
|
|
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):
|
|
|
|
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()
|