codekingpro/portable-devtools
115k
1import decimal2 3from wtforms import widgets4from wtforms.fields.core import Field5from wtforms.utils import unset_value6 7__all__ = (8 "IntegerField",9 "DecimalField",10 "FloatField",11 "IntegerRangeField",12 "DecimalRangeField",13)14 15 16class LocaleAwareNumberField(Field):17 """18 Base class for implementing locale-aware number parsing.19 20 Locale-aware numbers require the 'babel' package to be present.21 """22 23 def __init__(24 self,25 label=None,26 validators=None,27 use_locale=False,28 number_format=None,29 **kwargs,30 ):31 super().__init__(label, validators, **kwargs)32 self.use_locale = use_locale33 if use_locale:34 self.number_format = number_format35 self.locale = kwargs["_form"].meta.locales[0]36 self._init_babel()37 38 def _init_babel(self):39 try:40 from babel import numbers41 42 self.babel_numbers = numbers43 except ImportError as exc:44 raise ImportError(45 "Using locale-aware decimals requires the babel library."46 ) from exc47 48 def _parse_decimal(self, value):49 return self.babel_numbers.parse_decimal(value, self.locale)50 51 def _format_decimal(self, value):52 return self.babel_numbers.format_decimal(value, self.number_format, self.locale)53 54 55class IntegerField(Field):56 """57 A text field, except all input is coerced to an integer. Erroneous input58 is ignored and will not be accepted as a value.59 """60 61 widget = widgets.NumberInput()62 63 def __init__(self, label=None, validators=None, **kwargs):64 super().__init__(label, validators, **kwargs)65 66 def _value(self):67 if self.raw_data:68 return self.raw_data[0]69 if self.data is not None:70 return str(self.data)71 return ""72 73 def process_data(self, value):74 if value is None or value is unset_value:75 self.data = None76 return77 78 try:79 self.data = int(value)80 except (ValueError, TypeError) as exc:81 self.data = None82 raise ValueError(self.gettext("Not a valid integer value.")) from exc83 84 def process_formdata(self, valuelist):85 if not valuelist:86 return87 88 try:89 self.data = int(valuelist[0])90 except ValueError as exc:91 self.data = None92 raise ValueError(self.gettext("Not a valid integer value.")) from exc93 94 95class DecimalField(LocaleAwareNumberField):96 """97 A text field which displays and coerces data of the `decimal.Decimal` type.98 99 :param places:100 How many decimal places to quantize the value to for display on form.101 If unset, use 2 decimal places.102 If explicitely set to `None`, does not quantize value.103 :param rounding:104 How to round the value during quantize, for example105 `decimal.ROUND_UP`. If unset, uses the rounding value from the106 current thread's context.107 :param use_locale:108 If True, use locale-based number formatting. Locale-based number109 formatting requires the 'babel' package.110 :param number_format:111 Optional number format for locale. If omitted, use the default decimal112 format for the locale.113 """114 115 widget = widgets.NumberInput(step="any")116 117 def __init__(118 self, label=None, validators=None, places=unset_value, rounding=None, **kwargs119 ):120 super().__init__(label, validators, **kwargs)121 if self.use_locale and (places is not unset_value or rounding is not None):122 raise TypeError(123 "When using locale-aware numbers, 'places' and 'rounding' are ignored."124 )125 126 if places is unset_value:127 places = 2128 self.places = places129 self.rounding = rounding130 131 def _value(self):132 if self.raw_data:133 return self.raw_data[0]134 135 if self.data is None:136 return ""137 138 if self.use_locale:139 return str(self._format_decimal(self.data))140 141 if self.places is None:142 return str(self.data)143 144 if not hasattr(self.data, "quantize"):145 # If for some reason, data is a float or int, then format146 # as we would for floats using string formatting.147 format = "%%0.%df" % self.places148 return format % self.data149 150 exp = decimal.Decimal(".1") ** self.places151 if self.rounding is None:152 quantized = self.data.quantize(exp)153 else:154 quantized = self.data.quantize(exp, rounding=self.rounding)155 return str(quantized)156 157 def process_formdata(self, valuelist):158 if not valuelist:159 return160 161 try:162 if self.use_locale:163 self.data = self._parse_decimal(valuelist[0])164 else:165 self.data = decimal.Decimal(valuelist[0])166 except (decimal.InvalidOperation, ValueError) as exc:167 self.data = None168 raise ValueError(self.gettext("Not a valid decimal value.")) from exc169 170 171class FloatField(Field):172 """173 A text field, except all input is coerced to an float. Erroneous input174 is ignored and will not be accepted as a value.175 """176 177 widget = widgets.TextInput()178 179 def __init__(self, label=None, validators=None, **kwargs):180 super().__init__(label, validators, **kwargs)181 182 def _value(self):183 if self.raw_data:184 return self.raw_data[0]185 if self.data is not None:186 return str(self.data)187 return ""188 189 def process_formdata(self, valuelist):190 if not valuelist:191 return192 193 try:194 self.data = float(valuelist[0])195 except ValueError as exc:196 self.data = None197 raise ValueError(self.gettext("Not a valid float value.")) from exc198 199 200class IntegerRangeField(IntegerField):201 """202 Represents an ``<input type="range">``.203 """204 205 widget = widgets.RangeInput()206 207 208class DecimalRangeField(DecimalField):209 """210 Represents an ``<input type="range">``.211 """212 213 widget = widgets.RangeInput(step="any")214 