1. import decimal
    
  2. 
    
  3. from django.core.exceptions import ValidationError
    
  4. from django.forms import DecimalField, NumberInput, Widget
    
  5. from django.test import SimpleTestCase, ignore_warnings, override_settings
    
  6. from django.utils import formats, translation
    
  7. from django.utils.deprecation import RemovedInDjango50Warning
    
  8. 
    
  9. from . import FormFieldAssertionsMixin
    
  10. 
    
  11. 
    
  12. class DecimalFieldTest(FormFieldAssertionsMixin, SimpleTestCase):
    
  13.     def test_decimalfield_1(self):
    
  14.         f = DecimalField(max_digits=4, decimal_places=2)
    
  15.         self.assertWidgetRendersTo(
    
  16.             f, '<input id="id_f" step="0.01" type="number" name="f" required>'
    
  17.         )
    
  18.         with self.assertRaisesMessage(ValidationError, "'This field is required.'"):
    
  19.             f.clean("")
    
  20.         with self.assertRaisesMessage(ValidationError, "'This field is required.'"):
    
  21.             f.clean(None)
    
  22.         self.assertEqual(f.clean("1"), decimal.Decimal("1"))
    
  23.         self.assertIsInstance(f.clean("1"), decimal.Decimal)
    
  24.         self.assertEqual(f.clean("23"), decimal.Decimal("23"))
    
  25.         self.assertEqual(f.clean("3.14"), decimal.Decimal("3.14"))
    
  26.         self.assertEqual(f.clean(3.14), decimal.Decimal("3.14"))
    
  27.         self.assertEqual(f.clean(decimal.Decimal("3.14")), decimal.Decimal("3.14"))
    
  28.         self.assertEqual(f.clean("1.0 "), decimal.Decimal("1.0"))
    
  29.         self.assertEqual(f.clean(" 1.0"), decimal.Decimal("1.0"))
    
  30.         self.assertEqual(f.clean(" 1.0 "), decimal.Decimal("1.0"))
    
  31.         with self.assertRaisesMessage(
    
  32.             ValidationError, "'Ensure that there are no more than 4 digits in total.'"
    
  33.         ):
    
  34.             f.clean("123.45")
    
  35.         with self.assertRaisesMessage(
    
  36.             ValidationError, "'Ensure that there are no more than 2 decimal places.'"
    
  37.         ):
    
  38.             f.clean("1.234")
    
  39.         msg = "'Ensure that there are no more than 2 digits before the decimal point.'"
    
  40.         with self.assertRaisesMessage(ValidationError, msg):
    
  41.             f.clean("123.4")
    
  42.         self.assertEqual(f.clean("-12.34"), decimal.Decimal("-12.34"))
    
  43.         with self.assertRaisesMessage(
    
  44.             ValidationError, "'Ensure that there are no more than 4 digits in total.'"
    
  45.         ):
    
  46.             f.clean("-123.45")
    
  47.         self.assertEqual(f.clean("-.12"), decimal.Decimal("-0.12"))
    
  48.         self.assertEqual(f.clean("-00.12"), decimal.Decimal("-0.12"))
    
  49.         self.assertEqual(f.clean("-000.12"), decimal.Decimal("-0.12"))
    
  50.         with self.assertRaisesMessage(
    
  51.             ValidationError, "'Ensure that there are no more than 2 decimal places.'"
    
  52.         ):
    
  53.             f.clean("-000.123")
    
  54.         with self.assertRaisesMessage(
    
  55.             ValidationError, "'Ensure that there are no more than 4 digits in total.'"
    
  56.         ):
    
  57.             f.clean("-000.12345")
    
  58.         self.assertEqual(f.max_digits, 4)
    
  59.         self.assertEqual(f.decimal_places, 2)
    
  60.         self.assertIsNone(f.max_value)
    
  61.         self.assertIsNone(f.min_value)
    
  62. 
    
  63.     def test_enter_a_number_error(self):
    
  64.         f = DecimalField(max_value=1, max_digits=4, decimal_places=2)
    
  65.         values = (
    
  66.             "-NaN",
    
  67.             "NaN",
    
  68.             "+NaN",
    
  69.             "-sNaN",
    
  70.             "sNaN",
    
  71.             "+sNaN",
    
  72.             "-Inf",
    
  73.             "Inf",
    
  74.             "+Inf",
    
  75.             "-Infinity",
    
  76.             "Infinity",
    
  77.             "+Infinity",
    
  78.             "a",
    
  79.             "łąść",
    
  80.             "1.0a",
    
  81.             "--0.12",
    
  82.         )
    
  83.         for value in values:
    
  84.             with self.subTest(value=value), self.assertRaisesMessage(
    
  85.                 ValidationError, "'Enter a number.'"
    
  86.             ):
    
  87.                 f.clean(value)
    
  88. 
    
  89.     def test_decimalfield_2(self):
    
  90.         f = DecimalField(max_digits=4, decimal_places=2, required=False)
    
  91.         self.assertIsNone(f.clean(""))
    
  92.         self.assertIsNone(f.clean(None))
    
  93.         self.assertEqual(f.clean("1"), decimal.Decimal("1"))
    
  94.         self.assertEqual(f.max_digits, 4)
    
  95.         self.assertEqual(f.decimal_places, 2)
    
  96.         self.assertIsNone(f.max_value)
    
  97.         self.assertIsNone(f.min_value)
    
  98. 
    
  99.     def test_decimalfield_3(self):
    
  100.         f = DecimalField(
    
  101.             max_digits=4,
    
  102.             decimal_places=2,
    
  103.             max_value=decimal.Decimal("1.5"),
    
  104.             min_value=decimal.Decimal("0.5"),
    
  105.         )
    
  106.         self.assertWidgetRendersTo(
    
  107.             f,
    
  108.             '<input step="0.01" name="f" min="0.5" max="1.5" type="number" id="id_f" '
    
  109.             "required>",
    
  110.         )
    
  111.         with self.assertRaisesMessage(
    
  112.             ValidationError, "'Ensure this value is less than or equal to 1.5.'"
    
  113.         ):
    
  114.             f.clean("1.6")
    
  115.         with self.assertRaisesMessage(
    
  116.             ValidationError, "'Ensure this value is greater than or equal to 0.5.'"
    
  117.         ):
    
  118.             f.clean("0.4")
    
  119.         self.assertEqual(f.clean("1.5"), decimal.Decimal("1.5"))
    
  120.         self.assertEqual(f.clean("0.5"), decimal.Decimal("0.5"))
    
  121.         self.assertEqual(f.clean(".5"), decimal.Decimal("0.5"))
    
  122.         self.assertEqual(f.clean("00.50"), decimal.Decimal("0.50"))
    
  123.         self.assertEqual(f.max_digits, 4)
    
  124.         self.assertEqual(f.decimal_places, 2)
    
  125.         self.assertEqual(f.max_value, decimal.Decimal("1.5"))
    
  126.         self.assertEqual(f.min_value, decimal.Decimal("0.5"))
    
  127. 
    
  128.     def test_decimalfield_4(self):
    
  129.         f = DecimalField(decimal_places=2)
    
  130.         with self.assertRaisesMessage(
    
  131.             ValidationError, "'Ensure that there are no more than 2 decimal places.'"
    
  132.         ):
    
  133.             f.clean("0.00000001")
    
  134. 
    
  135.     def test_decimalfield_5(self):
    
  136.         f = DecimalField(max_digits=3)
    
  137.         # Leading whole zeros "collapse" to one digit.
    
  138.         self.assertEqual(f.clean("0000000.10"), decimal.Decimal("0.1"))
    
  139.         # But a leading 0 before the . doesn't count toward max_digits
    
  140.         self.assertEqual(f.clean("0000000.100"), decimal.Decimal("0.100"))
    
  141.         # Only leading whole zeros "collapse" to one digit.
    
  142.         self.assertEqual(f.clean("000000.02"), decimal.Decimal("0.02"))
    
  143.         with self.assertRaisesMessage(
    
  144.             ValidationError, "'Ensure that there are no more than 3 digits in total.'"
    
  145.         ):
    
  146.             f.clean("000000.0002")
    
  147.         self.assertEqual(f.clean(".002"), decimal.Decimal("0.002"))
    
  148. 
    
  149.     def test_decimalfield_6(self):
    
  150.         f = DecimalField(max_digits=2, decimal_places=2)
    
  151.         self.assertEqual(f.clean(".01"), decimal.Decimal(".01"))
    
  152.         msg = "'Ensure that there are no more than 0 digits before the decimal point.'"
    
  153.         with self.assertRaisesMessage(ValidationError, msg):
    
  154.             f.clean("1.1")
    
  155. 
    
  156.     def test_decimalfield_scientific(self):
    
  157.         f = DecimalField(max_digits=4, decimal_places=2)
    
  158.         with self.assertRaisesMessage(ValidationError, "Ensure that there are no more"):
    
  159.             f.clean("1E+2")
    
  160.         self.assertEqual(f.clean("1E+1"), decimal.Decimal("10"))
    
  161.         self.assertEqual(f.clean("1E-1"), decimal.Decimal("0.1"))
    
  162.         self.assertEqual(f.clean("0.546e+2"), decimal.Decimal("54.6"))
    
  163. 
    
  164.     def test_decimalfield_widget_attrs(self):
    
  165.         f = DecimalField(max_digits=6, decimal_places=2)
    
  166.         self.assertEqual(f.widget_attrs(Widget()), {})
    
  167.         self.assertEqual(f.widget_attrs(NumberInput()), {"step": "0.01"})
    
  168.         f = DecimalField(max_digits=10, decimal_places=0)
    
  169.         self.assertEqual(f.widget_attrs(NumberInput()), {"step": "1"})
    
  170.         f = DecimalField(max_digits=19, decimal_places=19)
    
  171.         self.assertEqual(f.widget_attrs(NumberInput()), {"step": "1e-19"})
    
  172.         f = DecimalField(max_digits=20)
    
  173.         self.assertEqual(f.widget_attrs(NumberInput()), {"step": "any"})
    
  174.         f = DecimalField(max_digits=6, widget=NumberInput(attrs={"step": "0.01"}))
    
  175.         self.assertWidgetRendersTo(
    
  176.             f, '<input step="0.01" name="f" type="number" id="id_f" required>'
    
  177.         )
    
  178. 
    
  179.     def test_decimalfield_localized(self):
    
  180.         """
    
  181.         A localized DecimalField's widget renders to a text input without
    
  182.         number input specific attributes.
    
  183.         """
    
  184.         f = DecimalField(localize=True)
    
  185.         self.assertWidgetRendersTo(f, '<input id="id_f" name="f" type="text" required>')
    
  186. 
    
  187.     def test_decimalfield_changed(self):
    
  188.         f = DecimalField(max_digits=2, decimal_places=2)
    
  189.         d = decimal.Decimal("0.1")
    
  190.         self.assertFalse(f.has_changed(d, "0.10"))
    
  191.         self.assertTrue(f.has_changed(d, "0.101"))
    
  192. 
    
  193.         with translation.override("fr"):
    
  194.             f = DecimalField(max_digits=2, decimal_places=2, localize=True)
    
  195.             localized_d = formats.localize_input(d)  # -> '0,1' in French
    
  196.             self.assertFalse(f.has_changed(d, localized_d))
    
  197. 
    
  198.     # RemovedInDjango50Warning: When the deprecation ends, remove
    
  199.     # @ignore_warnings and USE_L10N=False. The test should remain because
    
  200.     # format-related settings will take precedence over locale-dictated
    
  201.     # formats.
    
  202.     @ignore_warnings(category=RemovedInDjango50Warning)
    
  203.     @override_settings(USE_L10N=False, DECIMAL_SEPARATOR=",")
    
  204.     def test_decimalfield_support_decimal_separator(self):
    
  205.         f = DecimalField(localize=True)
    
  206.         self.assertEqual(f.clean("1001,10"), decimal.Decimal("1001.10"))
    
  207.         self.assertEqual(f.clean("1001.10"), decimal.Decimal("1001.10"))
    
  208. 
    
  209.     # RemovedInDjango50Warning: When the deprecation ends, remove
    
  210.     # @ignore_warnings and USE_L10N=False. The test should remain because
    
  211.     # format-related settings will take precedence over locale-dictated
    
  212.     # formats.
    
  213.     @ignore_warnings(category=RemovedInDjango50Warning)
    
  214.     @override_settings(
    
  215.         USE_L10N=False,
    
  216.         DECIMAL_SEPARATOR=",",
    
  217.         USE_THOUSAND_SEPARATOR=True,
    
  218.         THOUSAND_SEPARATOR=".",
    
  219.     )
    
  220.     def test_decimalfield_support_thousands_separator(self):
    
  221.         f = DecimalField(localize=True)
    
  222.         self.assertEqual(f.clean("1.001,10"), decimal.Decimal("1001.10"))
    
  223.         msg = "'Enter a number.'"
    
  224.         with self.assertRaisesMessage(ValidationError, msg):
    
  225.             f.clean("1,001.1")