├── 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 | --------------------------------------------------------------------------------