214 lines
6.0 KiB
Python
214 lines
6.0 KiB
Python
import decimal
|
|
|
|
from wtforms import widgets
|
|
from wtforms.fields.core import Field
|
|
from wtforms.utils import unset_value
|
|
|
|
__all__ = (
|
|
"IntegerField",
|
|
"DecimalField",
|
|
"FloatField",
|
|
"IntegerRangeField",
|
|
"DecimalRangeField",
|
|
)
|
|
|
|
|
|
class LocaleAwareNumberField(Field):
|
|
"""
|
|
Base class for implementing locale-aware number parsing.
|
|
|
|
Locale-aware numbers require the 'babel' package to be present.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
label=None,
|
|
validators=None,
|
|
use_locale=False,
|
|
number_format=None,
|
|
**kwargs,
|
|
):
|
|
super().__init__(label, validators, **kwargs)
|
|
self.use_locale = use_locale
|
|
if use_locale:
|
|
self.number_format = number_format
|
|
self.locale = kwargs["_form"].meta.locales[0]
|
|
self._init_babel()
|
|
|
|
def _init_babel(self):
|
|
try:
|
|
from babel import numbers
|
|
|
|
self.babel_numbers = numbers
|
|
except ImportError as exc:
|
|
raise ImportError(
|
|
"Using locale-aware decimals requires the babel library."
|
|
) from exc
|
|
|
|
def _parse_decimal(self, value):
|
|
return self.babel_numbers.parse_decimal(value, self.locale)
|
|
|
|
def _format_decimal(self, value):
|
|
return self.babel_numbers.format_decimal(value, self.number_format, self.locale)
|
|
|
|
|
|
class IntegerField(Field):
|
|
"""
|
|
A text field, except all input is coerced to an integer. Erroneous input
|
|
is ignored and will not be accepted as a value.
|
|
"""
|
|
|
|
widget = widgets.NumberInput()
|
|
|
|
def __init__(self, label=None, validators=None, **kwargs):
|
|
super().__init__(label, validators, **kwargs)
|
|
|
|
def _value(self):
|
|
if self.raw_data:
|
|
return self.raw_data[0]
|
|
if self.data is not None:
|
|
return str(self.data)
|
|
return ""
|
|
|
|
def process_data(self, value):
|
|
if value is None or value is unset_value:
|
|
self.data = None
|
|
return
|
|
|
|
try:
|
|
self.data = int(value)
|
|
except (ValueError, TypeError) as exc:
|
|
self.data = None
|
|
raise ValueError(self.gettext("Not a valid integer value.")) from exc
|
|
|
|
def process_formdata(self, valuelist):
|
|
if not valuelist:
|
|
return
|
|
|
|
try:
|
|
self.data = int(valuelist[0])
|
|
except ValueError as exc:
|
|
self.data = None
|
|
raise ValueError(self.gettext("Not a valid integer value.")) from exc
|
|
|
|
|
|
class DecimalField(LocaleAwareNumberField):
|
|
"""
|
|
A text field which displays and coerces data of the `decimal.Decimal` type.
|
|
|
|
:param places:
|
|
How many decimal places to quantize the value to for display on form.
|
|
If unset, use 2 decimal places.
|
|
If explicitely set to `None`, does not quantize value.
|
|
:param rounding:
|
|
How to round the value during quantize, for example
|
|
`decimal.ROUND_UP`. If unset, uses the rounding value from the
|
|
current thread's context.
|
|
:param use_locale:
|
|
If True, use locale-based number formatting. Locale-based number
|
|
formatting requires the 'babel' package.
|
|
:param number_format:
|
|
Optional number format for locale. If omitted, use the default decimal
|
|
format for the locale.
|
|
"""
|
|
|
|
widget = widgets.NumberInput(step="any")
|
|
|
|
def __init__(
|
|
self, label=None, validators=None, places=unset_value, rounding=None, **kwargs
|
|
):
|
|
super().__init__(label, validators, **kwargs)
|
|
if self.use_locale and (places is not unset_value or rounding is not None):
|
|
raise TypeError(
|
|
"When using locale-aware numbers, 'places' and 'rounding' are ignored."
|
|
)
|
|
|
|
if places is unset_value:
|
|
places = 2
|
|
self.places = places
|
|
self.rounding = rounding
|
|
|
|
def _value(self):
|
|
if self.raw_data:
|
|
return self.raw_data[0]
|
|
|
|
if self.data is None:
|
|
return ""
|
|
|
|
if self.use_locale:
|
|
return str(self._format_decimal(self.data))
|
|
|
|
if self.places is None:
|
|
return str(self.data)
|
|
|
|
if not hasattr(self.data, "quantize"):
|
|
# If for some reason, data is a float or int, then format
|
|
# as we would for floats using string formatting.
|
|
format = "%%0.%df" % self.places
|
|
return format % self.data
|
|
|
|
exp = decimal.Decimal(".1") ** self.places
|
|
if self.rounding is None:
|
|
quantized = self.data.quantize(exp)
|
|
else:
|
|
quantized = self.data.quantize(exp, rounding=self.rounding)
|
|
return str(quantized)
|
|
|
|
def process_formdata(self, valuelist):
|
|
if not valuelist:
|
|
return
|
|
|
|
try:
|
|
if self.use_locale:
|
|
self.data = self._parse_decimal(valuelist[0])
|
|
else:
|
|
self.data = decimal.Decimal(valuelist[0])
|
|
except (decimal.InvalidOperation, ValueError) as exc:
|
|
self.data = None
|
|
raise ValueError(self.gettext("Not a valid decimal value.")) from exc
|
|
|
|
|
|
class FloatField(Field):
|
|
"""
|
|
A text field, except all input is coerced to an float. Erroneous input
|
|
is ignored and will not be accepted as a value.
|
|
"""
|
|
|
|
widget = widgets.TextInput()
|
|
|
|
def __init__(self, label=None, validators=None, **kwargs):
|
|
super().__init__(label, validators, **kwargs)
|
|
|
|
def _value(self):
|
|
if self.raw_data:
|
|
return self.raw_data[0]
|
|
if self.data is not None:
|
|
return str(self.data)
|
|
return ""
|
|
|
|
def process_formdata(self, valuelist):
|
|
if not valuelist:
|
|
return
|
|
|
|
try:
|
|
self.data = float(valuelist[0])
|
|
except ValueError as exc:
|
|
self.data = None
|
|
raise ValueError(self.gettext("Not a valid float value.")) from exc
|
|
|
|
|
|
class IntegerRangeField(IntegerField):
|
|
"""
|
|
Represents an ``<input type="range">``.
|
|
"""
|
|
|
|
widget = widgets.RangeInput()
|
|
|
|
|
|
class DecimalRangeField(DecimalField):
|
|
"""
|
|
Represents an ``<input type="range">``.
|
|
"""
|
|
|
|
widget = widgets.RangeInput(step="any")
|