├── tests
├── __init__.py
├── models.py
├── test_validators.py
├── test_models.py
└── test_basic.py
├── requirements.txt
├── requirements-test.txt
├── setup.cfg
├── docs
├── index.rst
├── changelog.rst
├── conf.py
├── api.rst
└── usage.rst
├── .travis.yml
├── .gitignore
├── LICENSE
├── makefile
├── setup.py
├── README.rst
└── peewee_validates.py
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | peewee>=2.8.2,<4.0.0
2 | python-dateutil>=2.5.0
3 |
--------------------------------------------------------------------------------
/requirements-test.txt:
--------------------------------------------------------------------------------
1 | -r requirements.txt
2 |
3 | pytest==2.9.0
4 | pytest-cov==2.2.1
5 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [bumpversion]
2 | current_version = 1.0.5
3 | commit = True
4 | tag = True
5 |
6 | [bumpversion:file:peewee_validates.py]
7 |
8 | [wheel]
9 | universal = 1
10 |
11 | [flake8]
12 | max-line-length = 100
13 |
14 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../README.rst
2 |
3 | Contents
4 | ========
5 |
6 | .. toctree::
7 | :maxdepth: 2
8 | :glob:
9 |
10 | usage
11 | api
12 | changelog
13 |
14 |
15 | License
16 | =======
17 |
18 | .. include:: ../LICENSE
19 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | sudo: false
3 | python:
4 | - "3.4"
5 | - "3.5"
6 | - "3.6"
7 | env:
8 | - PEEWEE_VERSION=2.8.2
9 | - PEEWEE_VERSION=3.0.15
10 | install:
11 | - "pip install peewee==$PEEWEE_VERSION"
12 | - "pip install python-coveralls==2.7.0"
13 | - "pip install -r requirements-test.txt"
14 | before_script:
15 | - "coverage erase"
16 | script:
17 | - "py.test --cov=peewee_validates.py"
18 | after_success:
19 | - "coverage report"
20 | - "coveralls"
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # project files
2 | .idea
3 | .codeintel
4 | .vscode
5 | *.sublime-project
6 | *.sublime-workspace
7 |
8 | # python
9 | .env
10 | __pycache__/
11 | *.py[cod]
12 |
13 | # packaging
14 | env/
15 | build/
16 | develop-eggs/
17 | dist/
18 | downloads/
19 | eggs/
20 | .eggs/
21 | lib/
22 | lib64/
23 | parts/
24 | sdist/
25 | var/
26 | *.egg-info/
27 | .installed.cfg
28 | *.egg
29 |
30 | # testing
31 | htmlcov
32 | .coverage
33 | *.db
34 | .cache
35 |
36 | # logs
37 | pip-log.txt
38 |
39 | # compiled documentation
40 | docs/_build/
41 |
--------------------------------------------------------------------------------
/docs/changelog.rst:
--------------------------------------------------------------------------------
1 | Changelog
2 | #########
3 |
4 | 1.0.7
5 | =====
6 | - Fix compatibility with Peewee 3
7 |
8 | 1.0.6
9 | =====
10 | - Make compatible with Peewee 3
11 |
12 | 1.0.5
13 | =====
14 | - Fixed issue with converting DecimalField.
15 | - Fixed issue with converting IntegerField.
16 | - Fixed issue with using a list of non-string in one_of_validator.
17 |
18 | 1.0.4
19 | =====
20 | - Fixed issue with ModelValidator field overrides.
21 |
22 | 1.0.3
23 | =====
24 | - Fixed issue coercing numeric fields.
25 |
26 | 1.0.2
27 | =====
28 | - Fixed issue when passing dicts and validating unique indexes.
29 |
30 | 1.0.1
31 | =====
32 | - Fixed issue where ``None`` values were failing validators.
33 |
34 | 1.0.0
35 | =====
36 | - Major rewrite and first release with proper documentation.
37 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import os
3 |
4 | src_dir = os.path.realpath(os.path.dirname(os.path.dirname(__file__)))
5 | sys.path.insert(0, src_dir)
6 |
7 | from peewee_validates import __version__
8 |
9 | project = 'peewee-validates'
10 | copyright = 'Tim Shaffer'
11 | version = __version__
12 | release = __version__
13 |
14 | extensions = [
15 | 'sphinx.ext.autodoc',
16 | ]
17 |
18 | add_module_names = False
19 |
20 | # templates_path = ['_templates']
21 |
22 | source_suffix = '.rst'
23 |
24 | master_doc = 'index'
25 |
26 | exclude_patterns = ['_build']
27 |
28 | pygments_style = 'pastie'
29 |
30 | html_theme = 'sphinx_rtd_theme'
31 |
32 | # html_static_path = ['_static']
33 |
34 | htmlhelp_basename = 'peewee-validates'
35 |
36 | man_pages = [
37 | ('index', 'peewee-validates', 'peewee-validates documentation', ['Tim Shaffer'], 1)
38 | ]
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Tim Shaffer
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
6 | associated documentation files (the "Software"), to deal in the Software without restriction,
7 | including without limitation the rights to use, copy, modify, merge, publish, distribute,
8 | sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all copies or substantial
12 | portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
15 | NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
16 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
17 | OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19 |
--------------------------------------------------------------------------------
/makefile:
--------------------------------------------------------------------------------
1 | .PHONY: clean dist docs install lint release test
2 |
3 | help:
4 | @echo "clean remove all build, test, coverage, and Python artifacts"
5 | @echo "dist create a package"
6 | @echo "install install the package to the active Python's site-packages"
7 | @echo "release package and upload a release"
8 | @echo "test run tests quickly with the default Python"
9 | @echo "docs build documentation"
10 |
11 | clean:
12 | rm -fr build/
13 | rm -fr dist/
14 | rm -fr .eggs/
15 | rm -fr docs/_build/
16 | rm -fr .coverage htmlcov/
17 | find . -name '*.egg-info' -exec rm -fr {} +
18 | find . -name '*.egg' -exec rm -f {} +
19 | find . -name '*.pyc' -exec rm -f {} +
20 | find . -name '*.pyo' -exec rm -f {} +
21 | find . -name '*~' -exec rm -f {} +
22 | find . -name '__pycache__' -exec rm -fr {} +
23 |
24 | install: clean
25 | python setup.py install
26 |
27 | dist: clean
28 | python setup.py sdist
29 | python setup.py bdist_wheel
30 |
31 | release: clean
32 | python setup.py sdist upload -r pypi
33 | python setup.py bdist_wheel upload -r pypi
34 |
35 | test:
36 | py.test
37 |
38 | docs:
39 | sphinx-build -b html docs/ docs/_build/html
40 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 | from codecs import open
3 | from os import path
4 |
5 | # from peewee_validates import __version__ # the build requires preinstalled peewee and datautils
6 | __version__ = '1.0.8'
7 |
8 | root_dir = path.abspath(path.dirname(__file__))
9 |
10 | with open(path.join(root_dir, 'README.rst'), encoding='utf-8') as f:
11 | long_description = f.read()
12 |
13 | with open(path.join(root_dir, 'requirements.txt'), encoding='utf-8') as f:
14 | install_requires = list(map(str.strip, f.readlines()))
15 |
16 | setup(
17 | name='peewee-validates',
18 | version=__version__,
19 |
20 | description='Simple and flexible model validator for Peewee ORM.',
21 | long_description=long_description,
22 |
23 | url='https://github.com/timster/peewee-validates',
24 |
25 | author='Tim Shaffer',
26 | author_email='timshaffer@me.com',
27 |
28 | license='MIT',
29 |
30 | classifiers=[
31 | 'Development Status :: 3 - Alpha',
32 |
33 | 'Intended Audience :: Developers',
34 | 'Topic :: Database :: Front-Ends',
35 |
36 | 'License :: OSI Approved :: MIT License',
37 |
38 | 'Programming Language :: Python :: 3.3',
39 | 'Programming Language :: Python :: 3.4',
40 | 'Programming Language :: Python :: 3.5',
41 | ],
42 |
43 | keywords='peewee orm database form validation development',
44 |
45 | py_modules=['peewee_validates'],
46 |
47 | install_requires=install_requires,
48 | )
49 |
--------------------------------------------------------------------------------
/docs/api.rst:
--------------------------------------------------------------------------------
1 | API Documentation
2 | #################
3 |
4 | Validator Classes
5 | =================
6 |
7 | .. autoclass:: peewee_validates.Validator
8 | :members:
9 |
10 | .. autoclass:: peewee_validates.ModelValidator
11 | :members:
12 |
13 | Fields
14 | ======
15 |
16 | .. autoclass:: peewee_validates.Field
17 | .. autoclass:: peewee_validates.StringField
18 | .. autoclass:: peewee_validates.FloatField
19 | .. autoclass:: peewee_validates.IntegerField
20 | .. autoclass:: peewee_validates.DecimalField
21 | .. autoclass:: peewee_validates.DateField
22 | .. autoclass:: peewee_validates.TimeField
23 | .. autoclass:: peewee_validates.DateTimeField
24 | .. autoclass:: peewee_validates.BooleanField
25 | .. autoclass:: peewee_validates.ModelChoiceField
26 | .. autoclass:: peewee_validates.ManyModelChoiceField
27 |
28 | Field Validators
29 | ================
30 |
31 | This module includes some basic validators, but it's pretty easy to write your own if needed.
32 |
33 | All validators return a function that can be used to validate a field. For example:
34 |
35 | .. code-block:: python
36 |
37 | validator = peewee_validates.validate_required()
38 |
39 | # Raises ValidationError since no data was provided for this field.
40 | field = StringField()
41 | validator(field, {})
42 |
43 | # Does not raise any error since default data was provided.
44 | field = StringField(default='something')
45 | validator(field, {})
46 |
47 | .. autofunction:: peewee_validates.validate_email
48 | .. autofunction:: peewee_validates.validate_equal
49 | .. autofunction:: peewee_validates.validate_function
50 | .. autofunction:: peewee_validates.validate_length
51 | .. autofunction:: peewee_validates.validate_matches
52 | .. autofunction:: peewee_validates.validate_model_unique
53 | .. autofunction:: peewee_validates.validate_none_of
54 | .. autofunction:: peewee_validates.validate_not_empty
55 | .. autofunction:: peewee_validates.validate_one_of
56 | .. autofunction:: peewee_validates.validate_range
57 | .. autofunction:: peewee_validates.validate_regexp
58 | .. autofunction:: peewee_validates.validate_required
59 |
--------------------------------------------------------------------------------
/tests/models.py:
--------------------------------------------------------------------------------
1 | import peewee
2 | try:
3 | M2M_RELATED = 'related_name'
4 | from playhouse.fields import ManyToManyField
5 | except ImportError:
6 | M2M_RELATED = 'backref'
7 | from peewee import ManyToManyField
8 |
9 | database = peewee.SqliteDatabase(':memory:')
10 |
11 |
12 | def getname():
13 | return 'Tim'
14 |
15 |
16 | class BasicFields(peewee.Model):
17 | field1 = peewee.CharField(default=getname)
18 | field2 = peewee.CharField()
19 | field3 = peewee.CharField()
20 |
21 | class Meta:
22 | database = database
23 | indexes = (
24 | (('field1', 'field2'), True),
25 | (('field3',), False),
26 | )
27 |
28 |
29 | class Organization(peewee.Model):
30 | name = peewee.CharField(null=False)
31 |
32 | class Meta:
33 | database = database
34 |
35 |
36 | class PayGrade(peewee.Model):
37 | name = peewee.CharField(null=False)
38 |
39 | class Meta:
40 | database = database
41 |
42 |
43 | class Person(peewee.Model):
44 | name = peewee.CharField(null=False, max_length=5, unique=True)
45 |
46 | class Meta:
47 | database = database
48 |
49 |
50 | class ComplexPerson(Person):
51 | GENDER_CHOICES = (('M', 'Male'), ('F', 'Female'))
52 | gender = peewee.CharField(choices=GENDER_CHOICES)
53 |
54 | organization = peewee.ForeignKeyField(Organization, null=False)
55 | pay_grade = peewee.ForeignKeyField(PayGrade, null=True)
56 |
57 | class Meta:
58 | database = database
59 | indexes = (
60 | (('gender', 'name'), True),
61 | (('name', 'organization'), True),
62 | )
63 |
64 |
65 | class Student(peewee.Model):
66 | name = peewee.CharField(max_length=10)
67 |
68 | class Meta:
69 | database = database
70 |
71 |
72 | class Course(peewee.Model):
73 | name = peewee.CharField(max_length=10)
74 |
75 | params = {M2M_RELATED: 'courses'}
76 | students = ManyToManyField(Student, **params)
77 |
78 | class Meta:
79 | database = database
80 |
81 |
82 | Organization.create_table(fail_silently=True)
83 | PayGrade.create_table(fail_silently=True)
84 | ComplexPerson.create_table(fail_silently=True)
85 | Person.create_table(fail_silently=True)
86 | BasicFields.create_table(fail_silently=True)
87 |
88 | Student.create_table(fail_silently=True)
89 | Course.create_table(fail_silently=True)
90 | Course.students.get_through_model().create_table(fail_silently=True)
91 |
92 | organization = Organization(name='main')
93 | organization.save()
94 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | peewee-validates
2 | ################
3 |
4 | A simple and flexible model and data validator for `Peewee ORM `_.
5 |
6 | .. image:: http://img.shields.io/travis/timster/peewee-validates.svg?style=flat
7 | :target: http://travis-ci.org/timster/peewee-validates
8 | :alt: Build Status
9 |
10 | .. image:: http://img.shields.io/coveralls/timster/peewee-validates.svg?style=flat
11 | :target: https://coveralls.io/r/timster/peewee-validates
12 | :alt: Code Coverage
13 |
14 | .. image:: http://img.shields.io/pypi/v/peewee-validates.svg?style=flat
15 | :target: https://pypi.python.org/pypi/peewee-validates
16 | :alt: Version
17 |
18 | .. image:: http://img.shields.io/pypi/dm/peewee-validates.svg?style=flat
19 | :target: https://pypi.python.org/pypi/peewee-validates
20 | :alt: Downloads
21 |
22 | .. image:: https://readthedocs.org/projects/peewee-validates/badge/?version=latest
23 | :target: https://peewee-validates.readthedocs.io
24 | :alt: Documentation
25 |
26 | Requirements
27 | ============
28 |
29 | * python >= 3.3
30 | * peewee >= 2.8.2 (including Peewee 3)
31 | * python-dateutil >= 2.5.0
32 |
33 | Installation
34 | ============
35 |
36 | This package can be installed using pip:
37 |
38 | ::
39 |
40 | pip install peewee-validates
41 |
42 | Usage
43 | =====
44 |
45 | Here's a quick teaser of what you can do with peewee-validates:
46 |
47 | .. code:: python
48 |
49 | import peewee
50 | from peewee_validates import ModelValidator
51 |
52 | class Category(peewee.Model):
53 | code = peewee.IntegerField(unique=True)
54 | name = peewee.CharField(null=False, max_length=250)
55 |
56 | obj = Category(code=42)
57 |
58 | validator = ModelValidator(obj)
59 | validator.validate()
60 |
61 | print(validator.errors)
62 |
63 | # {'name': 'This field is required.', 'code': 'Must be a unique value.'}
64 |
65 | In fact, there is also a generic validator that does not even require a model:
66 |
67 | .. code:: python
68 |
69 | from peewee_validates import Validator, StringField
70 |
71 | class SimpleValidator(Validator):
72 | name = StringField(required=True, max_length=250)
73 | code = StringField(required=True, max_length=4)
74 |
75 | validator = SimpleValidator(obj)
76 | validator.validate({'code': 'toolong'})
77 |
78 | print(validator.errors)
79 |
80 | # {'name': 'This field is required.', 'code': 'Must be at most 5 characters.'}
81 |
82 | Documentation
83 | =============
84 |
85 | Check out the `Full Documentation `_ for more details.
86 |
--------------------------------------------------------------------------------
/tests/test_validators.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from peewee_validates import ValidationError
4 | from peewee_validates import StringField
5 | from peewee_validates import validate_email
6 | from peewee_validates import validate_equal
7 | from peewee_validates import validate_function
8 | from peewee_validates import validate_length
9 | from peewee_validates import validate_matches
10 | from peewee_validates import validate_none_of
11 | from peewee_validates import validate_not_empty
12 | from peewee_validates import validate_one_of
13 | from peewee_validates import validate_range
14 | from peewee_validates import validate_regexp
15 | from peewee_validates import validate_required
16 |
17 | field = StringField()
18 |
19 |
20 | def test_validate_required():
21 | validator = validate_required()
22 |
23 | for value in (None,):
24 | field.value = value
25 | with pytest.raises(ValidationError):
26 | validator(field, {})
27 |
28 | for value in ('okay', '', ' '):
29 | field.value = value
30 | validator(field, {})
31 |
32 |
33 | def test_validate_not_empty():
34 | validator = validate_not_empty()
35 |
36 | for value in ('', ' '):
37 | field.value = value
38 | with pytest.raises(ValidationError):
39 | validator(field, {})
40 |
41 | for value in (None, 'alright', '123'):
42 | field.value = value
43 | validator(field, {})
44 |
45 |
46 | def test_validate_length():
47 | validator = validate_length(low=2, high=None, equal=None)
48 |
49 | for value in ('1', [1]):
50 | field.value = value
51 | with pytest.raises(ValidationError):
52 | validator(field, {})
53 |
54 | for value in (None, '22', 'longer', [1, 2]):
55 | field.value = value
56 | validator(field, {})
57 |
58 | validator = validate_length(low=None, high=2, equal=None)
59 |
60 | for value in ('123', [1, 2, 3]):
61 | field.value = value
62 | with pytest.raises(ValidationError):
63 | validator(field, {})
64 |
65 | for value in (None, '22', '', [1, 2]):
66 | field.value = value
67 | validator(field, {})
68 |
69 | validator = validate_length(low=None, high=None, equal=2)
70 |
71 | for value in ('242', '', [1, 2, 3]):
72 | field.value = value
73 | with pytest.raises(ValidationError):
74 | validator(field, {})
75 |
76 | for value in (None, '22', [1, 2]):
77 | field.value = value
78 | validator(field, {})
79 |
80 | validator = validate_length(low=2, high=4, equal=None)
81 |
82 | for value in ('1', '', [1, 2, 3, 4, 5]):
83 | field.value = value
84 | with pytest.raises(ValidationError):
85 | validator(field, {})
86 |
87 | for value in (None, '22', '2222', [1, 2]):
88 | field.value = value
89 | validator(field, {})
90 |
91 |
92 | def test_validate_one_of():
93 | validator = validate_one_of(('a', 'b', 'c'))
94 |
95 | for value in ('1', '', [1, 2, 3, 4, 5]):
96 | field.value = value
97 | with pytest.raises(ValidationError):
98 | validator(field, {})
99 |
100 | for value in (None, 'a', 'b', 'c'):
101 | field.value = value
102 | validator(field, {})
103 |
104 |
105 | def test_validate_none_of():
106 | validator = validate_none_of(('a', 'b', 'c'))
107 |
108 | for value in ('a', 'b', 'c'):
109 | field.value = value
110 | with pytest.raises(ValidationError):
111 | validator(field, {})
112 |
113 | for value in (None, '1', '', [1, 2, 3, 4, 5]):
114 | field.value = value
115 | validator(field, {})
116 |
117 |
118 | def test_validate_range():
119 | validator = validate_range(low=10, high=100)
120 |
121 | for value in (8, 800):
122 | field.value = value
123 | with pytest.raises(ValidationError):
124 | validator(field, {})
125 |
126 | for value in (None, 10, 100):
127 | field.value = value
128 | validator(field, {})
129 |
130 |
131 | def test_validate_equal():
132 | validator = validate_equal('yes')
133 |
134 | for value in ('no', 100):
135 | field.value = value
136 | with pytest.raises(ValidationError):
137 | validator(field, {})
138 |
139 | for value in (None, 'yes'):
140 | field.value = value
141 | validator(field, {})
142 |
143 |
144 | def test_validate_matches():
145 | validator = validate_matches('other')
146 |
147 | for value in ('no', 100):
148 | field.value = value
149 | with pytest.raises(ValidationError):
150 | validator(field, {'other': 'yes'})
151 |
152 | for value in (None, 'yes'):
153 | field.value = value
154 | validator(field, {'other': 'yes'})
155 |
156 |
157 | def test_validate_regexp():
158 | validator = validate_regexp('^[a-z]{3}$', flags=0)
159 |
160 | for value in ('123', 'abcd', [123, 123]):
161 | field.value = value
162 | with pytest.raises(ValidationError):
163 | validator(field, {'other': 'yes'})
164 |
165 | for value in (None, 'yes', 'abc'):
166 | field.value = value
167 | validator(field, {'other': 'yes'})
168 |
169 |
170 | def test_validate_function():
171 | def verify(value, check):
172 | return value == check
173 |
174 | validator = validate_function(verify, check='tim')
175 |
176 | for value in ('123', 'abcd', [123, 123]):
177 | field.value = value
178 | with pytest.raises(ValidationError):
179 | validator(field, {'other': 'yes'})
180 |
181 | for value in (None, 'tim'):
182 | field.value = value
183 | validator(field, {'other': 'yes'})
184 |
185 |
186 | def test_validate_email():
187 | validator = validate_email()
188 |
189 | for value in ('bad', '())@asdfsd.com', 'tim@().com', [123, 123]):
190 | field.value = value
191 | with pytest.raises(ValidationError):
192 | validator(field, {'other': 'yes'})
193 |
194 | for value in (None, 'tim@example.com', 'tim@localhost'):
195 | field.value = value
196 | validator(field, {'other': 'yes'})
197 |
--------------------------------------------------------------------------------
/tests/test_models.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from peewee_validates import DEFAULT_MESSAGES
4 | from peewee_validates import ModelValidator
5 | from peewee_validates import ValidationError
6 | from peewee_validates import ManyModelChoiceField
7 |
8 | from tests.models import BasicFields
9 | from tests.models import ComplexPerson
10 | from tests.models import Course
11 | from tests.models import Organization
12 | from tests.models import Person
13 | from tests.models import Student
14 |
15 |
16 | def test_not_instance():
17 | with pytest.raises(AttributeError):
18 | ModelValidator(Person)
19 |
20 |
21 | def test_instance():
22 | instance = Person()
23 | validator = ModelValidator(instance)
24 | valid = validator.validate({'name': 'tim'})
25 | assert valid
26 | assert validator.data['name'] == 'tim'
27 |
28 |
29 | def test_required():
30 | validator = ModelValidator(Person())
31 | valid = validator.validate()
32 | assert not valid
33 | assert validator.errors['name'] == DEFAULT_MESSAGES['required']
34 |
35 |
36 | def test_clean():
37 | class TestValidator(ModelValidator):
38 | def clean(self, data):
39 | super().clean(data)
40 | data['name'] += 'awesome'
41 | return data
42 |
43 | validator = TestValidator(Person())
44 | valid = validator.validate({'name': 'tim'})
45 | assert valid
46 | assert validator.data['name'] == 'timawesome'
47 |
48 |
49 | def test_clean_error():
50 | class TestValidator(ModelValidator):
51 | def clean(self, data):
52 | raise ValidationError('required')
53 |
54 | validator = TestValidator(Person())
55 | valid = validator.validate({'name': 'tim'})
56 | assert not valid
57 | assert validator.data['name'] == 'tim'
58 | assert validator.errors['__base__'] == DEFAULT_MESSAGES['required']
59 |
60 |
61 | def test_choices():
62 | validator = ModelValidator(ComplexPerson(name='tim'))
63 |
64 | valid = validator.validate({'organization': 1, 'gender': 'S'})
65 | assert not valid
66 | assert validator.errors['gender'] == DEFAULT_MESSAGES['one_of'].format(choices='M, F')
67 | assert 'name' not in validator.errors
68 |
69 | valid = validator.validate({'organization': 1, 'gender': 'M'})
70 | assert valid
71 |
72 |
73 | def test_default():
74 | validator = ModelValidator(BasicFields())
75 | valid = validator.validate()
76 | assert not valid
77 | assert validator.data['field1'] == 'Tim'
78 | assert validator.errors['field2'] == DEFAULT_MESSAGES['required']
79 | assert validator.errors['field3'] == DEFAULT_MESSAGES['required']
80 |
81 |
82 | def test_related_required_missing():
83 | validator = ModelValidator(ComplexPerson(name='tim', gender='M'))
84 |
85 | valid = validator.validate({'organization': 999})
86 | assert not valid
87 | assert validator.errors['organization'] == DEFAULT_MESSAGES['related'].format(field='id', values=999)
88 |
89 | valid = validator.validate({'organization': None})
90 | assert not valid
91 | assert validator.errors['organization'] == DEFAULT_MESSAGES['required']
92 |
93 | valid = validator.validate()
94 | assert not valid
95 | assert validator.errors['organization'] == DEFAULT_MESSAGES['required']
96 |
97 |
98 | def test_related_optional_missing():
99 | validator = ModelValidator(ComplexPerson(name='tim', gender='M', organization=1))
100 |
101 | valid = validator.validate({'pay_grade': 999})
102 | assert not valid
103 | assert validator.errors['pay_grade'] == DEFAULT_MESSAGES['related'].format(field='id', values=999)
104 |
105 | valid = validator.validate({'pay_grade': None})
106 | assert valid
107 |
108 | valid = validator.validate()
109 | assert valid
110 |
111 |
112 | def test_related_required_int():
113 | org = Organization.create(name='new1')
114 | validator = ModelValidator(ComplexPerson(name='tim', gender='M'))
115 | valid = validator.validate({'organization': org.id})
116 | assert valid
117 |
118 |
119 | def test_related_required_instance():
120 | org = Organization.create(name='new1')
121 | validator = ModelValidator(ComplexPerson(name='tim', gender='M'))
122 | valid = validator.validate({'organization': org})
123 | assert valid
124 |
125 |
126 | def test_related_required_dict():
127 | org = Organization.create(name='new1')
128 | validator = ModelValidator(ComplexPerson(name='tim', gender='M'))
129 | valid = validator.validate({'organization': {'id': org.id}})
130 | assert valid
131 |
132 |
133 | def test_related_required_dict_missing():
134 | validator = ModelValidator(ComplexPerson(name='tim', gender='M'))
135 | validator.validate({'organization': {}})
136 | assert validator.errors['organization'] == DEFAULT_MESSAGES['required']
137 |
138 |
139 | def test_related_optional_dict_missing():
140 | validator = ModelValidator(ComplexPerson(name='tim', gender='M', organization=1))
141 | valid = validator.validate({'pay_grade': {}})
142 | assert valid
143 |
144 |
145 | def test_unique():
146 | person = Person.create(name='tim')
147 |
148 | validator = ModelValidator(Person(name='tim'))
149 | valid = validator.validate({'gender': 'M'})
150 | assert not valid
151 | assert validator.errors['name'] == DEFAULT_MESSAGES['unique']
152 |
153 | validator = ModelValidator(person)
154 | valid = validator.validate({'gender': 'M'})
155 | assert valid
156 |
157 |
158 | def test_unique_index():
159 | obj1 = BasicFields.create(field1='one', field2='two', field3='three')
160 | obj2 = BasicFields(field1='one', field2='two', field3='three')
161 |
162 | validator = ModelValidator(obj2)
163 | valid = validator.validate()
164 | assert not valid
165 | assert validator.errors['field1'] == DEFAULT_MESSAGES['index']
166 | assert validator.errors['field2'] == DEFAULT_MESSAGES['index']
167 |
168 | validator = ModelValidator(obj1)
169 | valid = validator.validate()
170 | assert valid
171 |
172 |
173 | def test_validate_only():
174 | obj = BasicFields(field1='one')
175 |
176 | validator = ModelValidator(obj)
177 | valid = validator.validate(only=('field1', ))
178 | assert valid
179 |
180 |
181 | def test_save():
182 | obj = BasicFields(field1='one', field2='124124', field3='1232314')
183 |
184 | validator = ModelValidator(obj)
185 | valid = validator.validate({'field1': 'updated'})
186 | assert valid
187 |
188 | validator.save()
189 |
190 | assert obj.id
191 | assert obj.field1 == 'updated'
192 |
193 |
194 | def test_m2m_empty():
195 | validator = ModelValidator(Student(name='tim'))
196 |
197 | valid = validator.validate()
198 | assert valid
199 |
200 | valid = validator.validate({'courses': []})
201 | assert valid
202 |
203 |
204 | def test_m2m_missing():
205 | validator = ModelValidator(Student(name='tim'))
206 |
207 | valid = validator.validate({'courses': [1, 33]})
208 | assert not valid
209 | assert validator.errors['courses'] == DEFAULT_MESSAGES['related'].format(field='id', values=[1, 33])
210 |
211 |
212 | def test_m2m_ints():
213 | validator = ModelValidator(Student(name='tim'))
214 |
215 | c1 = Course.create(name='course1')
216 | c2 = Course.create(name='course2')
217 |
218 | valid = validator.validate({'courses': [c1.id, c2.id]})
219 | print(validator.errors)
220 | assert valid
221 |
222 | valid = validator.validate({'courses': c1.id})
223 | assert valid
224 |
225 | valid = validator.validate({'courses': str(c1.id)})
226 | assert valid
227 |
228 |
229 | def test_m2m_instances():
230 | validator = ModelValidator(Student(name='tim'))
231 |
232 | c1 = Course.create(name='course1')
233 | c2 = Course.create(name='course2')
234 |
235 | valid = validator.validate({'courses': [c1, c2]})
236 | assert valid
237 |
238 | valid = validator.validate({'courses': c1})
239 | assert valid
240 |
241 |
242 | def test_m2m_dicts():
243 | validator = ModelValidator(Student(name='tim'))
244 |
245 | c1 = Course.create(name='course1')
246 | c2 = Course.create(name='course2')
247 |
248 | valid = validator.validate({'courses': [{'id': c1.id}, {'id': c2.id}]})
249 | assert valid
250 |
251 | valid = validator.validate({'courses': {'id': c1.id}})
252 | assert valid
253 |
254 |
255 | def test_m2m_dicts_blank():
256 | validator = ModelValidator(Student(name='tim'))
257 |
258 | valid = validator.validate({'courses': [{}, {}]})
259 | assert valid
260 |
261 | valid = validator.validate({'courses': {}})
262 | assert valid
263 |
264 |
265 | def test_m2m_save():
266 | obj = Student(name='tim')
267 | validator = ModelValidator(obj)
268 |
269 | c1 = Course.create(name='course1')
270 | c2 = Course.create(name='course2')
271 |
272 | valid = validator.validate({'courses': [c1, c2]})
273 | assert valid
274 |
275 | validator.save()
276 |
277 | assert obj.id
278 | assert c1 in obj.courses
279 | assert c2 in obj.courses
280 |
281 |
282 | def test_m2m_save_blank():
283 | obj = Student(name='tim')
284 | validator = ModelValidator(obj)
285 |
286 | valid = validator.validate({'courses': [{}, {}]})
287 | assert valid
288 |
289 | validator.save()
290 |
291 | assert obj.id
292 |
293 |
294 | def test_overrides():
295 |
296 | class CustomValidator(ModelValidator):
297 | students = ManyModelChoiceField(Student.select(), Student.name)
298 |
299 | Student.create(name='tim')
300 | Student.create(name='bob')
301 |
302 | obj = Course.create(name='course1')
303 |
304 | validator = CustomValidator(obj)
305 |
306 | data = {'students': [{'name': 'tim'}, 'bob']}
307 | valid = validator.validate(data)
308 | print(validator.errors)
309 | assert valid
310 |
311 | validator.save()
312 |
313 | assert obj.id
314 | assert len(obj.students) == 2
315 |
--------------------------------------------------------------------------------
/docs/usage.rst:
--------------------------------------------------------------------------------
1 | Basic Validation
2 | ================
3 |
4 | The very basic usage is a validator class that looks like this:
5 |
6 | .. code:: python
7 |
8 | from peewee_validates import Validator, StringField, validate_not_empty
9 |
10 | class SimpleValidator(Validator):
11 | first_name = StringField(validators=[validate_not_empty()])
12 |
13 | validator = SimpleValidator()
14 |
15 | This tells us that we want to validate data for one field (first_name).
16 |
17 | Each field has an associated data type. In this case, using StringField will coerce the input data
18 | to ``str``.
19 |
20 | After creating an instance of our valitdator, then we call the ``validate()`` method and
21 | pass the data that we want to validate. The result we get back is a boolean indicating whether
22 | all validations were successful.
23 |
24 | The validator then has two dictionaries that you mway want to access: ``data`` and ``errors``.
25 |
26 | ``data`` is the input data that may have been mutated after validations.
27 |
28 | ``errors`` is a dictionary of any error messages.
29 |
30 | .. code:: python
31 |
32 | data = {'first_name': ''}
33 | validator.validate(data)
34 |
35 | print(validator.data)
36 | # {}
37 |
38 | print(validator.errors)
39 | # {'first_name': 'This field is required'}
40 |
41 | In this case we can see that there was one error for ``first_name``.
42 | That's because we gave it the ``validate_not_empty()`` validator but did not pass any data for
43 | that field. Also notice that the ``data`` dict is empty because the validators did not pass.
44 |
45 | When we pass data that matches all validators, the ``errors`` dict will be empty and the ``data``
46 | dict will be populated:
47 |
48 | .. code:: python
49 |
50 | data = {'first_name': 'Tim'}
51 | validator.validate(data)
52 |
53 | print(validator.data)
54 | # {'first_name': 'Tim'}
55 |
56 | print(validator.errors)
57 | # {}
58 |
59 | The ``data`` dict will contain the values after any validators, type coersions, and
60 | any other custom modifiers. Also notice that we are able to reuse the same validator instance
61 | while passing a new data dict.
62 |
63 | Data Type Coersion
64 | ------------------
65 |
66 | One of the first processes that happens when data validation takes place is data type coersion.
67 |
68 | There are a number of different fields built-in. Check out the full list in the API Documentation.
69 |
70 | Here's an example of a field.
71 | This just duplicates the functionality of ``IntegerField`` to show you an as example.
72 |
73 | .. code:: python
74 |
75 | class CustomIntegerField(Field):
76 | def coerce(self, value):
77 | try:
78 | return int(value)
79 | except (TypeError, ValueError):
80 | raise ValidationError('coerce_int')
81 |
82 | class SimpleValidator(Validator):
83 | code = CustomIntegerField()
84 |
85 | validator = SimpleValidator()
86 | validator.validate({'code': 'text'})
87 |
88 | validator.data
89 | # {}
90 |
91 | validator.errors
92 | # {'code': 'Must be a valid integer.'}
93 |
94 | Available Validators
95 | ====================
96 |
97 | There are a bunch of built-in validators that can be accessed by importing from ``peewee_validates``.
98 |
99 | * ``validate_email()`` - validate that data is an email address
100 | * ``validate_equal(value)`` - validate that data is equal to ``value``
101 | * ``validate_function(method, **kwargs)`` - runs ``method`` with field value as first argument and ``kwargs`` and alidates that the result is truthy
102 | * ``validate_length(low, high, equal)`` - validate that length is between ``low`` and ``high`` or equal to ``equal``
103 | * ``validate_none_of(values)`` - validate that value is not in ``values``. ``values`` can also be a callable that returns values when called
104 | * ``validate_not_empty()`` - validate that data is not empty
105 | * ``validate_one_of(values)`` - validate that value is in ``values``. ``values`` can also be a callable that returns values when called
106 | * ``validate_range(low, high)`` - validate that value is between ``low`` and ``high``
107 | * ``validate_regexp(pattern, flags=0)`` - validate that value matches ``patten``
108 | * ``validate_required()`` - validate that the field is present
109 |
110 | Custom Validators
111 | =================
112 |
113 | A field validator is just a method with the signature ``validator(field, data)`` where
114 | field is a ``Field`` instance and ``data`` is the data dict that is passed to ``validate()``.
115 |
116 | If we want to implement a validator that makes sure the name is always "tim" we could do it
117 | like this:
118 |
119 | .. code:: python
120 |
121 | def always_tim(field, data):
122 | if field.value and field.value != 'tim':
123 | raise ValidationError('not_tim')
124 |
125 | class SimpleValidator(Validator):
126 | name = StringField(validators=[always_tim])
127 |
128 | validator = SimpleValidator()
129 | validator.validate({'name': 'bob'})
130 |
131 | validator.errors
132 | # {'name': 'Validation failed.'}
133 |
134 | That's not a very pretty error message, but I'll show you soon how to customize that.
135 |
136 | Now let's say you want to implement a validator that checks the length of the field.
137 | The length should be configurable. So we can implement a validator that accepts a parameter
138 | and returns a validator function. We basically wrap our actual validator function with
139 | another function. That looks like this:
140 |
141 | .. code:: python
142 |
143 | def length(max_length):
144 | def validator(field, data):
145 | if field.value and len(field.value) > max_length:
146 | raise ValidationError('too_long')
147 | return validator
148 |
149 | class SimpleValidator(Validator):
150 | name = StringField(validators=[length(2)])
151 |
152 | validator = SimpleValidator()
153 | validator.validate({'name': 'bob'})
154 |
155 | validator.errors
156 | # {'name': 'Validation failed.'}
157 |
158 | Custom Error Messages
159 | =====================
160 |
161 | In some of the previous examples, we saw that the default error messages are not always that
162 | friendly. Error messages can be changed by settings the ``messages`` attribute on the ``Meta``
163 | class. Error messages are looked up by a key, and optionally prefixed with the field name.
164 |
165 | The key is the first argument passed to ``ValidationError`` when an error is raised.
166 |
167 | .. code:: python
168 |
169 | class SimpleValidator(Validator):
170 | name = StringField(required=True)
171 |
172 | class Meta:
173 | messages = {
174 | 'required': 'Please enter a value.'
175 | }
176 |
177 | Now any field that is required will have the error message "please enter a value".
178 | We can also change this for specific fields by prefixing with field name:
179 |
180 | .. code:: python
181 |
182 | class SimpleValidator(Validator):
183 | name = StringField(required=True)
184 | color = StringField(required=True)
185 |
186 | class Meta:
187 | messages = {
188 | 'name.required': 'Enter your name.',
189 | 'required': 'Please enter a value.',
190 | }
191 |
192 | Now the ``name`` field will have the error message "Enter your name." but all other
193 | required fields will use the other error message.
194 |
195 | Excluding/Limiting Fields
196 | =========================
197 |
198 | It's possible to limit or exclude fields from validation. This can be done at the class level
199 | or when calling ``validate()``.
200 |
201 | This will only validate the ``name`` and ``color`` fields when ``validate()`` is called:
202 |
203 | .. code:: python
204 |
205 | class SimpleValidator(Validator):
206 | name = StringField(required=True)
207 | color = StringField(required=True)
208 | age = IntegerField(required=True)
209 |
210 | class Meta:
211 | only = ('name', 'color')
212 |
213 | And similarly, you can override this when ``validate()`` is called:
214 |
215 | .. code:: python
216 |
217 | validator = SimpleValidator()
218 | validator.validate(data, only=('color', 'name'))
219 |
220 | Now only ``color`` and ``name`` will be validated, ignoring the definition on the class.
221 |
222 | There's also an ``exclude`` attribute to exclude specific fields from validation. It works
223 | the same way that ``only`` does.
224 |
225 | Model Validation
226 | ================
227 |
228 | You may be wondering why this package is called peewee-validates when nothing we have discussed
229 | so far has anything to do with Peewee. Well here is where you find out. This package includes a
230 | ModelValidator class for using the validators we already talked about to validate model instances.
231 |
232 | .. code:: python
233 |
234 | import peewee
235 | from peewee_validates import ModelValidator
236 |
237 | class Category(peewee.Model):
238 | code = peewee.IntegerField(unique=True)
239 | name = peewee.CharField(max_length=250)
240 |
241 | obj = Category(code=42)
242 |
243 | validator = ModelValidator(obj)
244 | validator.validate()
245 |
246 | In this case, the ModelValidator has built a Validator class that looks like this:
247 |
248 | .. code:: python
249 |
250 | unique_code_validator = validate_model_unique(
251 | Category.code, Category.select(), pk_field=Category.id, pk_value=obj.id)
252 |
253 | class CategoryValidator(Validator):
254 | code = peewee.IntegerField(
255 | required=True,
256 | validators=[unique_code_validator])
257 | name = peewee.StringField(required=True, max_length=250)
258 |
259 | Notice the many things that have been defined in our model that have been automatically converted
260 | to validator attributes:
261 |
262 | * name is required string
263 | * name must be 250 character or less
264 | * code is required integer
265 | * code must be a unique value in the table
266 |
267 | We can then use the validator to validate data.
268 |
269 | By default, it will validate the data directly on the model instance, but you can always pass
270 | a dictionary to ``validates`` that will override any data on the instance.
271 |
272 | .. code:: python
273 |
274 | obj = Category(code=42)
275 | data = {'code': 'notnum'}
276 |
277 | validator = ModelValidator(obj)
278 | validator.validate(data)
279 |
280 | validator.errors
281 | # {'code': 'Must be a valid integer.'}
282 |
283 | This fails validation because the data passed in was not a number, even though the data on the
284 | instance was valid.
285 |
286 | You can also create a subclass of ``ModelValidator`` to use all the other things we have
287 | shown already:
288 |
289 | .. code:: python
290 |
291 | import peewee
292 | from peewee_validates import ModelValidator
293 |
294 | class CategoryValidator(ModelValidator):
295 | class Meta:
296 | messages = {
297 | 'name.required': 'Enter your name.',
298 | 'required': 'Please enter a value.',
299 | }
300 |
301 | validator = ModelValidator(obj)
302 | validator.validate(data)
303 |
304 | When validations is successful for ModelValidator, the given model instance will have been mutated.
305 |
306 | .. code:: python
307 |
308 | validator = ModelValidator(obj)
309 |
310 | obj.name
311 | # 'tim'
312 |
313 | validator.validate({'name': 'newname'})
314 |
315 | obj.name
316 | # 'newname'
317 |
318 | Field Validations
319 | -----------------
320 |
321 | Using the ModelValidator provides a couple extra goodies that are not found in the standard
322 | Validator class.
323 |
324 | **Uniqueness**
325 |
326 | If the Peewee field was defined with ``unique=True`` then a validator will be added to the
327 | field that will look up the value in the database to make sure it's unique. This is smart enough
328 | to know to exclude the current instance if it has already been saved to the database.
329 |
330 | **Foreign Key**
331 |
332 | If the Peewee field is a ``ForeignKeyField`` then a validator will be added to the field
333 | that will look up the value in the related table to make sure it's a valid instance.
334 |
335 | **Many to Many**
336 |
337 | If the Peewee field is a ``ManyToManyField`` then a validator will be added to the field
338 | that will look up the values in the related table to make sure it's valid list of instances.
339 |
340 | **Index Validation**
341 |
342 | If you have defined unique indexes on the model like the example below, they will also
343 | be validated (after all the other field level validations have succeeded).
344 |
345 | .. code:: python
346 |
347 | class Category(peewee.Model):
348 | code = peewee.IntegerField(unique=True)
349 | name = peewee.CharField(max_length=250)
350 |
351 | class Meta:
352 | indexes = (
353 | (('name', 'code'), True),
354 | )
355 |
356 | Field Overrides
357 | ===============
358 |
359 | If you need to change the way a model field is validated, you can simply override the field
360 | in your custom class. Given the following model:
361 |
362 | .. code:: python
363 |
364 | class Category(peewee.Model):
365 | code = peewee.IntegerField(required=True)
366 |
367 | This would generate a field for ``code`` with a required validator.
368 |
369 | .. code:: python
370 |
371 | class CategoryValidator(ModelValidator):
372 | code = IntegerField(required=False)
373 |
374 | validator = CategoryValidator(category)
375 | validator.validate()
376 |
377 | Now ``code`` will not be required when the call to ``validate`` happens.
378 |
379 | Overriding Behaviors
380 | ====================
381 |
382 | Cleaning
383 | --------
384 |
385 | Once all field-level data has been validated during ``validate()``, the resulting data is
386 | passed to the ``clean()`` method before being returned in the result. You can override this
387 | method to perform any validations you like, or mutate the data before returning it.
388 |
389 | .. code:: python
390 |
391 | class MyValidator(Validator):
392 | name1 = StringField()
393 | name2 = StringField()
394 |
395 | def clean(self, data):
396 | # make sure name1 is the same as name2
397 | if data['name1'] != data['name2']:
398 | raise ValidationError('name_different')
399 | # and if they are the same, uppercase them
400 | data['name1'] = data['name1'].upper()
401 | data['name2'] = data['name2'].upper()
402 | return data
403 |
404 | class Meta:
405 | messages = {
406 | 'name_different': 'The names should be the same.'
407 | }
408 |
409 | Adding Fields Dynamically
410 | -------------------------
411 |
412 | If you need to, you can dynamically add a field to a validator instance.
413 | They are stored in the ``_meta.fields`` dict, which you can manipulate as much as you want.
414 |
415 | .. code:: python
416 |
417 | validator = MyValidator()
418 | validator._meta.fields['newfield'] = IntegerField(required=True)
419 |
--------------------------------------------------------------------------------
/tests/test_basic.py:
--------------------------------------------------------------------------------
1 | from datetime import date
2 | from datetime import datetime
3 | from datetime import time
4 |
5 | from peewee_validates import DEFAULT_MESSAGES
6 | from peewee_validates import Field
7 | from peewee_validates import Validator
8 | from peewee_validates import ValidationError
9 | from peewee_validates import StringField
10 | from peewee_validates import FloatField
11 | from peewee_validates import IntegerField
12 | from peewee_validates import validate_length
13 | from peewee_validates import DecimalField
14 | from peewee_validates import DateField
15 | from peewee_validates import TimeField
16 | from peewee_validates import DateTimeField
17 | from peewee_validates import BooleanField
18 | from peewee_validates import validate_not_empty
19 | from peewee_validates import validate_one_of
20 | from peewee_validates import validate_none_of
21 | from peewee_validates import validate_equal
22 | from peewee_validates import validate_regexp
23 | from peewee_validates import validate_email
24 | from peewee_validates import validate_function
25 |
26 |
27 | def test_raw_field():
28 | class TestValidator(Validator):
29 | field1 = Field()
30 |
31 | validator = TestValidator()
32 | valid = validator.validate({'field1': 'thing'})
33 | assert valid
34 | assert validator.data['field1'] == 'thing'
35 |
36 |
37 | def test_required():
38 | class TestValidator(Validator):
39 | bool_field = BooleanField(required=True)
40 | decimal_field = DecimalField(required=True)
41 | float_field = FloatField(required=True, low=10.0, high=50.0)
42 | int_field = IntegerField(required=True)
43 | str_field = StringField(required=True)
44 | date_field = DateField(required=True, low='jan 1, 2010', high='dec 1, 2010')
45 | time_field = TimeField(required=True, low='9 am', high='10 am')
46 | datetime_field = DateTimeField(required=True, low='jan 1, 2010', high='dec 1, 2010')
47 |
48 | validator = TestValidator()
49 | valid = validator.validate()
50 | assert not valid
51 | assert validator.errors['bool_field'] == DEFAULT_MESSAGES['required']
52 | assert validator.errors['decimal_field'] == DEFAULT_MESSAGES['required']
53 | assert validator.errors['float_field'] == DEFAULT_MESSAGES['required']
54 | assert validator.errors['int_field'] == DEFAULT_MESSAGES['required']
55 | assert validator.errors['str_field'] == DEFAULT_MESSAGES['required']
56 | assert validator.errors['date_field'] == DEFAULT_MESSAGES['required']
57 | assert validator.errors['time_field'] == DEFAULT_MESSAGES['required']
58 | assert validator.errors['datetime_field'] == DEFAULT_MESSAGES['required']
59 |
60 |
61 | def test_integerfield():
62 | class TestValidator(Validator):
63 | int_field = IntegerField(required=True)
64 |
65 | data = {'int_field': 0}
66 | validator = TestValidator()
67 | valid = validator.validate(data)
68 | assert valid
69 |
70 |
71 | def test_coerce_fails():
72 | class TestValidator(Validator):
73 | float_field = FloatField()
74 | int_field = IntegerField(required=True)
75 | decimal_field = DecimalField(required=True)
76 | boolean_field = BooleanField()
77 |
78 | validator = TestValidator()
79 | data = {'int_field': 'a', 'float_field': 'a', 'decimal_field': 'a', 'boolean_field': 'false'}
80 | valid = validator.validate(data)
81 | assert not valid
82 | assert validator.errors['decimal_field'] == DEFAULT_MESSAGES['coerce_decimal']
83 | assert validator.errors['float_field'] == DEFAULT_MESSAGES['coerce_float']
84 | assert validator.errors['int_field'] == DEFAULT_MESSAGES['coerce_int']
85 |
86 |
87 | def test_decimal():
88 | class TestValidator(Validator):
89 | low_field = DecimalField(low=-42.0)
90 | high_field = DecimalField(high=42.0)
91 | low_high_field = DecimalField(low=-42.0, high=42.0)
92 |
93 | validator = TestValidator()
94 | data = {'low_field': '-99.99', 'high_field': '99.99', 'low_high_field': '99.99'}
95 | valid = validator.validate(data)
96 | assert not valid
97 | assert validator.errors['low_field'] == 'Must be at least -42.0.'
98 | assert validator.errors['high_field'] == 'Must be between None and 42.0.'
99 | assert validator.errors['low_high_field'] == 'Must be between -42.0 and 42.0.'
100 |
101 |
102 | def test_required_empty():
103 | class TestValidator(Validator):
104 | field1 = StringField(required=False, validators=[validate_not_empty()])
105 |
106 | validator = TestValidator()
107 |
108 | valid = validator.validate()
109 | assert valid
110 |
111 | valid = validator.validate({'field1': ''})
112 | assert not valid
113 | assert validator.errors['field1'] == DEFAULT_MESSAGES['empty']
114 |
115 |
116 | def test_dates_empty():
117 | class TestValidator(Validator):
118 | date_field = DateField()
119 | time_field = TimeField()
120 | datetime_field = DateTimeField()
121 |
122 | data = {
123 | 'date_field': '',
124 | 'time_field': '',
125 | 'datetime_field': '',
126 | }
127 |
128 | validator = TestValidator()
129 | valid = validator.validate(data)
130 |
131 | print(validator.errors)
132 | assert valid
133 | assert not validator.data['datetime_field']
134 | assert not validator.data['date_field']
135 | assert not validator.data['time_field']
136 |
137 |
138 | def test_dates_coersions():
139 | class TestValidator(Validator):
140 | date_field = DateField(required=True)
141 | time_field = TimeField(required=True)
142 | datetime_field = DateTimeField(required=True)
143 |
144 | data = {
145 | 'date_field': 'jan 1, 2015',
146 | 'time_field': 'jan 1, 2015 3:20 pm',
147 | 'datetime_field': 'jan 1, 2015 3:20 pm',
148 | }
149 |
150 | validator = TestValidator()
151 | valid = validator.validate(data)
152 |
153 | assert valid
154 | assert validator.data['datetime_field'] == datetime(2015, 1, 1, 15, 20)
155 | assert validator.data['date_field'] == date(2015, 1, 1)
156 | assert validator.data['time_field'] == time(15, 20)
157 |
158 |
159 | def test_dates_native():
160 | class TestValidator(Validator):
161 | date_field = DateField(required=True)
162 | time_field = TimeField(required=True)
163 | datetime_field = DateTimeField(required=True)
164 |
165 | data = {
166 | 'date_field': date(2015, 1, 1),
167 | 'time_field': time(15, 20),
168 | 'datetime_field': datetime(2015, 1, 1, 15, 20),
169 | }
170 |
171 | validator = TestValidator()
172 | valid = validator.validate(data)
173 |
174 | assert valid
175 | assert validator.data['datetime_field'] == datetime(2015, 1, 1, 15, 20)
176 | assert validator.data['date_field'] == date(2015, 1, 1)
177 | assert validator.data['time_field'] == time(15, 20)
178 |
179 |
180 | def test_date_coerce_fail():
181 | class TestValidator(Validator):
182 | date_field = DateField(required=True)
183 | time_field = TimeField(required=True)
184 | datetime_field = DateTimeField(required=True)
185 |
186 | data = {
187 | 'date_field': 'failure',
188 | 'time_field': 'failure',
189 | 'datetime_field': 'failure',
190 | }
191 |
192 | validator = TestValidator()
193 | valid = validator.validate(data)
194 |
195 | assert not valid
196 | assert validator.errors['datetime_field'] == DEFAULT_MESSAGES['coerce_datetime']
197 | assert validator.errors['date_field'] == DEFAULT_MESSAGES['coerce_date']
198 | assert validator.errors['time_field'] == DEFAULT_MESSAGES['coerce_time']
199 |
200 |
201 | def test_default():
202 | class TestValidator(Validator):
203 | str_field = StringField(required=True, default='timster')
204 |
205 | validator = TestValidator()
206 | valid = validator.validate()
207 | assert valid
208 | assert validator.data['str_field'] == 'timster'
209 |
210 |
211 | def test_callable_default():
212 | def getname():
213 | return 'timster'
214 |
215 | class TestValidator(Validator):
216 | str_field = StringField(required=True, default=getname)
217 |
218 | validator = TestValidator()
219 | valid = validator.validate()
220 | assert valid
221 | assert validator.data['str_field'] == 'timster'
222 |
223 |
224 | def test_lengths():
225 | class TestValidator(Validator):
226 | max_field = StringField(max_length=5)
227 | min_field = StringField(min_length=5)
228 | len_field = StringField(validators=[validate_length(equal=10)])
229 |
230 | validator = TestValidator()
231 | valid = validator.validate({'min_field': 'shrt', 'max_field': 'toolong', 'len_field': '3'})
232 | assert not valid
233 | assert validator.errors['min_field'] == DEFAULT_MESSAGES['length_low'].format(low=5)
234 | assert validator.errors['max_field'] == DEFAULT_MESSAGES['length_high'].format(high=5)
235 | assert validator.errors['len_field'] == DEFAULT_MESSAGES['length_equal'].format(equal=10)
236 |
237 |
238 | def test_range():
239 | class TestValidator(Validator):
240 | range1 = IntegerField(low=1, high=5)
241 | range2 = IntegerField(low=1, high=5)
242 |
243 | validator = TestValidator()
244 | valid = validator.validate({'range1': '44', 'range2': '3'})
245 | assert not valid
246 | assert validator.errors['range1'] == DEFAULT_MESSAGES['range_between'].format(low=1, high=5)
247 | assert 'range2' not in validator.errors
248 |
249 |
250 | def test_coerce_error():
251 | class TestValidator(Validator):
252 | date_field = DateField()
253 |
254 | validator = TestValidator()
255 | valid = validator.validate({'date_field': 'not a real date'})
256 | assert not valid
257 | assert validator.errors['date_field'] == DEFAULT_MESSAGES['coerce_date']
258 |
259 |
260 | def test_choices():
261 | class TestValidator(Validator):
262 | first_name = StringField(validators=[validate_one_of(('tim', 'bob'))])
263 |
264 | validator = TestValidator()
265 | valid = validator.validate()
266 | assert valid
267 |
268 | validator = TestValidator()
269 | valid = validator.validate({'first_name': 'tim'})
270 | assert valid
271 |
272 | validator = TestValidator()
273 | valid = validator.validate({'first_name': 'asdf'})
274 | assert not valid
275 | assert validator.errors['first_name'] == DEFAULT_MESSAGES['one_of'].format(choices='tim, bob')
276 |
277 |
278 | def test_choices_integers():
279 | class TestValidator(Validator):
280 | int_field = IntegerField(validators=[validate_one_of((1, 2, 3))])
281 |
282 | validator = TestValidator()
283 | valid = validator.validate({'int_field': 4})
284 | assert not valid
285 |
286 |
287 | def test_exclude():
288 | class TestValidator(Validator):
289 | first_name = StringField(validators=[validate_none_of(('tim', 'bob'))])
290 |
291 | validator = TestValidator()
292 | valid = validator.validate({'first_name': 'tim'})
293 | assert not valid
294 | assert validator.errors['first_name'] == DEFAULT_MESSAGES['none_of'].format(choices='tim, bob')
295 |
296 | validator = TestValidator()
297 | valid = validator.validate({'first_name': 'asdf'})
298 | assert valid
299 |
300 |
301 | def test_callable_choices():
302 | def getchoices():
303 | return ('tim', 'bob')
304 |
305 | class TestValidator(Validator):
306 | first_name = StringField(validators=[validate_one_of(getchoices)])
307 |
308 | validator = TestValidator()
309 | valid = validator.validate({'first_name': 'asdf'})
310 | assert not valid
311 | assert validator.errors['first_name'] == DEFAULT_MESSAGES['one_of'].format(choices='tim, bob')
312 |
313 | validator = TestValidator()
314 | valid = validator.validate({'first_name': 'tim'})
315 | assert valid
316 |
317 |
318 | def test_callable_exclude():
319 | def getchoices():
320 | return ('tim', 'bob')
321 |
322 | class TestValidator(Validator):
323 | first_name = StringField(validators=[validate_none_of(getchoices)])
324 |
325 | validator = TestValidator()
326 | valid = validator.validate({'first_name': 'tim'})
327 | assert not valid
328 | assert validator.errors['first_name'] == DEFAULT_MESSAGES['none_of'].format(choices='tim, bob')
329 |
330 | validator = TestValidator()
331 | valid = validator.validate({'first_name': 'asdf'})
332 | assert valid
333 |
334 |
335 | def test_equal():
336 | class TestValidator(Validator):
337 | first_name = StringField(validators=[validate_equal('tim')])
338 |
339 | validator = TestValidator()
340 | valid = validator.validate({'first_name': 'tim'})
341 | assert valid
342 |
343 | validator = TestValidator()
344 | valid = validator.validate({'first_name': 'asdf'})
345 | assert not valid
346 | assert validator.errors['first_name'] == DEFAULT_MESSAGES['equal'].format(other='tim')
347 |
348 |
349 | def test_regexp():
350 | class TestValidator(Validator):
351 | first_name = StringField(validators=[validate_regexp('^[i-t]+$')])
352 |
353 | validator = TestValidator()
354 | valid = validator.validate({'first_name': 'tim'})
355 | assert valid
356 |
357 | validator = TestValidator()
358 | valid = validator.validate({'first_name': 'asdf'})
359 | assert not valid
360 | assert validator.errors['first_name'] == DEFAULT_MESSAGES['regexp'].format(pattern='^[i-t]+$')
361 |
362 |
363 | def test_email():
364 | class TestValidator(Validator):
365 | email = StringField(validators=[validate_email()])
366 |
367 | validator = TestValidator()
368 | valid = validator.validate({'email': 'bad-domain@asdfasdf'})
369 | assert not valid
370 | assert validator.errors['email'] == DEFAULT_MESSAGES['email']
371 |
372 |
373 | def test_function():
374 | def alwaystim(value):
375 | if value == 'tim':
376 | return True
377 |
378 | class TestValidator(Validator):
379 | first_name = StringField(validators=[validate_function(alwaystim)])
380 |
381 | class Meta:
382 | messages = {
383 | 'function': 'your name must be tim'
384 | }
385 |
386 | validator = TestValidator()
387 | valid = validator.validate({'first_name': 'tim'})
388 | assert valid
389 |
390 | validator = TestValidator()
391 | valid = validator.validate({'first_name': 'asdf'})
392 | assert not valid
393 | assert validator.errors['first_name'] == validator._meta.messages['function']
394 |
395 |
396 | def test_only_exclude():
397 | class TestValidator(Validator):
398 | field1 = StringField(required=True)
399 | field2 = StringField(required=True)
400 |
401 | validator = TestValidator()
402 | valid = validator.validate({'field1': 'shrt'}, only=['field1'])
403 | assert valid
404 |
405 | valid = validator.validate({'field1': 'shrt'}, exclude=['field2'])
406 | assert valid
407 |
408 |
409 | def test_clean_field():
410 | class TestValidator(Validator):
411 | field1 = StringField(required=True)
412 |
413 | def clean_field1(self, value):
414 | return value + '-awesome'
415 |
416 | validator = TestValidator()
417 | valid = validator.validate({'field1': 'tim'})
418 | assert valid
419 | assert validator.data['field1'] == 'tim-awesome'
420 |
421 |
422 | def test_clean_field_error():
423 | class TestValidator(Validator):
424 | field1 = StringField(required=True)
425 |
426 | def clean_field1(self, value):
427 | raise ValidationError('required')
428 |
429 | validator = TestValidator()
430 | valid = validator.validate({'field1': 'tim'})
431 | assert not valid
432 | assert validator.data['field1'] == 'tim'
433 | assert validator.errors['field1'] == DEFAULT_MESSAGES['required']
434 |
435 |
436 | def test_clean():
437 | class TestValidator(Validator):
438 | field1 = StringField(required=True)
439 |
440 | def clean(self, data):
441 | data['field1'] += 'awesome'
442 | return data
443 |
444 | validator = TestValidator()
445 | valid = validator.validate({'field1': 'tim'})
446 | assert valid
447 | assert validator.data['field1'] == 'timawesome'
448 |
449 |
450 | def test_clean_error():
451 | class TestValidator(Validator):
452 | field1 = StringField(required=True)
453 |
454 | def clean(self, data):
455 | raise ValidationError('required')
456 |
457 | validator = TestValidator()
458 | valid = validator.validate({'field1': 'tim'})
459 | assert not valid
460 | assert validator.data['field1'] == 'tim'
461 | assert validator.errors['__base__'] == DEFAULT_MESSAGES['required']
462 |
463 |
464 | def test_custom_messages():
465 | class TestValidator(Validator):
466 | field1 = StringField(required=True)
467 | field2 = StringField(required=True)
468 | field3 = IntegerField(required=True)
469 |
470 | class Meta:
471 | messages = {
472 | 'required': 'enter value',
473 | 'field2.required': 'field2 required',
474 | 'field3.coerce_int': 'pick a number',
475 | }
476 |
477 | validator = TestValidator()
478 | valid = validator.validate({'field3': 'asdfasdf'})
479 | assert not valid
480 | assert validator.errors['field1'] == 'enter value'
481 | assert validator.errors['field2'] == 'field2 required'
482 | assert validator.errors['field3'] == 'pick a number'
483 |
484 |
485 | def test_subclass():
486 | class ParentValidator(Validator):
487 | field1 = StringField(required=True)
488 | field2 = StringField(required=False)
489 |
490 | class TestValidator(ParentValidator):
491 | field2 = StringField(required=True)
492 | field3 = StringField(required=True)
493 |
494 | validator = TestValidator()
495 | valid = validator.validate({})
496 | assert not valid
497 | assert validator.errors['field1'] == DEFAULT_MESSAGES['required']
498 | assert validator.errors['field2'] == DEFAULT_MESSAGES['required']
499 | assert validator.errors['field3'] == DEFAULT_MESSAGES['required']
500 |
--------------------------------------------------------------------------------
/peewee_validates.py:
--------------------------------------------------------------------------------
1 | """peewee-validates is a validator module designed to work with the Peewee ORM."""
2 |
3 | import datetime
4 | import re
5 | from decimal import Decimal
6 | from decimal import InvalidOperation
7 | from inspect import isgeneratorfunction
8 | from inspect import isgenerator
9 | from collections import Iterable
10 |
11 | import peewee
12 | from dateutil.parser import parse as dateutil_parse
13 | try:
14 | from playhouse.fields import ManyToManyField
15 | except ImportError:
16 | from peewee import ManyToManyField
17 |
18 | __version__ = '1.0.8'
19 |
20 | __all__ = [
21 | 'Field', 'Validator', 'ModelValidator', 'ValidationError', 'StringField', 'FloatField',
22 | 'IntegerField', 'DecimalField', 'DateField', 'TimeField', 'DateTimeField', 'BooleanField',
23 | 'ModelChoiceField', 'ManyModelChoiceField',
24 | ]
25 |
26 | PEEWEE3 = peewee.__version__ >= '3.0.0'
27 |
28 | DEFAULT_MESSAGES = {
29 | 'required': 'This field is required.',
30 | 'empty': 'This field must not be blank.',
31 | 'one_of': 'Must be one of the choices: {choices}.',
32 | 'none_of': 'Must not be one of the choices: {choices}.',
33 | 'equal': 'Must be equal to {other}.',
34 | 'regexp': 'Must match the pattern {pattern}.',
35 | 'matches': 'Must match the field {other}.',
36 | 'email': 'Must be a valid email address.',
37 | 'function': 'Failed validation for {function}.',
38 | 'length_high': 'Must be at most {high} characters.',
39 | 'length_low': 'Must be at least {low} characters.',
40 | 'length_between': 'Must be between {low} and {high} characters.',
41 | 'length_equal': 'Must be exactly {equal} characters.',
42 | 'range_high': 'Must be at most {high}.',
43 | 'range_low': 'Must be at least {low}.',
44 | 'range_between': 'Must be between {low} and {high}.',
45 | 'coerce_decimal': 'Must be a valid decimal.',
46 | 'coerce_date': 'Must be a valid date.',
47 | 'coerce_time': 'Must be a valid time.',
48 | 'coerce_datetime': 'Must be a valid datetime.',
49 | 'coerce_float': 'Must be a valid float.',
50 | 'coerce_int': 'Must be a valid integer.',
51 | 'related': 'Unable to find object with {field} = {values}.',
52 | 'list': 'Must be a list of values',
53 | 'unique': 'Must be a unique value.',
54 | 'index': 'Fields must be unique together.',
55 | }
56 |
57 |
58 | class ValidationError(Exception):
59 | """An exception class that should be raised when a validation error occurs on data."""
60 | def __init__(self, key, *args, **kwargs):
61 | self.key = key
62 | self.kwargs = kwargs
63 | super().__init__(*args)
64 |
65 |
66 | def validate_required():
67 | """
68 | Validate that a field is present in the data.
69 |
70 | :raises: ``ValidationError('required')``
71 | """
72 | def required_validator(field, data):
73 | if field.value is None:
74 | raise ValidationError('required')
75 | return required_validator
76 |
77 |
78 | def validate_not_empty():
79 | """
80 | Validate that a field is not empty (blank string).
81 |
82 | :raises: ``ValidationError('empty')``
83 | """
84 | def empty_validator(field, data):
85 | if isinstance(field.value, str) and not field.value.strip():
86 | raise ValidationError('empty')
87 | return empty_validator
88 |
89 |
90 | def validate_length(low=None, high=None, equal=None):
91 | """
92 | Validate the length of a field with either low, high, or equal.
93 | Should work with anything that supports len().
94 |
95 | :param low: Smallest length required.
96 | :param high: Longest length required.
97 | :param equal: Exact length required.
98 | :raises: ``ValidationError('length_low')``
99 | :raises: ``ValidationError('length_high')``
100 | :raises: ``ValidationError('length_between')``
101 | :raises: ``ValidationError('length_equal')``
102 | """
103 | def length_validator(field, data):
104 | if field.value is None:
105 | return
106 | if equal is not None and len(field.value) != equal:
107 | raise ValidationError('length_equal', equal=equal)
108 | if low is not None and len(field.value) < low:
109 | key = 'length_low' if high is None else 'length_between'
110 | raise ValidationError(key, low=low, high=high)
111 | if high is not None and len(field.value) > high:
112 | key = 'length_high' if low is None else 'length_between'
113 | raise ValidationError(key, low=low, high=high)
114 | return length_validator
115 |
116 |
117 | def validate_one_of(values):
118 | """
119 | Validate that a field is in one of the given values.
120 |
121 | :param values: Iterable of valid values.
122 | :raises: ``ValidationError('one_of')``
123 | """
124 | def one_of_validator(field, data):
125 | if field.value is None:
126 | return
127 | options = values
128 | if callable(options):
129 | options = options()
130 | if field.value not in options:
131 | raise ValidationError('one_of', choices=', '.join(map(str, options)))
132 | return one_of_validator
133 |
134 |
135 | def validate_none_of(values):
136 | """
137 | Validate that a field is not in one of the given values.
138 |
139 | :param values: Iterable of invalid values.
140 | :raises: ``ValidationError('none_of')``
141 | """
142 | def none_of_validator(field, data):
143 | options = values
144 | if callable(options):
145 | options = options()
146 | if field.value in options:
147 | raise ValidationError('none_of', choices=str.join(', ', options))
148 | return none_of_validator
149 |
150 |
151 | def validate_range(low=None, high=None):
152 | """
153 | Validate the range of a field with either low, high, or equal.
154 | Should work with anything that supports '>' and '<' operators.
155 |
156 | :param low: Smallest value required.
157 | :param high: Longest value required.
158 | :raises: ``ValidationError('range_low')``
159 | :raises: ``ValidationError('range_high')``
160 | :raises: ``ValidationError('range_between')``
161 | """
162 | def range_validator(field, data):
163 | if field.value is None:
164 | return
165 | if low is not None and field.value < low:
166 | key = 'range_low' if high is None else 'range_between'
167 | raise ValidationError(key, low=low, high=high)
168 | if high is not None and field.value > high:
169 | key = 'range_high' if high is None else 'range_between'
170 | raise ValidationError(key, low=low, high=high)
171 | return range_validator
172 |
173 |
174 | def validate_equal(value):
175 | """
176 | Validate the field value is equal to the given value.
177 | Should work with anything that supports '==' operator.
178 |
179 | :param value: Value to compare.
180 | :raises: ``ValidationError('equal')``
181 | """
182 | def equal_validator(field, data):
183 | if field.value is None:
184 | return
185 | if not (field.value == value):
186 | raise ValidationError('equal', other=value)
187 | return equal_validator
188 |
189 |
190 | def validate_matches(other):
191 | """
192 | Validate the field value is equal to another field in the data.
193 | Should work with anything that supports '==' operator.
194 |
195 | :param value: Field key to compare.
196 | :raises: ``ValidationError('matches')``
197 | """
198 | def matches_validator(field, data):
199 | if field.value is None:
200 | return
201 | if not (field.value == data.get(other)):
202 | raise ValidationError('matches', other=other)
203 | return matches_validator
204 |
205 |
206 | def validate_regexp(pattern, flags=0):
207 | """
208 | Validate the field matches the given regular expression.
209 | Should work with anything that supports '==' operator.
210 |
211 | :param pattern: Regular expresion to match. String or regular expression instance.
212 | :param pattern: Flags for the regular expression.
213 | :raises: ``ValidationError('equal')``
214 | """
215 | regex = re.compile(pattern, flags) if isinstance(pattern, str) else pattern
216 |
217 | def regexp_validator(field, data):
218 | if field.value is None:
219 | return
220 | if regex.match(str(field.value)) is None:
221 | raise ValidationError('regexp', pattern=pattern)
222 | return regexp_validator
223 |
224 |
225 | def validate_function(method, **kwargs):
226 | """
227 | Validate the field matches the result of calling the given method. Example::
228 |
229 | def myfunc(value, name):
230 | return value == name
231 |
232 | validator = validate_function(myfunc, name='tim')
233 |
234 | Essentially creates a validator that only accepts the name 'tim'.
235 |
236 | :param method: Method to call.
237 | :param kwargs: Additional keyword arguments passed to the method.
238 | :raises: ``ValidationError('function')``
239 | """
240 | def function_validator(field, data):
241 | if field.value is None:
242 | return
243 | if not method(field.value, **kwargs):
244 | raise ValidationError('function', function=method.__name__)
245 | return function_validator
246 |
247 |
248 | def validate_email():
249 | """
250 | Validate the field is a valid email address.
251 |
252 | :raises: ``ValidationError('email')``
253 | """
254 | user_regex = re.compile(
255 | r"(^[-!#$%&'*+/=?^`{}|~\w]+(\.[-!#$%&'*+/=?^`{}|~\w]+)*$"
256 | r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]'
257 | r'|\\[\001-\011\013\014\016-\177])*"$)', re.IGNORECASE | re.UNICODE)
258 |
259 | domain_regex = re.compile(
260 | r'(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+'
261 | r'(?:[A-Z]{2,6}|[A-Z0-9-]{2,})$'
262 | r'|^\[(25[0-5]|2[0-4]\d|[0-1]?\d?\d)'
263 | r'(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\]$', re.IGNORECASE | re.UNICODE)
264 |
265 | domain_whitelist = ('localhost',)
266 |
267 | def email_validator(field, data):
268 | if field.value is None:
269 | return
270 |
271 | value = str(field.value)
272 |
273 | if '@' not in value:
274 | raise ValidationError('email')
275 |
276 | user_part, domain_part = value.rsplit('@', 1)
277 |
278 | if not user_regex.match(user_part):
279 | raise ValidationError('email')
280 |
281 | if domain_part in domain_whitelist:
282 | return
283 |
284 | if not domain_regex.match(domain_part):
285 | raise ValidationError('email')
286 |
287 | return email_validator
288 |
289 |
290 | def validate_model_unique(lookup_field, queryset, pk_field=None, pk_value=None):
291 | """
292 | Validate the field is a unique, given a queryset and lookup_field. Example::
293 |
294 | validator = validate_model_unique(User.email, User.select())
295 |
296 | Creates a validator that can validate the uniqueness of an email address.
297 |
298 | :param lookup_field: Peewee model field that should be used for checking existing values.
299 | :param queryset: Queryset to use for lookup.
300 | :param pk_field: Field instance to use when excluding existing instance.
301 | :param pk_value: Field value to use when excluding existing instance.
302 | :raises: ``ValidationError('unique')``
303 | """
304 | def unique_validator(field, data):
305 | # If we have a PK, ignore it because it represents the current record.
306 | query = queryset.where(lookup_field == field.value)
307 | if pk_field and pk_value:
308 | query = query.where(~(pk_field == pk_value))
309 | if query.count():
310 | raise ValidationError('unique')
311 | return unique_validator
312 |
313 |
314 | def coerce_single_instance(lookup_field, value):
315 | """
316 | Convert from whatever value is given to a scalar value for lookup_field.
317 | If value is a dict, then lookup_field.name is used to get the value from the dict. Example:
318 | lookup_field.name = 'id'
319 | value = {'id': 123, 'name': 'tim'}
320 | returns = 123
321 | If value is a model, then lookup_field.name is extracted from the model. Example:
322 | lookup_field.name = 'id'
323 | value =
324 | returns = 123
325 | Otherwise the value is returned as-is.
326 |
327 | :param lookup_field: Peewee model field used for getting name from value.
328 | :param value: Some kind of value (usually a dict, Model instance, or scalar).
329 | """
330 | if isinstance(value, dict):
331 | return value.get(lookup_field.name)
332 | if isinstance(value, peewee.Model):
333 | return getattr(value, lookup_field.name)
334 | return value
335 |
336 |
337 | def isiterable_notstring(value):
338 | """
339 | Returns True if the value is iterable but not a string. Otherwise returns False.
340 |
341 | :param value: Value to check.
342 | """
343 | if isinstance(value, str):
344 | return False
345 | return isinstance(value, Iterable) or isgeneratorfunction(value) or isgenerator(value)
346 |
347 |
348 | class Field:
349 | """
350 | Base class from which all other fields should be derived.
351 |
352 | :param default: Is this field required?
353 | :param default: Default value to be used if no incoming value is provided.
354 | :param validators: List of validator functions to run.
355 | """
356 |
357 | __slots__ = ('value', 'required', 'default', 'validators')
358 |
359 | def __init__(self, required=False, default=None, validators=None):
360 | self.default = default
361 | self.value = None
362 | self.validators = validators or []
363 | if required:
364 | self.validators.append(validate_required())
365 |
366 | def coerce(self, value):
367 | """
368 | Coerce the given value into some type. By default a no-op.
369 | Used by sub-classes to enforce specific types.
370 | If there is a problem with the coersion, raise ValidationError.
371 |
372 | :param value: Value to coerce.
373 | :raises: ValidationError
374 | :return: The updated value.
375 | :rtype: any
376 | """
377 | return value
378 |
379 | def get_value(self, name, data):
380 | """
381 | Get the value of this field from the data.
382 | If there is a problem with the data, raise ValidationError.
383 |
384 | :param name: Name of this field (to retrieve from data).
385 | :param data: Dictionary of data for all fields.
386 | :raises: ValidationError
387 | :return: The value of this field.
388 | :rtype: any
389 | """
390 | if name in data:
391 | return data.get(name)
392 | if self.default:
393 | if callable(self.default):
394 | return self.default()
395 | return self.default
396 | return None
397 |
398 | def validate(self, name, data):
399 | """
400 | Check to make sure ths data for this field is valid.
401 | Usually runs all validators in self.validators list.
402 | If there is a problem with the data, raise ValidationError.
403 |
404 | :param name: The name of this field.
405 | :param data: Dictionary of data for all fields.
406 | :raises: ValidationError
407 | """
408 | self.value = self.get_value(name, data)
409 | if self.value is not None:
410 | self.value = self.coerce(self.value)
411 | for method in self.validators:
412 | method(self, data)
413 |
414 |
415 | class StringField(Field):
416 | """
417 | A field that will try to coerce value to a string.
418 |
419 | :param required: Is this field required?
420 | :param default: Default value to be used if no incoming value is provided.
421 | :param validators: List of validator functions to run.
422 | :param max_length: Maximum length that should be enfocred.
423 | :param min_length: Minimum length that should be enfocred.
424 | """
425 |
426 | __slots__ = ('value', 'required', 'default', 'validators')
427 |
428 | def __init__(self, required=False, max_length=None, min_length=None, default=None, validators=None):
429 | validators = validators or []
430 | if max_length or min_length:
431 | validators.append(validate_length(high=max_length, low=min_length))
432 | super().__init__(required=required, default=default, validators=validators)
433 |
434 | def coerce(self, value):
435 | return str(value)
436 |
437 |
438 | class FloatField(Field):
439 | """
440 | A field that will try to coerce value to a float.
441 |
442 | :param required: Is this field required?
443 | :param default: Default value to be used if no incoming value is provided.
444 | :param validators: List of validator functions to run.
445 | :param low: Lowest value that should be enfocred.
446 | :param high: Highest value that should be enfocred.
447 | """
448 |
449 | __slots__ = ('value', 'required', 'default', 'validators')
450 |
451 | def __init__(self, required=False, low=None, high=None, default=None, validators=None):
452 | validators = validators or []
453 | if low or high:
454 | validators.append(validate_range(low=low, high=high))
455 | super().__init__(required=required, default=default, validators=validators)
456 |
457 | def coerce(self, value):
458 | try:
459 | return float(value) if value else None
460 | except (TypeError, ValueError):
461 | raise ValidationError('coerce_float')
462 |
463 |
464 | class IntegerField(Field):
465 | """
466 | A field that will try to coerce value to an integer.
467 |
468 | :param required: Is this field required?
469 | :param default: Default value to be used if no incoming value is provided.
470 | :param validators: List of validator functions to run.
471 | :param low: Lowest value that should be enfocred.
472 | :param high: Highest value that should be enfocred.
473 | """
474 |
475 | __slots__ = ('value', 'required', 'default', 'validators')
476 |
477 | def __init__(self, required=False, low=None, high=None, default=None, validators=None):
478 | validators = validators or []
479 | if low or high:
480 | validators.append(validate_range(low=low, high=high))
481 | super().__init__(required=required, default=default, validators=validators)
482 |
483 | def coerce(self, value):
484 | try:
485 | return int(value) if value is not None else None
486 | except (TypeError, ValueError):
487 | raise ValidationError('coerce_int')
488 |
489 |
490 | class DecimalField(Field):
491 | """
492 | A field that will try to coerce value to a decimal.
493 |
494 | :param required: Is this field required?
495 | :param default: Default value to be used if no incoming value is provided.
496 | :param validators: List of validator functions to run.
497 | :param low: Lowest value that should be enfocred.
498 | :param high: Highest value that should be enfocred.
499 | """
500 |
501 | __slots__ = ('value', 'required', 'default', 'validators')
502 |
503 | def __init__(self, required=False, low=None, high=None, default=None, validators=None):
504 | validators = validators or []
505 | if low or high:
506 | validators.append(validate_range(low=low, high=high))
507 | super().__init__(required=required, default=default, validators=validators)
508 |
509 | def coerce(self, value):
510 | try:
511 | return Decimal(value) if value else None
512 | except (TypeError, ValueError, InvalidOperation):
513 | raise ValidationError('coerce_decimal')
514 |
515 |
516 | class DateField(Field):
517 | """
518 | A field that will try to coerce value to a date.
519 | Can accept a date object, string, or anything else that can be converted
520 | by `dateutil.parser.parse`.
521 |
522 | :param required: Is this field required?
523 | :param default: Default value to be used if no incoming value is provided.
524 | :param validators: List of validator functions to run.
525 | :param low: Lowest value that should be enfocred.
526 | :param high: Highest value that should be enfocred.
527 | """
528 |
529 | __slots__ = ('value', 'required', 'default', 'validators')
530 |
531 | def __init__(self, required=False, low=None, high=None, default=None, validators=None):
532 | validators = validators or []
533 | if low or high:
534 | validators.append(validate_range(low=low, high=high))
535 | super().__init__(required=required, default=default, validators=validators)
536 |
537 | def coerce(self, value):
538 | if not value or isinstance(value, datetime.date):
539 | return value
540 | try:
541 | return dateutil_parse(value).date()
542 | except (TypeError, ValueError):
543 | raise ValidationError('coerce_date')
544 |
545 |
546 | class TimeField(Field):
547 | """
548 | A field that will try to coerce value to a time.
549 | Can accept a time object, string, or anything else that can be converted
550 | by `dateutil.parser.parse`.
551 |
552 | :param required: Is this field required?
553 | :param default: Default value to be used if no incoming value is provided.
554 | :param validators: List of validator functions to run.
555 | :param low: Lowest value that should be enfocred.
556 | :param high: Highest value that should be enfocred.
557 | """
558 |
559 | __slots__ = ('value', 'required', 'default', 'validators')
560 |
561 | def __init__(self, required=False, low=None, high=None, default=None, validators=None):
562 | validators = validators or []
563 | if low or high:
564 | validators.append(validate_range(low=low, high=high))
565 | super().__init__(required=required, default=default, validators=validators)
566 |
567 | def coerce(self, value):
568 | if not value or isinstance(value, datetime.time):
569 | return value
570 | try:
571 | return dateutil_parse(value).time()
572 | except (TypeError, ValueError):
573 | raise ValidationError('coerce_time')
574 |
575 |
576 | class DateTimeField(Field):
577 | """
578 | A field that will try to coerce value to a datetime.
579 | Can accept a datetime object, string, or anything else that can be converted
580 | by `dateutil.parser.parse`.
581 |
582 | :param required: Is this field required?
583 | :param default: Default value to be used if no incoming value is provided.
584 | :param validators: List of validator functions to run.
585 | :param low: Lowest value that should be enfocred.
586 | :param high: Highest value that should be enfocred.
587 | """
588 |
589 | __slots__ = ('value', 'required', 'default', 'validators')
590 |
591 | def __init__(self, required=False, low=None, high=None, default=None, validators=None):
592 | validators = validators or []
593 | if low or high:
594 | validators.append(validate_range(low=low, high=high))
595 | super().__init__(required=required, default=default, validators=validators)
596 |
597 | def coerce(self, value):
598 | if not value or isinstance(value, datetime.datetime):
599 | return value
600 | try:
601 | return dateutil_parse(value)
602 | except (TypeError, ValueError):
603 | raise ValidationError('coerce_datetime')
604 |
605 |
606 | class BooleanField(Field):
607 | """
608 | A field that will try to coerce value to a boolean.
609 | By default the values is converted to string first, then compared to these values:
610 | values which are considered False: ('0', '{}', 'none', 'false')
611 | And everything else is True.
612 | """
613 |
614 | __slots__ = ('value', 'required', 'default', 'validators')
615 |
616 | false_values = ('0', '{}', '[]', 'none', 'false')
617 |
618 | def coerce(self, value):
619 | return not str(value).lower() in self.false_values
620 |
621 |
622 | class ModelChoiceField(Field):
623 | """
624 | A field that allows for a single value based on a model query and lookup field.
625 |
626 | :param query: Query to use for lookup.
627 | :param lookup_field: Field that will be queried for the value.
628 | """
629 |
630 | __slots__ = ('query', 'lookup_field', 'value', 'required', 'default', 'validators')
631 |
632 | def __init__(self, query, lookup_field, required=False, **kwargs):
633 | self.query = query
634 | self.lookup_field = lookup_field
635 | super().__init__(required=required, **kwargs)
636 |
637 | def coerce(self, value):
638 | """Convert from whatever is given to a scalar value for lookup_field."""
639 | return coerce_single_instance(self.lookup_field, value)
640 |
641 | def validate(self, name, data):
642 | """
643 | If there is a problem with the data, raise ValidationError.
644 |
645 | :param name: The name of this field.
646 | :param data: Dictionary of data for all fields.
647 | :raises: ValidationError
648 | """
649 | super().validate(name, data)
650 | if self.value is not None:
651 | try:
652 | self.value = self.query.get(self.lookup_field == self.value)
653 | except (AttributeError, ValueError, peewee.DoesNotExist):
654 | raise ValidationError('related', field=self.lookup_field.name, values=self.value)
655 |
656 |
657 | class ManyModelChoiceField(Field):
658 | """
659 | A field that allows for multiple values based on a model query and lookup field.
660 |
661 | :param query: Query to use for lookup.
662 | :param lookup_field: Field that will be queried for the value.
663 | """
664 |
665 | __slots__ = ('query', 'lookup_field', 'value', 'required', 'default', 'validators')
666 |
667 | def __init__(self, query, lookup_field, required=False, **kwargs):
668 | self.query = query
669 | self.lookup_field = lookup_field
670 | super().__init__(required=required, **kwargs)
671 |
672 | def coerce(self, value):
673 | """Convert from whatever is given to a list of scalars for the lookup_field."""
674 | if isinstance(value, dict):
675 | value = [value]
676 | if not isiterable_notstring(value):
677 | value = [value]
678 | return [coerce_single_instance(self.lookup_field, v) for v in value]
679 |
680 | def validate(self, name, data):
681 | """
682 | If there is a problem with the data, raise ValidationError.
683 |
684 | :param name: The name of this field.
685 | :param data: Dictionary of data for all fields.
686 | :raises: ValidationError
687 | """
688 | super().validate(name, data)
689 | if self.value is not None:
690 | try:
691 | # self.query could be a query like "User.select()" or a model like "User"
692 | # so ".select().where()" handles both cases.
693 | self.value = [self.query.select().where(self.lookup_field == v).get() for v in self.value if v]
694 | except (AttributeError, ValueError, peewee.DoesNotExist):
695 | raise ValidationError('related', field=self.lookup_field.name, values=self.value)
696 |
697 |
698 | class ValidatorOptions:
699 | def __init__(self, obj):
700 | self.fields = {}
701 | self.messages = {}
702 | self.only = []
703 | self.exclude = []
704 |
705 |
706 | class Validator:
707 | """
708 | A validator class. Can have many fields attached to it to perform validation on data.
709 | """
710 |
711 | class Meta:
712 | """
713 | A meta class to specify options for the validator. Uses the following fields:
714 |
715 | ``messages = {}``
716 |
717 | ``only = []``
718 |
719 | ``exclues = []``
720 | """
721 | pass
722 |
723 | __slots__ = ('data', 'errors', '_meta')
724 |
725 | def __init__(self):
726 | self.errors = {}
727 | self.data = {}
728 |
729 | self._meta = ValidatorOptions(self)
730 | self._meta.__dict__.update(self.Meta.__dict__)
731 |
732 | self.initialize_fields()
733 |
734 | def add_error(self, name, error):
735 | message = self._meta.messages.get('{}.{}'.format(name, error.key))
736 | if not message:
737 | message = self._meta.messages.get(error.key)
738 | if not message:
739 | message = DEFAULT_MESSAGES.get(error.key, 'Validation failed.')
740 | self.errors[name] = message.format(**error.kwargs)
741 |
742 | def initialize_fields(self):
743 | """
744 | The dict self.base_fields is a model instance at this point.
745 | Turn it into an instance attribute on this meta class.
746 | Also intitialize any other special fields if needed in sub-classes.
747 |
748 | :return: None
749 | """
750 | for field in dir(self):
751 | obj = getattr(self, field)
752 | if isinstance(obj, Field):
753 | self._meta.fields[field] = obj
754 |
755 | def validate(self, data=None, only=None, exclude=None):
756 | """
757 | Validate the data for all fields and return whether the validation was successful.
758 | This method also retains the validated data in ``self.data`` so that it can be accessed later.
759 |
760 | This is usually the method you want to call after creating the validator instance.
761 |
762 | :param data: Dictionary of data to validate.
763 | :param only: List or tuple of fields to validate.
764 | :param exclude: List or tuple of fields to exclude from validation.
765 | :return: True if validation was successful. Otherwise False.
766 | """
767 | only = only or []
768 | exclude = exclude or []
769 | data = data or {}
770 | self.errors = {}
771 | self.data = {}
772 |
773 | # Validate individual fields.
774 | for name, field in self._meta.fields.items():
775 | if name in exclude or (only and name not in only):
776 | continue
777 | try:
778 | field.validate(name, data)
779 | except ValidationError as err:
780 | self.add_error(name, err)
781 | continue
782 | self.data[name] = field.value
783 |
784 | # Clean individual fields.
785 | if not self.errors:
786 | self.clean_fields(self.data)
787 |
788 | # Then finally clean the whole data dict.
789 | if not self.errors:
790 | try:
791 | self.data = self.clean(self.data)
792 | except ValidationError as err:
793 | self.add_error('__base__', err)
794 |
795 | return (not self.errors)
796 |
797 | def clean_fields(self, data):
798 | """
799 | For each field, check to see if there is a clean_ method.
800 | If so, run that method and set the returned value on the self.data dict.
801 | This happens after all validations so that each field can act on the
802 | cleaned data of other fields if needed.
803 |
804 | :param data: Dictionary of data to clean.
805 | :return: None
806 | """
807 | for name, value in data.items():
808 | try:
809 | method = getattr(self, 'clean_{}'.format(name), None)
810 | if method:
811 | self.data[name] = method(value)
812 | except ValidationError as err:
813 | self.add_error(name, err)
814 | continue
815 |
816 | def clean(self, data):
817 | """
818 | Clean the data dictionary and return the cleaned values.
819 |
820 | :param data: Dictionary of data for all fields.
821 | :return: Dictionary of "clean" values.
822 | :rtype: dict
823 | """
824 | return data
825 |
826 |
827 | class ModelValidator(Validator):
828 | """
829 | A validator class based on a Peewee model instance.
830 | Fields are automatically added based on the model instance, but can be customized.
831 |
832 | :param instance: Peewee model instance to use for data lookups and field generation.
833 | """
834 |
835 | __slots__ = ('data', 'errors', '_meta', 'instance', 'pk_field', 'pk_value')
836 |
837 | FIELD_MAP = {
838 | 'smallint': IntegerField,
839 | 'bigint': IntegerField,
840 | 'bool': BooleanField,
841 | 'date': DateField,
842 | 'datetime': DateTimeField,
843 | 'decimal': DecimalField,
844 | 'double': FloatField,
845 | 'float': FloatField,
846 | 'int': IntegerField,
847 | 'time': TimeField,
848 | }
849 |
850 | def __init__(self, instance):
851 | if not isinstance(instance, peewee.Model):
852 | message = 'First argument to {} must be an instance of peewee.Model.'
853 | raise AttributeError(message.format(type(self).__name__))
854 |
855 | self.instance = instance
856 | self.pk_field = self.instance._meta.primary_key
857 | if PEEWEE3:
858 | self.pk_value = self.instance.get_id()
859 | else:
860 | self.pk_value = self.instance._get_pk_value()
861 |
862 | super().__init__()
863 |
864 | def initialize_fields(self):
865 | """
866 | Convert all model fields to validator fields.
867 | Then call the parent so that overwrites can happen if necessary for manually defined fields.
868 |
869 | :return: None
870 | """
871 | # # Pull all the "normal" fields off the model instance meta.
872 | for name, field in self.instance._meta.fields.items():
873 | if getattr(field, 'primary_key', False):
874 | continue
875 | self._meta.fields[name] = self.convert_field(name, field)
876 |
877 | # Many-to-many fields are not stored in the meta fields dict.
878 | # Pull them directly off the class.
879 | for name in dir(type(self.instance)):
880 | field = getattr(type(self.instance), name, None)
881 | if isinstance(field, ManyToManyField):
882 | self._meta.fields[name] = self.convert_field(name, field)
883 |
884 | super().initialize_fields()
885 |
886 | def convert_field(self, name, field):
887 | """
888 | Convert a single field from a Peewee model field to a validator field.
889 |
890 | :param name: Name of the field as defined on this validator.
891 | :param name: Peewee field instance.
892 | :return: Validator field.
893 | """
894 | if PEEWEE3:
895 | field_type = field.field_type.lower()
896 | else:
897 | field_type = field.db_field
898 |
899 | pwv_field = ModelValidator.FIELD_MAP.get(field_type, StringField)
900 |
901 | validators = []
902 | required = not bool(getattr(field, 'null', True))
903 | choices = getattr(field, 'choices', ())
904 | default = getattr(field, 'default', None)
905 | max_length = getattr(field, 'max_length', None)
906 | unique = getattr(field, 'unique', False)
907 |
908 | if required:
909 | validators.append(validate_required())
910 |
911 | if choices:
912 | validators.append(validate_one_of([c[0] for c in choices]))
913 |
914 | if max_length:
915 | validators.append(validate_length(high=max_length))
916 |
917 | if unique:
918 | validators.append(validate_model_unique(field, self.instance.select(), self.pk_field, self.pk_value))
919 |
920 | if isinstance(field, peewee.ForeignKeyField):
921 | if PEEWEE3:
922 | rel_field = field.rel_field
923 | else:
924 | rel_field = field.to_field
925 | return ModelChoiceField(field.rel_model, rel_field, default=default, validators=validators)
926 |
927 | if isinstance(field, ManyToManyField):
928 | return ManyModelChoiceField(
929 | field.rel_model, field.rel_model._meta.primary_key,
930 | default=default, validators=validators)
931 |
932 | return pwv_field(default=default, validators=validators)
933 |
934 | def validate(self, data=None, only=None, exclude=None):
935 | """
936 | Validate the data for all fields and return whether the validation was successful.
937 | This method also retains the validated data in ``self.data`` so that it can be accessed later.
938 |
939 | If data for a field is not provided in ``data`` then this validator will check against the
940 | provided model instance.
941 |
942 | This is usually the method you want to call after creating the validator instance.
943 |
944 | :param data: Dictionary of data to validate.
945 | :param only: List or tuple of fields to validate.
946 | :param exclude: List or tuple of fields to exclude from validation.
947 | :return: True if validation is successful, otherwise False.
948 | """
949 | data = data or {}
950 | only = only or self._meta.only
951 | exclude = exclude or self._meta.exclude
952 |
953 | for name, field in self.instance._meta.fields.items():
954 | if name in exclude or (only and name not in only):
955 | continue
956 | try:
957 | data.setdefault(name, getattr(self.instance, name, None))
958 | except (peewee.DoesNotExist):
959 | if PEEWEE3:
960 | instance_data = self.instance.__data__
961 | else:
962 | instance_data = self.instance._data
963 | data.setdefault(name, instance_data.get(name, None))
964 |
965 | # This will set self.data which we should use from now on.
966 | super().validate(data=data, only=only, exclude=exclude)
967 |
968 | if not self.errors:
969 | self.perform_index_validation(self.data)
970 |
971 | return (not self.errors)
972 |
973 | def perform_index_validation(self, data):
974 | """
975 | Validate any unique indexes specified on the model.
976 | This should happen after all the normal fields have been validated.
977 | This can add error messages to multiple fields.
978 |
979 | :return: None
980 | """
981 | # Build a list of dict containing query values for each unique index.
982 | index_data = []
983 | for columns, unique in self.instance._meta.indexes:
984 | if not unique:
985 | continue
986 | index_data.append({col: data.get(col, None) for col in columns})
987 |
988 | # Then query for each unique index to see if the value is unique.
989 | for index in index_data:
990 | query = self.instance.filter(**index)
991 | # If we have a primary key, need to exclude the current record from the check.
992 | if self.pk_field and self.pk_value:
993 | query = query.where(~(self.pk_field == self.pk_value))
994 | if query.count():
995 | err = ValidationError('index', fields=str.join(', ', index.keys()))
996 | for col in index.keys():
997 | self.add_error(col, err)
998 |
999 | def save(self, force_insert=False):
1000 | """
1001 | Save the model and any related many-to-many fields.
1002 |
1003 | :param force_insert: Should the save force an insert?
1004 | :return: Number of rows impacted, or False.
1005 | """
1006 | delayed = {}
1007 | for field, value in self.data.items():
1008 | model_field = getattr(type(self.instance), field, None)
1009 |
1010 | # If this is a many-to-many field, we cannot save it to the instance until the instance
1011 | # is saved to the database. Collect these fields and delay the setting until after
1012 | # the model instance is saved.
1013 | if isinstance(model_field, ManyToManyField):
1014 | if value is not None:
1015 | delayed[field] = value
1016 | continue
1017 |
1018 | setattr(self.instance, field, value)
1019 |
1020 | rv = self.instance.save(force_insert=force_insert)
1021 |
1022 | for field, value in delayed.items():
1023 | setattr(self.instance, field, value)
1024 |
1025 | return rv
1026 |
--------------------------------------------------------------------------------