├── .github └── workflows │ ├── main.yml │ └── python-publish.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── changelog.md ├── setup.py └── src ├── expression_fields ├── __init__.py ├── expr.py └── fields.py ├── runtests.py └── tests ├── __init__.py ├── test_settings.py └── tests.py /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build and test package 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | python-version: ["3.9", "3.10"] 17 | django-version: ["3.2", "4.0"] 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v2 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install --upgrade pip 28 | python -m pip install -q Django==${{ matrix.django-version }} 29 | python setup.py -q install 30 | 31 | # python -m pip install flake8 pytest 32 | # if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 33 | # - name: Test with pytest 34 | # run: | 35 | # pytest 36 | - name: Run ugly custom tests 37 | run: | 38 | python src/runtests.py 39 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [published] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install build 25 | - name: Build package 26 | run: python -m build 27 | - name: Publish package 28 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 29 | with: 30 | user: __token__ 31 | password: ${{ secrets.PYPI_API_TOKEN }} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Matt Cooper and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/vtbassmatt/django-expression-fields.svg?branch=master)](https://travis-ci.org/vtbassmatt/django-expression-fields) 2 | [![PyPI](https://img.shields.io/pypi/v/django-expression-fields.svg)](https://pypi.python.org/pypi/django-expression-fields) 3 | 4 | Introduction 5 | ------------ 6 | 7 | django-expression-fields lets your users type a mathematical expression in a form field. 8 | Python does the math and stores the result in the database. For example, suppose you have a model to track Things, like this: 9 | 10 | class Thing(models.Model): 11 | cost = models.DecimalField( 12 | max_digits=5, decimal_places=2, null=True, blank=True) 13 | 14 | Suppose Things come in packs of 12 for $7.99. Your users have to do some math to fill in the cost of a single Thing, $0.67. 15 | 16 | But not with an expression field! Create your form like this: 17 | 18 | class ThingForm(forms.Form): 19 | cost = DecimalExpressionField( 20 | max_digits=5, decimal_places=2, required=False) 21 | 22 | Now your user can simply type `7.99/12` in the field and Python will do the math for them! 23 | 24 | 25 | Requirements and Installation 26 | ----------------------------- 27 | 28 | Right now, the project has no dependencies outside of Django itself. 29 | It has been updated to work with Python 3.9-3.10 and Django 3.2-4.0. 30 | 31 | * `pip install django-expression-fields` 32 | * Add `expression_fields` to your `INSTALLED_APPS`. 33 | 34 | 35 | Use 36 | --- 37 | 38 | from django import forms 39 | from expression_fields.fields import DivideDecimalField 40 | 41 | class MyForm(forms.Form): 42 | cost = DecimalExpressionField( 43 | max_digits=5, decimal_places=2, required=False) 44 | 45 | 46 | Tests 47 | ----- 48 | 49 | `./run-tests.sh`. 50 | 51 | 52 | Limitations 53 | ----------- 54 | 55 | * Only `DecimalExpressionField` exists today. I later intend to build Integer and Float expression fields as well. 56 | * This field slightly changes the behavior of the existing `DecimalField`: Inputs that would rejected for having too many digits after the decimal are instead rounded. 57 | * For historical reasons, there's a `DivideDecimalField` which allows a single division sign. 58 | 59 | 60 | Contributions 61 | ------------- 62 | 63 | I built this little project to satisfy a personal need, but thought it might be useful enough for others. 64 | If you have contributions, please don't hesitate to send a PR. 65 | Let's keep the tests passing and all will be well. 66 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | 0.3.0 2 | ----- 3 | * Fix bug where a zero entry would be dropped -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | version = '0.4.0' 5 | README = """ 6 | django-expression-fields lets your users type a mathematical expression in a form field. 7 | Python does the math and stores the result in the database. For example, suppose you have a model to track Things, like this:: 8 | 9 | class Thing(models.Model): 10 | cost = models.DecimalField( 11 | max_digits=5, decimal_places=2, null=True, blank=True) 12 | 13 | Suppose Things come in packs of 12 for $7.99. Your users have to do some math to fill in the cost of a single Thing, $0.67. 14 | 15 | But not with an expression field! Create your form like this:: 16 | 17 | class ThingForm(forms.Form): 18 | cost = DecimalExpressionField( 19 | max_digits=5, decimal_places=2, required=False) 20 | 21 | Now your user can simply type ``7.99/12`` in the field and Python will do the math for them! 22 | """ 23 | 24 | # allow setup.py to be run from any path 25 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 26 | 27 | setup( 28 | name = 'django-expression-fields', 29 | version = version, 30 | description = 'django-expression-fields allows typing mathematical expressions into form fields and having only the calculated result stored in the database.', 31 | long_description = README, 32 | keywords = 'django field expression math', 33 | license = 'MIT License', 34 | author = 'Matt Cooper', 35 | author_email = 'vtbassmatt@gmail.com', 36 | url = 'http://github.com/vtbassmatt/django-expression-fields/', 37 | install_requires = ['django'], 38 | classifiers=[ 39 | 'Development Status :: 4 - Beta', 40 | 'Environment :: Plugins', 41 | 'Environment :: Web Environment', 42 | 'Framework :: Django', 43 | 'Intended Audience :: Developers', 44 | 'License :: OSI Approved :: MIT License', 45 | 'Programming Language :: Python', 46 | 'Programming Language :: Python :: 2.7', 47 | 'Programming Language :: Python :: 3', 48 | 'Programming Language :: Python :: 3.4', 49 | 'Topic :: Software Development :: Libraries :: Python Modules', 50 | ], 51 | package_dir = {'': 'src'}, 52 | packages = ['expression_fields'], 53 | include_package_data = True, 54 | ) 55 | 56 | -------------------------------------------------------------------------------- /src/expression_fields/__init__.py: -------------------------------------------------------------------------------- 1 | from pkg_resources import get_distribution, DistributionNotFound 2 | 3 | try: 4 | _dist = get_distribution('django-fields') 5 | except DistributionNotFound: 6 | __version__ = 'Please install this project with setup.py' 7 | else: 8 | __version__ = _dist.version 9 | VERSION = __version__ 10 | -------------------------------------------------------------------------------- /src/expression_fields/expr.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | from math import * 3 | 4 | def calculate(expr_string): 5 | 6 | math_list = ['math', 'acos', 'asin', 'atan', 'atan2', 'ceil', 7 | 'cos', 'cosh', 'degrees', 'e', 'exp', 'fabs', 'floor', 'fmod', 8 | 'frexp', 'hypot', 'ldexp', 'log', 'log10', 'modf', 'pi', 9 | 'pow', 'radians', 'sin', 'sinh', 'sqrt', 'tan', 'tanh'] 10 | 11 | builtins_list = [abs] 12 | 13 | local_ctx = dict([ (k, globals().get(k, None)) for k in math_list ]) 14 | local_ctx.update(dict([ (b.__name__, b) for b in builtins_list ])) 15 | 16 | try: 17 | return eval(expr_string, { "__builtins__": None }, local_ctx) 18 | except (SyntaxError, TypeError, NameError): 19 | return None 20 | -------------------------------------------------------------------------------- /src/expression_fields/fields.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | from django.forms.fields import DecimalField 3 | from django.forms.widgets import TextInput 4 | from django.utils import formats 5 | from django.utils.encoding import smart_str 6 | from django.core.exceptions import ValidationError 7 | from .expr import calculate 8 | 9 | 10 | class DivideDecimalField(DecimalField): 11 | """A decimal field which allows the division operator.""" 12 | widget = TextInput 13 | 14 | def __init__(self, *args, **kwargs): 15 | kwargs.setdefault('help_text', "You can specify a decimal or use '/' to do simple division.") 16 | super(DivideDecimalField, self).__init__(*args, **kwargs) 17 | 18 | def to_python(self, value): 19 | if value in self.empty_values: 20 | return None 21 | if self.localize: 22 | value = formats.sanitize_separators(value) 23 | value = smart_str(value).strip() 24 | if '/' in value: 25 | numerator, denominator = value.split('/', 2) 26 | tp = super(DivideDecimalField, self).to_python 27 | value = tp(numerator) / tp(denominator) 28 | if value: 29 | # In Python 3, a simple round() call is enough. To support 30 | # Python 2, we have to do this quantize thing. 31 | quantize_target = ".".join(["1", "0" * self.decimal_places]) 32 | return value.quantize(Decimal(quantize_target)) 33 | else: 34 | return super(DivideDecimalField, self).to_python(value) 35 | 36 | 37 | class DecimalExpressionField(DecimalField): 38 | """A decimal field which allows arbitrary math.""" 39 | widget = TextInput 40 | 41 | def __init__(self, *args, **kwargs): 42 | kwargs.setdefault('help_text', "You can specify a decimal or do simple arithmetic.") 43 | super(DecimalExpressionField, self).__init__(*args, **kwargs) 44 | 45 | def to_python(self, value): 46 | if value in self.empty_values: 47 | return None 48 | value = smart_str(value).strip() 49 | value = calculate(value) 50 | value = super(DecimalExpressionField, self).to_python(value) 51 | if value is not None: 52 | # In Python 3, a simple round() call is enough. To support 53 | # Python 2, we have to do this quantize thing. 54 | try: 55 | quantize_target = ".".join(["1", "0" * self.decimal_places]) 56 | return value.quantize(Decimal(quantize_target)) 57 | except (ValueError, TypeError): 58 | raise ValidationError('Enter an expression.', code='invalid') 59 | 60 | 61 | class FutureField(object): 62 | def __init__(self, *args, **kwargs): 63 | raise NotImplementedError 64 | 65 | 66 | class FloatExpressionField(FutureField): 67 | pass 68 | 69 | 70 | class IntegerExpressionField(FutureField): 71 | pass 72 | -------------------------------------------------------------------------------- /src/runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | import django 6 | from django.conf import settings 7 | from django.test.utils import get_runner 8 | 9 | if __name__ == "__main__": 10 | os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.test_settings' 11 | django.setup() 12 | TestRunner = get_runner(settings) 13 | test_runner = TestRunner() 14 | failures = test_runner.run_tests(["tests"]) 15 | sys.exit(bool(failures)) 16 | -------------------------------------------------------------------------------- /src/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vtbassmatt/django-expression-fields/5bdf58087126ab0ce913ed84021c8476c2e7d901/src/tests/__init__.py -------------------------------------------------------------------------------- /src/tests/test_settings.py: -------------------------------------------------------------------------------- 1 | SECRET_KEY = 'tests' 2 | INSTALLED_APPS = [ 3 | "expression_fields", 4 | ] 5 | -------------------------------------------------------------------------------- /src/tests/tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from decimal import Decimal 3 | from django.forms import Form 4 | from expression_fields.fields import DivideDecimalField, DecimalExpressionField 5 | 6 | 7 | class DivDecForm(Form): 8 | field1 = DivideDecimalField(max_digits=5, decimal_places=2, required=False) 9 | field2 = DivideDecimalField(max_digits=4, decimal_places=1) 10 | field3 = DivideDecimalField(max_digits=5, decimal_places=0, required=False) 11 | 12 | 13 | class DivideDecimalTests(unittest.TestCase): 14 | 15 | def test_base_behavior(self): 16 | """ 17 | Sanity test to make sure we didn't break the base DecimalField. 18 | """ 19 | form1 = DivDecForm({'field1': '256.32', 'field2': '123.4'}) 20 | form2 = DivDecForm({'field1': '256', 'field2': '123'}) 21 | form3 = DivDecForm({'field2': '123'}) 22 | form4 = DivDecForm({'field1': '123'}) 23 | form5 = DivDecForm({'field2': '123', 'field3': '456'}) 24 | form6 = DivDecForm({'field2': '123', 'field3': '456.0'}) 25 | form7 = DivDecForm({'field1': '', 'field2': '0.0'}) 26 | self.assertTrue(form1.is_valid()) 27 | self.assertEqual(form1.cleaned_data['field1'], Decimal('256.32')) 28 | self.assertEqual(form1.cleaned_data['field2'], Decimal('123.4')) 29 | self.assertTrue(form2.is_valid()) 30 | self.assertEqual(form2.cleaned_data['field1'], Decimal('256')) 31 | self.assertEqual(form2.cleaned_data['field2'], Decimal('123')) 32 | self.assertTrue(form3.is_valid()) 33 | self.assertFalse(form4.is_valid()) 34 | self.assertTrue(form5.is_valid()) 35 | self.assertFalse(form6.is_valid()) 36 | self.assertTrue(form7.is_valid()) 37 | self.assertEqual(form7.cleaned_data['field1'], None) 38 | self.assertEqual(form7.cleaned_data['field2'], Decimal('0')) 39 | self.assertEqual(form7.cleaned_data['field3'], None) 40 | 41 | def test_divide(self): 42 | """ 43 | Test a few instances of fields with a '/'. 44 | """ 45 | form1 = DivDecForm({'field1': '1/2', 'field2': '2/1', 'field3': '3/3'}) 46 | self.assertTrue(form1.is_valid()) 47 | self.assertEqual(form1.cleaned_data['field1'], Decimal('.5')) 48 | self.assertEqual(form1.cleaned_data['field2'], Decimal('2')) 49 | self.assertEqual(form1.cleaned_data['field3'], Decimal('1')) 50 | 51 | form2 = DivDecForm({'field1': '1/3', 'field2': '1/3', 'field3': '1/3'}) 52 | self.assertTrue(form2.is_valid()) 53 | self.assertEqual(form2.cleaned_data['field1'], Decimal('.33')) 54 | self.assertEqual(form2.cleaned_data['field2'], Decimal('.3')) 55 | self.assertEqual(form2.cleaned_data['field3'], Decimal('0')) 56 | 57 | form3 = DivDecForm({'field1': '1/4', 'field2': '1/4', 'field3': '1/4'}) 58 | self.assertTrue(form3.is_valid()) 59 | self.assertEqual(form3.cleaned_data['field1'], Decimal('.25')) 60 | self.assertEqual(form3.cleaned_data['field2'], Decimal('.2')) 61 | self.assertEqual(form3.cleaned_data['field3'], Decimal('0')) 62 | 63 | form4 = DivDecForm({'field1': '1/2', 'field2': '1/2', 'field3': '1/2'}) 64 | self.assertTrue(form4.is_valid()) 65 | self.assertEqual(form4.cleaned_data['field1'], Decimal('.5')) 66 | self.assertEqual(form4.cleaned_data['field2'], Decimal('.5')) 67 | self.assertEqual(form4.cleaned_data['field3'], Decimal('0')) 68 | 69 | def test_negative_numerators(self): 70 | """ 71 | Test a few instances of negative numerators. 72 | """ 73 | formN1 = DivDecForm({'field1': '-1/2', 'field2': '-2/1', 'field3': '-3/3'}) 74 | self.assertTrue(formN1.is_valid()) 75 | self.assertEqual(formN1.cleaned_data['field1'], Decimal('-.5')) 76 | self.assertEqual(formN1.cleaned_data['field2'], Decimal('-2')) 77 | self.assertEqual(formN1.cleaned_data['field3'], Decimal('-1')) 78 | 79 | formN2 = DivDecForm({'field1': '-1/3', 'field2': '-1/3', 'field3': '-1/3'}) 80 | self.assertTrue(formN2.is_valid()) 81 | self.assertEqual(formN2.cleaned_data['field1'], Decimal('-.33')) 82 | self.assertEqual(formN2.cleaned_data['field2'], Decimal('-.3')) 83 | self.assertEqual(formN2.cleaned_data['field3'], Decimal('-0')) 84 | 85 | formN3 = DivDecForm({'field1': '-1/4', 'field2': '-1/4', 'field3': '-1/4'}) 86 | self.assertTrue(formN3.is_valid()) 87 | self.assertEqual(formN3.cleaned_data['field1'], Decimal('-.25')) 88 | self.assertEqual(formN3.cleaned_data['field2'], Decimal('-.2')) 89 | self.assertEqual(formN3.cleaned_data['field3'], Decimal('0')) 90 | 91 | formN4 = DivDecForm({'field1': '-1/2', 'field2': '-1/2', 'field3': '-1/2'}) 92 | self.assertTrue(formN4.is_valid()) 93 | self.assertEqual(formN4.cleaned_data['field1'], Decimal('-.5')) 94 | self.assertEqual(formN4.cleaned_data['field2'], Decimal('-.5')) 95 | self.assertEqual(formN4.cleaned_data['field3'], Decimal('0')) 96 | 97 | def test_negative_denominators(self): 98 | """ 99 | Test a few instances of negative numerators. 100 | """ 101 | formN1 = DivDecForm({'field1': '1/-2', 'field2': '2/-1', 'field3': '3/-3'}) 102 | self.assertTrue(formN1.is_valid()) 103 | self.assertEqual(formN1.cleaned_data['field1'], Decimal('-.5')) 104 | self.assertEqual(formN1.cleaned_data['field2'], Decimal('-2')) 105 | self.assertEqual(formN1.cleaned_data['field3'], Decimal('-1')) 106 | 107 | formN2 = DivDecForm({'field1': '1/-3', 'field2': '1/-3', 'field3': '1/-3'}) 108 | self.assertTrue(formN2.is_valid()) 109 | self.assertEqual(formN2.cleaned_data['field1'], Decimal('-.33')) 110 | self.assertEqual(formN2.cleaned_data['field2'], Decimal('-.3')) 111 | self.assertEqual(formN2.cleaned_data['field3'], Decimal('-0')) 112 | 113 | formN3 = DivDecForm({'field1': '1/-4', 'field2': '1/-4', 'field3': '1/-4'}) 114 | self.assertTrue(formN3.is_valid()) 115 | self.assertEqual(formN3.cleaned_data['field1'], Decimal('-.25')) 116 | self.assertEqual(formN3.cleaned_data['field2'], Decimal('-.2')) 117 | self.assertEqual(formN3.cleaned_data['field3'], Decimal('0')) 118 | 119 | formN4 = DivDecForm({'field1': '1/-2', 'field2': '1/-2', 'field3': '1/-2'}) 120 | self.assertTrue(formN4.is_valid()) 121 | self.assertEqual(formN4.cleaned_data['field1'], Decimal('-.5')) 122 | self.assertEqual(formN4.cleaned_data['field2'], Decimal('-.5')) 123 | self.assertEqual(formN4.cleaned_data['field3'], Decimal('0')) 124 | 125 | def test_divide_negative_by_negative(self): 126 | """ 127 | Ensure that -/- is positive. 128 | """ 129 | form1 = DivDecForm({'field1': '-1/-2', 'field2': '-2/-1', 'field3': '-3/-3'}) 130 | self.assertTrue(form1.is_valid()) 131 | self.assertEqual(form1.cleaned_data['field1'], Decimal('.5')) 132 | self.assertEqual(form1.cleaned_data['field2'], Decimal('2')) 133 | self.assertEqual(form1.cleaned_data['field3'], Decimal('1')) 134 | 135 | form2 = DivDecForm({'field1': '-1/-3', 'field2': '-1/-3', 'field3': '-1/-3'}) 136 | self.assertTrue(form2.is_valid()) 137 | self.assertEqual(form2.cleaned_data['field1'], Decimal('.33')) 138 | self.assertEqual(form2.cleaned_data['field2'], Decimal('.3')) 139 | self.assertEqual(form2.cleaned_data['field3'], Decimal('0')) 140 | 141 | form3 = DivDecForm({'field1': '-1/-4', 'field2': '-1/-4', 'field3': '-1/-4'}) 142 | self.assertTrue(form3.is_valid()) 143 | self.assertEqual(form3.cleaned_data['field1'], Decimal('.25')) 144 | self.assertEqual(form3.cleaned_data['field2'], Decimal('.2')) 145 | self.assertEqual(form3.cleaned_data['field3'], Decimal('0')) 146 | 147 | form4 = DivDecForm({'field1': '-1/-2', 'field2': '-1/-2', 'field3': '-1/-2'}) 148 | self.assertTrue(form4.is_valid()) 149 | self.assertEqual(form4.cleaned_data['field1'], Decimal('.5')) 150 | self.assertEqual(form4.cleaned_data['field2'], Decimal('.5')) 151 | self.assertEqual(form4.cleaned_data['field3'], Decimal('0')) 152 | 153 | 154 | class DecExprForm(Form): 155 | field1 = DecimalExpressionField(max_digits=5, decimal_places=2, required=False) 156 | field2 = DecimalExpressionField(max_digits=4, decimal_places=1) 157 | field3 = DecimalExpressionField(max_digits=5, decimal_places=0, required=False) 158 | 159 | 160 | class DecimalExpressionDivisionTests(unittest.TestCase): 161 | """Cloned from DivideDecimalField tests""" 162 | def test_base_behavior(self): 163 | """ 164 | Sanity test to make sure we didn't break the base DecimalField. 165 | """ 166 | form1 = DecExprForm({'field1': '256.32', 'field2': '123.4'}) 167 | form2 = DecExprForm({'field1': '256', 'field2': '123'}) 168 | form3 = DecExprForm({'field2': '123'}) 169 | form4 = DecExprForm({'field1': '123'}) 170 | form5 = DecExprForm({'field2': '123', 'field3': '456'}) 171 | form6 = DecExprForm({'field2': '123', 'field3': '456.0'}) 172 | form7 = DecExprForm({'field1': '', 'field2': '0.0'}) 173 | self.assertTrue(form1.is_valid()) 174 | self.assertEqual(form1.cleaned_data['field1'], Decimal('256.32')) 175 | self.assertEqual(form1.cleaned_data['field2'], Decimal('123.4')) 176 | self.assertTrue(form2.is_valid()) 177 | self.assertEqual(form2.cleaned_data['field1'], Decimal('256')) 178 | self.assertEqual(form2.cleaned_data['field2'], Decimal('123')) 179 | self.assertTrue(form3.is_valid()) 180 | self.assertFalse(form4.is_valid()) 181 | self.assertTrue(form5.is_valid()) 182 | # The assertion below would fail since all inputs are rounded 183 | # to the correct number of digits after the decimal. This is 184 | # documented in the README. 185 | # self.assertFalse(form6.is_valid()) 186 | self.assertTrue(form7.is_valid()) 187 | self.assertEqual(form7.cleaned_data['field1'], None) 188 | self.assertEqual(form7.cleaned_data['field2'], Decimal('0')) 189 | self.assertEqual(form7.cleaned_data['field3'], None) 190 | 191 | def test_divide(self): 192 | """ 193 | Test a few instances of fields with a '/'. 194 | """ 195 | form1 = DecExprForm({'field1': '1/2', 'field2': '2/1', 'field3': '3/3'}) 196 | self.assertTrue(form1.is_valid()) 197 | self.assertEqual(form1.cleaned_data['field1'], Decimal('.5')) 198 | self.assertEqual(form1.cleaned_data['field2'], Decimal('2')) 199 | self.assertEqual(form1.cleaned_data['field3'], Decimal('1')) 200 | 201 | form2 = DecExprForm({'field1': '1/3', 'field2': '1/3', 'field3': '1/3'}) 202 | self.assertTrue(form2.is_valid()) 203 | self.assertEqual(form2.cleaned_data['field1'], Decimal('.33')) 204 | self.assertEqual(form2.cleaned_data['field2'], Decimal('.3')) 205 | self.assertEqual(form2.cleaned_data['field3'], Decimal('0')) 206 | 207 | form3 = DecExprForm({'field1': '1/4', 'field2': '1/4', 'field3': '1/4'}) 208 | self.assertTrue(form3.is_valid()) 209 | self.assertEqual(form3.cleaned_data['field1'], Decimal('.25')) 210 | self.assertEqual(form3.cleaned_data['field2'], Decimal('.2')) 211 | self.assertEqual(form3.cleaned_data['field3'], Decimal('0')) 212 | 213 | form4 = DecExprForm({'field1': '1/2', 'field2': '1/2', 'field3': '1/2'}) 214 | self.assertTrue(form4.is_valid()) 215 | self.assertEqual(form4.cleaned_data['field1'], Decimal('.5')) 216 | self.assertEqual(form4.cleaned_data['field2'], Decimal('.5')) 217 | self.assertEqual(form4.cleaned_data['field3'], Decimal('0')) 218 | 219 | def test_negative_numerators(self): 220 | """ 221 | Test a few instances of negative numerators. 222 | """ 223 | formN1 = DecExprForm({'field1': '-1/2', 'field2': '-2/1', 'field3': '-3/3'}) 224 | self.assertTrue(formN1.is_valid()) 225 | self.assertEqual(formN1.cleaned_data['field1'], Decimal('-.5')) 226 | self.assertEqual(formN1.cleaned_data['field2'], Decimal('-2')) 227 | self.assertEqual(formN1.cleaned_data['field3'], Decimal('-1')) 228 | 229 | formN2 = DecExprForm({'field1': '-1/3', 'field2': '-1/3', 'field3': '-1/3'}) 230 | self.assertTrue(formN2.is_valid()) 231 | self.assertEqual(formN2.cleaned_data['field1'], Decimal('-.33')) 232 | self.assertEqual(formN2.cleaned_data['field2'], Decimal('-.3')) 233 | self.assertEqual(formN2.cleaned_data['field3'], Decimal('-0')) 234 | 235 | formN3 = DecExprForm({'field1': '-1/4', 'field2': '-1/4', 'field3': '-1/4'}) 236 | self.assertTrue(formN3.is_valid()) 237 | self.assertEqual(formN3.cleaned_data['field1'], Decimal('-.25')) 238 | self.assertEqual(formN3.cleaned_data['field2'], Decimal('-.2')) 239 | self.assertEqual(formN3.cleaned_data['field3'], Decimal('0')) 240 | 241 | formN4 = DecExprForm({'field1': '-1/2', 'field2': '-1/2', 'field3': '-1/2'}) 242 | self.assertTrue(formN4.is_valid()) 243 | self.assertEqual(formN4.cleaned_data['field1'], Decimal('-.5')) 244 | self.assertEqual(formN4.cleaned_data['field2'], Decimal('-.5')) 245 | self.assertEqual(formN4.cleaned_data['field3'], Decimal('0')) 246 | 247 | def test_negative_denominators(self): 248 | """ 249 | Test a few instances of negative numerators. 250 | """ 251 | formN1 = DecExprForm({'field1': '1/-2', 'field2': '2/-1', 'field3': '3/-3'}) 252 | self.assertTrue(formN1.is_valid()) 253 | self.assertEqual(formN1.cleaned_data['field1'], Decimal('-.5')) 254 | self.assertEqual(formN1.cleaned_data['field2'], Decimal('-2')) 255 | self.assertEqual(formN1.cleaned_data['field3'], Decimal('-1')) 256 | 257 | formN2 = DecExprForm({'field1': '1/-3', 'field2': '1/-3', 'field3': '1/-3'}) 258 | self.assertTrue(formN2.is_valid()) 259 | self.assertEqual(formN2.cleaned_data['field1'], Decimal('-.33')) 260 | self.assertEqual(formN2.cleaned_data['field2'], Decimal('-.3')) 261 | self.assertEqual(formN2.cleaned_data['field3'], Decimal('-0')) 262 | 263 | formN3 = DecExprForm({'field1': '1/-4', 'field2': '1/-4', 'field3': '1/-4'}) 264 | self.assertTrue(formN3.is_valid()) 265 | self.assertEqual(formN3.cleaned_data['field1'], Decimal('-.25')) 266 | self.assertEqual(formN3.cleaned_data['field2'], Decimal('-.2')) 267 | self.assertEqual(formN3.cleaned_data['field3'], Decimal('0')) 268 | 269 | formN4 = DecExprForm({'field1': '1/-2', 'field2': '1/-2', 'field3': '1/-2'}) 270 | self.assertTrue(formN4.is_valid()) 271 | self.assertEqual(formN4.cleaned_data['field1'], Decimal('-.5')) 272 | self.assertEqual(formN4.cleaned_data['field2'], Decimal('-.5')) 273 | self.assertEqual(formN4.cleaned_data['field3'], Decimal('0')) 274 | 275 | def test_divide_negative_by_negative(self): 276 | """ 277 | Ensure that -/- is positive. 278 | """ 279 | form1 = DecExprForm({'field1': '-1/-2', 'field2': '-2/-1', 'field3': '-3/-3'}) 280 | self.assertTrue(form1.is_valid()) 281 | self.assertEqual(form1.cleaned_data['field1'], Decimal('.5')) 282 | self.assertEqual(form1.cleaned_data['field2'], Decimal('2')) 283 | self.assertEqual(form1.cleaned_data['field3'], Decimal('1')) 284 | 285 | form2 = DecExprForm({'field1': '-1/-3', 'field2': '-1/-3', 'field3': '-1/-3'}) 286 | self.assertTrue(form2.is_valid()) 287 | self.assertEqual(form2.cleaned_data['field1'], Decimal('.33')) 288 | self.assertEqual(form2.cleaned_data['field2'], Decimal('.3')) 289 | self.assertEqual(form2.cleaned_data['field3'], Decimal('0')) 290 | 291 | form3 = DecExprForm({'field1': '-1/-4', 'field2': '-1/-4', 'field3': '-1/-4'}) 292 | self.assertTrue(form3.is_valid()) 293 | self.assertEqual(form3.cleaned_data['field1'], Decimal('.25')) 294 | self.assertEqual(form3.cleaned_data['field2'], Decimal('.2')) 295 | self.assertEqual(form3.cleaned_data['field3'], Decimal('0')) 296 | 297 | form4 = DecExprForm({'field1': '-1/-2', 'field2': '-1/-2', 'field3': '-1/-2'}) 298 | self.assertTrue(form4.is_valid()) 299 | self.assertEqual(form4.cleaned_data['field1'], Decimal('.5')) 300 | self.assertEqual(form4.cleaned_data['field2'], Decimal('.5')) 301 | self.assertEqual(form4.cleaned_data['field3'], Decimal('0')) 302 | 303 | 304 | class DecimalExpressionTests(unittest.TestCase): 305 | """more interesting expressions to test""" 306 | def test_valid(self): 307 | """ 308 | Some simple valid expressions. 309 | """ 310 | form1 = DecExprForm({'field1': '-2+1', 'field2': '1-2', 'field3': '1+-2'}) 311 | self.assertTrue(form1.is_valid()) 312 | self.assertEqual(form1.cleaned_data['field1'], Decimal('-1')) 313 | self.assertEqual(form1.cleaned_data['field2'], Decimal('-1')) 314 | self.assertEqual(form1.cleaned_data['field3'], Decimal('-1')) 315 | 316 | form2 = DecExprForm({'field1': 'pi', 'field2': 'pi', 'field3': 'pi'}) 317 | self.assertTrue(form2.is_valid()) 318 | self.assertEqual(form2.cleaned_data['field1'], Decimal('3.14')) 319 | self.assertEqual(form2.cleaned_data['field2'], Decimal('3.1')) 320 | self.assertEqual(form2.cleaned_data['field3'], Decimal('3')) 321 | 322 | form3 = DecExprForm({'field1': 'sin(90)', 'field2': 'cos(-180)*100', 'field3': 'pow(2,5)'}) 323 | self.assertTrue(form3.is_valid()) 324 | self.assertEqual(form3.cleaned_data['field1'], Decimal('0.89')) 325 | self.assertEqual(form3.cleaned_data['field2'], Decimal('-59.8')) 326 | self.assertEqual(form3.cleaned_data['field3'], Decimal('32')) 327 | 328 | form4 = DecExprForm({'field1': 'abs(-1/2)', 'field2': 'abs(-10)', 'field3': 'sqrt(4)'}) 329 | self.assertTrue(form4.is_valid()) 330 | self.assertEqual(form4.cleaned_data['field1'], Decimal('0.5')) 331 | self.assertEqual(form4.cleaned_data['field2'], Decimal('10')) 332 | self.assertEqual(form4.cleaned_data['field3'], Decimal('2')) 333 | 334 | def test_invalid(self): 335 | """ 336 | Some ways to try and blow it up. 337 | """ 338 | formN1 = DecExprForm({'field1': 'ceil', 'field2': 'int', 'field3': '2+x'}) 339 | self.assertFalse(formN1.is_valid()) 340 | 341 | formN2 = DecExprForm({'field1': '-', 'field2': '*', 'field3': '2++3'}) 342 | self.assertFalse(formN2.is_valid()) 343 | 344 | formN3 = DecExprForm({'field1': '1**2', 'field2': 'pow(1,2,3)', 'field3': 'abs(5,6)'}) 345 | self.assertFalse(formN3.is_valid()) 346 | --------------------------------------------------------------------------------