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 ````.
"""
widget = widgets.RangeInput()
class DecimalRangeField(DecimalField):
"""
Represents an ````.
"""
widget = widgets.RangeInput(step="any")