├── .bumpversion.cfg
├── .coveragerc
├── .editorconfig
├── .envrc.example
├── .fussyfox.yml
├── .gitattributes
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.yaml
│ └── feature_request.yaml
├── dependabot.yml
└── workflows
│ ├── auto-approve.yml
│ ├── auto-merge.yml
│ ├── changelog.yml
│ ├── codacy-analysis.yml
│ ├── gh-pages.yml
│ ├── greetings.yml
│ ├── lint.yml
│ ├── release.yml
│ ├── test.yml
│ └── update-doc-assets.yml
├── .gitignore
├── .pep8speaks.yml
├── .pre-commit-config.yaml
├── .pypirc
├── .pyup.yml
├── .whitesource
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── LICENSE-APACHE
├── LICENSE-MIT
├── MANIFEST.in
├── Makefile
├── README.md
├── demo
├── __init__.py
├── apps.py
├── migrations
│ ├── 0001_initial.py
│ └── __init__.py
└── models.py
├── django_extra_field_validation
├── __init__.py
├── settings.py
└── wsgi.py
├── docs
├── .nojekyll
├── README.md
└── index.html
├── extra_validator
├── __init__.py
├── apps.py
├── field_validation
│ ├── __init__.py
│ └── validator.py
└── tests.py
├── manage.py
├── pyproject.toml
├── pytest.ini
├── renovate.json
├── requirements.txt
├── setup.py
└── tox.ini
/.bumpversion.cfg:
--------------------------------------------------------------------------------
1 | [bumpversion]
2 | current_version = 1.1.1
3 | commit = True
4 | tag = True
5 |
6 | [bumpversion:file:setup.py]
7 | search = version="{current_version}"
8 | replace = version="{new_version}"
9 |
10 | [bumpversion:file:extra_validator/__init__.py]
11 | search = __version__ = "{current_version}"
12 | replace = __version__ = "{new_version}"
13 |
14 | [bdist_wheel]
15 | universal = 1
16 |
--------------------------------------------------------------------------------
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | source = extra_validator
3 | omit =
4 | .tox
5 | demo/apps.py
6 | django_extra_field_validation/wsgi.py
7 | setup.py
8 | manage.py
9 | extra_validator/apps.py
10 | extra_validator/tests.py
11 | branch = True
12 |
13 | [report]
14 | exclude_lines =
15 | no cov
16 | no qa
17 | noqa
18 | pragma: no cover
19 | if __name__ == .__main__.:
20 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.{py,rst,ini}]
12 | indent_style = space
13 | indent_size = 4
14 |
15 | [*.{html,css,scss,json,yml}]
16 | indent_style = space
17 | indent_size = 2
18 |
19 | [*.md]
20 | trim_trailing_whitespace = false
21 |
22 | [Makefile]
23 | indent_style = tab
24 |
--------------------------------------------------------------------------------
/.envrc.example:
--------------------------------------------------------------------------------
1 | export CODACY_PROJECT_TOKEN=
2 |
--------------------------------------------------------------------------------
/.fussyfox.yml:
--------------------------------------------------------------------------------
1 | ## list of checks you would like to run on this repository
2 | # e.g.:
3 | - flake8
4 | - black
5 | # - pycodestyle
6 | # - pydocstyle
7 | # - pyflakes
8 | # - bandit
9 | # - isort
10 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: jackton1
4 | patreon: # Replace with a single Patreon username
5 | open_collective: tj-django
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: []
13 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yaml:
--------------------------------------------------------------------------------
1 | name: 🐞 Bug
2 | description: Create a report to help us improve
3 | title: "[BUG]
"
4 | labels: [bug, needs triage]
5 |
6 | body:
7 | - type: markdown
8 | attributes:
9 | value: |
10 | Thanks for taking the time to fill out this bug report!
11 | - type: checkboxes
12 | attributes:
13 | label: Is there an existing issue for this?
14 | description: Please search to see if an issue already exists for the bug you encountered.
15 | options:
16 | - label: I have searched the existing issues
17 | required: true
18 | - type: checkboxes
19 | attributes:
20 | label: Does this issue exist in the latest version?
21 | description: Please view all releases to confirm that this issue hasn't already been fixed.
22 | options:
23 | - label: I'm using the latest release
24 | required: true
25 | - type: textarea
26 | id: what-happened
27 | attributes:
28 | label: Describe the bug?
29 | description: A clear and concise description of what the bug is
30 | placeholder: Tell us what you see!
31 | validations:
32 | required: true
33 | - type: textarea
34 | id: reproduce
35 | attributes:
36 | label: To Reproduce
37 | description: Steps to reproduce the behavior?
38 | placeholder: |
39 | 1. In this environment...
40 | 2. With this config...
41 | 3. Run '...'
42 | 4. See error...
43 | validations:
44 | required: true
45 | - type: dropdown
46 | id: os
47 | attributes:
48 | label: What OS are you seeing the problem on?
49 | multiple: true
50 | options:
51 | - all
52 | - ubuntu-latest or ubuntu-20.04
53 | - ubuntu-18.04
54 | - macos-latest or macos-10.15
55 | - macos-11
56 | - windows-latest or windows-2019
57 | - windows-2016
58 | validations:
59 | required: true
60 | - type: textarea
61 | id: expected
62 | attributes:
63 | label: Expected behavior?
64 | description: A clear and concise description of what you expected to happen.
65 | placeholder: Tell us what you expected!
66 | validations:
67 | required: true
68 | - type: textarea
69 | id: logs
70 | attributes:
71 | label: Relevant log output
72 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
73 | render: shell
74 | - type: textarea
75 | attributes:
76 | label: Anything else?
77 | description: |
78 | Links? or References?
79 |
80 | Anything that will give us more context about the issue you are encountering!
81 |
82 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
83 | validations:
84 | required: false
85 | - type: checkboxes
86 | id: terms
87 | attributes:
88 | label: Code of Conduct
89 | description: By submitting this issue, you agree to follow our [Code of Conduct](../blob/main/CODE_OF_CONDUCT.md)
90 | options:
91 | - label: I agree to follow this project's Code of Conduct
92 | required: true
93 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yaml:
--------------------------------------------------------------------------------
1 | name: Feature request
2 | description: Suggest an idea for this project
3 | title: "[Feature] "
4 | labels: [enhancement]
5 |
6 | body:
7 | - type: markdown
8 | attributes:
9 | value: |
10 | Thanks for taking the time to fill out this feature request!
11 | - type: checkboxes
12 | attributes:
13 | label: Is this feature missing in the latest version?
14 | description: Please upgrade to the latest version to verify that this feature is still missing.
15 | options:
16 | - label: I'm using the latest release
17 | required: true
18 | - type: textarea
19 | id: what-happened
20 | attributes:
21 | label: Is your feature request related to a problem? Please describe.
22 | description: |
23 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
24 | placeholder: Tell us what you see!
25 | validations:
26 | required: true
27 | - type: textarea
28 | id: requests
29 | attributes:
30 | label: Describe the solution you'd like?
31 | description: A clear and concise description of what you want to happen.
32 | validations:
33 | required: true
34 | - type: textarea
35 | id: alternative
36 | attributes:
37 | label: Describe alternatives you've considered?
38 | description: A clear and concise description of any alternative solutions or features you've considered.
39 | validations:
40 | required: false
41 | - type: textarea
42 | attributes:
43 | label: Anything else?
44 | description: |
45 | Links? or References?
46 |
47 | Add any other context or screenshots about the feature request here.
48 |
49 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
50 | validations:
51 | required: false
52 | - type: checkboxes
53 | id: terms
54 | attributes:
55 | label: Code of Conduct
56 | description: By submitting this issue, you agree to follow our [Code of Conduct](./CODE_OF_CONDUCT.md)
57 | options:
58 | - label: I agree to follow this project's Code of Conduct
59 | required: true
60 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: pip
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 10
8 | labels:
9 | - "dependencies"
10 | - "dependabot"
11 | - "automerge"
12 | - package-ecosystem: github-actions
13 | directory: "/"
14 | schedule:
15 | interval: daily
16 | open-pull-requests-limit: 10
17 | labels:
18 | - "dependencies"
19 | - "dependabot"
20 | - "automerge"
21 |
--------------------------------------------------------------------------------
/.github/workflows/auto-approve.yml:
--------------------------------------------------------------------------------
1 | name: Auto approve
2 |
3 | on:
4 | pull_request_target
5 |
6 | jobs:
7 | auto-approve:
8 | runs-on: ubuntu-latest
9 | if: |
10 | (
11 | github.event.pull_request.user.login == 'dependabot[bot]' ||
12 | github.event.pull_request.user.login == 'dependabot' ||
13 | github.event.pull_request.user.login == 'dependabot-preview[bot]' ||
14 | github.event.pull_request.user.login == 'dependabot-preview' ||
15 | github.event.pull_request.user.login == 'renovate[bot]' ||
16 | github.event.pull_request.user.login == 'renovate' ||
17 | github.event.pull_request.user.login == 'github-actions[bot]' ||
18 | github.event.pull_request.user.login == 'pre-commit-ci' ||
19 | github.event.pull_request.user.login == 'pre-commit-ci[bot]'
20 | )
21 | &&
22 | (
23 | github.actor == 'dependabot[bot]' ||
24 | github.actor == 'dependabot' ||
25 | github.actor == 'dependabot-preview[bot]' ||
26 | github.actor == 'dependabot-preview' ||
27 | github.actor == 'renovate[bot]' ||
28 | github.actor == 'renovate' ||
29 | github.actor == 'github-actions[bot]' ||
30 | github.actor == 'pre-commit-ci' ||
31 | github.actor == 'pre-commit-ci[bot]'
32 | )
33 | steps:
34 | - uses: hmarr/auto-approve-action@v2
35 | with:
36 | github-token: ${{ secrets.PAT_TOKEN }}
37 |
--------------------------------------------------------------------------------
/.github/workflows/auto-merge.yml:
--------------------------------------------------------------------------------
1 | name: automerge
2 | on:
3 | check_suite:
4 | types:
5 | - completed
6 |
7 | jobs:
8 | automerge:
9 | runs-on: ubuntu-latest
10 | if: |
11 | github.actor == 'dependabot[bot]' ||
12 | github.actor == 'dependabot' ||
13 | github.actor == 'dependabot-preview[bot]' ||
14 | github.actor == 'dependabot-preview' ||
15 | github.actor == 'renovate[bot]' ||
16 | github.actor == 'renovate'
17 | steps:
18 | - name: automerge
19 | uses: pascalgn/automerge-action@v0.15.3
20 | env:
21 | GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}
22 | MERGE_METHOD: "rebase"
23 | UPDATE_METHOD: "rebase"
24 | MERGE_RETRIES: "6"
25 | MERGE_RETRY_SLEEP: "100000"
26 | MERGE_LABELS: ""
27 |
--------------------------------------------------------------------------------
/.github/workflows/changelog.yml:
--------------------------------------------------------------------------------
1 | name: Update CHANGELOG
2 |
3 | on:
4 | release:
5 | types: [published]
6 | push:
7 | tags:
8 | - '*'
9 |
10 | jobs:
11 | changelog:
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v3.0.2
17 |
18 | - name: Generate CHANGELOG
19 | uses: tj-actions/github-changelog-generator@v1.13
20 | with:
21 | token: ${{ secrets.PAT_TOKEN }}
22 |
23 | - name: Create Pull Request
24 | uses: peter-evans/create-pull-request@v4.0.4
25 | with:
26 | token: ${{ secrets.PAT_TOKEN }}
27 | commit-message: Update CHANGELOG
28 | committer: github-actions[bot]
29 | author: github-actions[bot]
30 | branch: chore/update-changelog
31 | base: main
32 | delete-branch: true
33 | title: 'Update CHANGELOG'
34 | body: |
35 | Update changelog
36 | - Auto-generated by github-actions[bot]
37 | assignees: jackton1
38 | reviewers: jackton1
39 |
--------------------------------------------------------------------------------
/.github/workflows/codacy-analysis.yml:
--------------------------------------------------------------------------------
1 | # This workflow checks out code, performs a Codacy security scan
2 | # and integrates the results with the
3 | # GitHub Advanced Security code scanning feature. For more information on
4 | # the Codacy security scan action usage and parameters, see
5 | # https://github.com/codacy/codacy-analysis-cli-action.
6 | # For more information on Codacy Analysis CLI in general, see
7 | # https://github.com/codacy/codacy-analysis-cli.
8 |
9 | name: Codacy Security Scan
10 |
11 | on:
12 | push:
13 | branches: [ main ]
14 | pull_request:
15 | # The branches below must be a subset of the branches above
16 | branches: [ main ]
17 | schedule:
18 | - cron: '15 16 * * 2'
19 |
20 | jobs:
21 | codacy-security-scan:
22 | name: Codacy Security Scan
23 | runs-on: ubuntu-latest
24 | steps:
25 | # Checkout the repository to the GitHub Actions runner
26 | - name: Checkout code
27 | uses: actions/checkout@v3
28 |
29 | # Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis
30 | - name: Run Codacy Analysis CLI
31 | uses: codacy/codacy-analysis-cli-action@4.0.2
32 | with:
33 | # Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository
34 | # You can also omit the token and run the tools that support default configurations
35 | project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
36 | verbose: true
37 | output: results.sarif
38 | format: sarif
39 | # Adjust severity of non-security issues
40 | gh-code-scanning-compat: true
41 | # Force 0 exit code to allow SARIF file generation
42 | # This will handover control about PR rejection to the GitHub side
43 | max-allowed-issues: 2147483647
44 |
45 | # Upload the SARIF file generated in the previous step
46 | - name: Upload SARIF results file
47 | uses: github/codeql-action/upload-sarif@v2
48 | with:
49 | sarif_file: results.sarif
50 |
--------------------------------------------------------------------------------
/.github/workflows/gh-pages.yml:
--------------------------------------------------------------------------------
1 | name: Github pages
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | deploy:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v3.0.2
13 | with:
14 | fetch-depth: 0
15 |
16 | - name: Deploy
17 | uses: peaceiris/actions-gh-pages@v3.8.0
18 | with:
19 | github_token: ${{ secrets.GITHUB_TOKEN }}
20 | publish_dir: ./docs
21 |
--------------------------------------------------------------------------------
/.github/workflows/greetings.yml:
--------------------------------------------------------------------------------
1 | name: Greetings
2 |
3 | on: [pull_request_target, issues]
4 |
5 | jobs:
6 | greeting:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/first-interaction@v1.1.0
10 | with:
11 | repo-token: ${{ secrets.GITHUB_TOKEN }}
12 | issue-message: "Thanks for reporting this issue, don't forget to star this project to help us reach a wider audience."
13 | pr-message: "Thanks for implementing a fix, could you ensure that the test covers your changes."
14 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Run linters
2 | on:
3 | pull_request:
4 | branches:
5 | - main
6 | paths-ignore:
7 | - "README.md"
8 | - "docs/**"
9 |
10 | jobs:
11 | lint:
12 | runs-on: ubuntu-latest
13 | if: github.actor != 'dependabot[bot]' && github.actor != 'dependabot'
14 | strategy:
15 | matrix:
16 | python-version: [3.5, 3.6, 3.7, 3.8, 3.9]
17 |
18 | steps:
19 | - uses: actions/checkout@v3.0.2
20 | - name: Set up Python ${{ matrix.python-version }}
21 | uses: actions/setup-python@v3.1.2
22 | with:
23 | python-version: ${{ matrix.python-version }}
24 | - uses: actions/cache@v3.0.4
25 | id: pip-cache
26 | with:
27 | path: ~/.cache/pip
28 | key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}
29 | restore-keys: |
30 | ${{ runner.os }}-pip-${{ matrix.python-version }}-
31 | - name: Install dependencies
32 | run: |
33 | pip install -U pip
34 | pip install flake8==3.8.4
35 | - name: Install black
36 | if: ${{ matrix.python-version != '3.5' }}
37 | run: |
38 | pip install black
39 | - name: Run Lint
40 | uses: wearerequired/lint-action@v2.0.0
41 | with:
42 | github_token: ${{ secrets.github_token }}
43 | black: ${{ matrix.python-version != '3.5' }}
44 | flake8: true
45 | git_email: "github-action[bot]@github.com"
46 | auto_fix: ${{ matrix.python-version != '3.5' }}
47 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Create New Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - '*'
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout
13 | uses: actions/checkout@v3.0.2
14 | - name: Create Release
15 | uses: softprops/action-gh-release@v1
16 | env:
17 | GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}
18 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: CI Test
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | paths-ignore:
7 | - "README.md"
8 | - "docs/**"
9 | pull_request:
10 | branches: [ main ]
11 | paths-ignore:
12 | - "README.md"
13 | - "docs/**"
14 |
15 | jobs:
16 | build:
17 | runs-on: ubuntu-latest
18 | strategy:
19 | matrix:
20 | python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9, 'pypy-2.7', 'pypy-3.6', 'pypy-3.7']
21 |
22 | steps:
23 | - uses: actions/checkout@v3.0.2
24 | - name: Set up Python ${{ matrix.python-version }}
25 | uses: actions/setup-python@v3.1.2
26 | with:
27 | python-version: ${{ matrix.python-version }}
28 | - name: Install dependencies
29 | run: |
30 | python -m pip install --upgrade pip
31 | pip install tox tox-gh-actions
32 | - name: Test with tox
33 | run: tox
34 | env:
35 | DJANGO_SETTINGS_MODULE: django_extra_field_validation.settings
36 | CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }}
37 |
--------------------------------------------------------------------------------
/.github/workflows/update-doc-assets.yml:
--------------------------------------------------------------------------------
1 | name: Sync doc assets.
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | sync-readme:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v3
13 | with:
14 | fetch-depth: 0
15 |
16 | - name: Run remark
17 | uses: tj-actions/remark@v3
18 |
19 | - name: Verify Changed files
20 | uses: tj-actions/verify-changed-files@v9
21 | id: verify_changed_files
22 | with:
23 | files: |
24 | README.md
25 |
26 | - name: README.md changed
27 | if: steps.verify_changed_files.outputs.files_changed == 'true'
28 | run: |
29 | echo "README.md has uncommited changes"
30 | exit 1
31 |
32 | - name: Create Pull Request
33 | if: failure()
34 | uses: peter-evans/create-pull-request@v4
35 | with:
36 | base: "main"
37 | title: "Updated README.md"
38 | branch: "chore/update-readme"
39 | commit-message: "Updated README.md"
40 | body: "Updated README.md"
41 | token: ${{ secrets.PAT_TOKEN }}
42 |
43 | - name: Copy README
44 | run: |
45 | cp -f README.md docs/README.md
46 |
47 | - name: Create Pull Request
48 | uses: peter-evans/create-pull-request@v4.0.4
49 | with:
50 | commit-message: Synced README changes to docs
51 | committer: github-actions[bot]
52 | author: github-actions[bot]
53 | branch: chore/update-docs
54 | base: main
55 | delete-branch: true
56 | title: Updated docs
57 | body: |
58 | Updated docs
59 | - Auto-generated by github-actions[bot]
60 | assignees: jackton1
61 | reviewers: jackton1
62 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | *.pyc
3 | .cache/
4 | .coverage
5 | .idea/
6 | .vscode/
7 | *.egg-info/
8 | build/
9 | dist/
10 | docs/build/
11 | venv/
12 | wheelhouse/
13 | .tox
14 | *.sqlite3
15 | .DS_*
16 | pip-wheel-metadata/
17 | .envrc
18 | coverage.xml
19 |
--------------------------------------------------------------------------------
/.pep8speaks.yml:
--------------------------------------------------------------------------------
1 | scanner:
2 | diff_only: True # If False, the entire file touched by the Pull Request is scanned for errors. If True, only the diff is scanned.
3 | linter: pycodestyle
4 |
5 | pycodestyle: # Same as scanner.linter value. Other option is flake8
6 | max-line-length: 100 # Default is 79 in PEP 8
7 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/myint/autoflake
3 | rev: v1.4
4 | hooks:
5 | - id: autoflake
6 | args: ['--in-place', '--remove-all-unused-imports', '--remove-unused-variable']
7 |
8 | - repo: https://github.com/pycqa/isort
9 | rev: 5.10.1
10 | hooks:
11 | - id: isort
12 | args: ["--profile", "black", "--filter-files"]
13 |
14 | - repo: https://github.com/pre-commit/pre-commit-hooks
15 | rev: v4.2.0
16 | hooks:
17 | - id: trailing-whitespace
18 | exclude: ^docs/.*|.*.md
19 | - id: end-of-file-fixer
20 | exclude: ^docs/.*|.*.md
21 |
22 | - repo: https://github.com/psf/black
23 | rev: 22.3.0
24 | hooks:
25 | - id: black
26 | language_version: python3
27 |
--------------------------------------------------------------------------------
/.pypirc:
--------------------------------------------------------------------------------
1 | [distutils]
2 | index-servers =
3 | pypi
4 |
5 | [pypi]
6 | repository: https://upload.pypi.org/legacy/
7 |
--------------------------------------------------------------------------------
/.pyup.yml:
--------------------------------------------------------------------------------
1 | # autogenerated pyup.io config file
2 | # see https://pyup.io/docs/configuration/ for all available options
3 |
4 | schedule: ''
5 | update: False
6 |
--------------------------------------------------------------------------------
/.whitesource:
--------------------------------------------------------------------------------
1 | {
2 | "scanSettings": {
3 | "baseBranches": []
4 | },
5 | "checkRunSettings": {
6 | "vulnerableCheckRunConclusionLevel": "failure",
7 | "displayMode": "diff"
8 | },
9 | "issueSettings": {
10 | "minSeverityLevel": "LOW"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## [v1.1.1](https://github.com/tj-django/django-extra-field-validation/tree/v1.1.1) (2021-03-21)
4 |
5 | [Full Changelog](https://github.com/tj-django/django-extra-field-validation/compare/v1.1.1...HEAD)
6 |
7 | **Merged pull requests:**
8 |
9 | - Updated docs [\#122](https://github.com/tj-django/django-extra-field-validation/pull/122) ([github-actions[bot]](https://github.com/apps/github-actions))
10 | - Update CHANGELOG [\#121](https://github.com/tj-django/django-extra-field-validation/pull/121) ([jackton1](https://github.com/jackton1))
11 |
12 | ## [v1.1.1](https://github.com/tj-django/django-extra-field-validation/tree/v1.1.1) (2021-03-21)
13 |
14 | [Full Changelog](https://github.com/tj-django/django-extra-field-validation/compare/v1.1.0...v1.1.1)
15 |
16 | ## [v1.1.0](https://github.com/tj-django/django-extra-field-validation/tree/v1.1.0) (2021-03-21)
17 |
18 | [Full Changelog](https://github.com/tj-django/django-extra-field-validation/compare/v1.0.2...v1.1.0)
19 |
20 | **Merged pull requests:**
21 |
22 | - Updated docs [\#120](https://github.com/tj-django/django-extra-field-validation/pull/120) ([github-actions[bot]](https://github.com/apps/github-actions))
23 |
24 | ## [v1.0.2](https://github.com/tj-django/django-extra-field-validation/tree/v1.0.2) (2021-03-20)
25 |
26 | [Full Changelog](https://github.com/tj-django/django-extra-field-validation/compare/v1.0.1...v1.0.2)
27 |
28 | ## [v1.0.1](https://github.com/tj-django/django-extra-field-validation/tree/v1.0.1) (2021-03-20)
29 |
30 | [Full Changelog](https://github.com/tj-django/django-extra-field-validation/compare/v1.0.0...v1.0.1)
31 |
32 | **Closed issues:**
33 |
34 | - Fix README adding links to sections [\#110](https://github.com/tj-django/django-extra-field-validation/issues/110)
35 |
36 | **Merged pull requests:**
37 |
38 | - Updated docs [\#118](https://github.com/tj-django/django-extra-field-validation/pull/118) ([github-actions[bot]](https://github.com/apps/github-actions))
39 | - Updated docs [\#117](https://github.com/tj-django/django-extra-field-validation/pull/117) ([github-actions[bot]](https://github.com/apps/github-actions))
40 | - Update wearerequired/lint-action action to v1.9.0 [\#116](https://github.com/tj-django/django-extra-field-validation/pull/116) ([renovate[bot]](https://github.com/apps/renovate))
41 | - Updated docs [\#115](https://github.com/tj-django/django-extra-field-validation/pull/115) ([github-actions[bot]](https://github.com/apps/github-actions))
42 | - Update wearerequired/lint-action action to v1.8.0 [\#114](https://github.com/tj-django/django-extra-field-validation/pull/114) ([renovate[bot]](https://github.com/apps/renovate))
43 | - Updated docs [\#112](https://github.com/tj-django/django-extra-field-validation/pull/112) ([github-actions[bot]](https://github.com/apps/github-actions))
44 | - Update CHANGELOG [\#111](https://github.com/tj-django/django-extra-field-validation/pull/111) ([jackton1](https://github.com/jackton1))
45 |
46 | ## [v1.0.0](https://github.com/tj-django/django-extra-field-validation/tree/v1.0.0) (2021-03-03)
47 |
48 | [Full Changelog](https://github.com/tj-django/django-extra-field-validation/compare/v0.2.2...v1.0.0)
49 |
50 | ## [v0.2.2](https://github.com/tj-django/django-extra-field-validation/tree/v0.2.2) (2021-03-03)
51 |
52 | [Full Changelog](https://github.com/tj-django/django-extra-field-validation/compare/v0.2.0...v0.2.2)
53 |
54 | **Merged pull requests:**
55 |
56 | - Update dependency bump2version to v1 [\#113](https://github.com/tj-django/django-extra-field-validation/pull/113) ([renovate[bot]](https://github.com/apps/renovate))
57 | - Updated docs [\#109](https://github.com/tj-django/django-extra-field-validation/pull/109) ([github-actions[bot]](https://github.com/apps/github-actions))
58 | - Update to use bump2version [\#108](https://github.com/tj-django/django-extra-field-validation/pull/108) ([jackton1](https://github.com/jackton1))
59 | - Increased test coverage and update tox config. [\#106](https://github.com/tj-django/django-extra-field-validation/pull/106) ([jackton1](https://github.com/jackton1))
60 | - Update CHANGELOG [\#105](https://github.com/tj-django/django-extra-field-validation/pull/105) ([jackton1](https://github.com/jackton1))
61 |
62 | ## [v0.2.0](https://github.com/tj-django/django-extra-field-validation/tree/v0.2.0) (2021-02-25)
63 |
64 | [Full Changelog](https://github.com/tj-django/django-extra-field-validation/compare/v0.1.13...v0.2.0)
65 |
66 | **Closed issues:**
67 |
68 | - Add black to auto fix lint errors. [\#99](https://github.com/tj-django/django-extra-field-validation/issues/99)
69 | - Update usage of ugettext to gettext to fix Deprecation warnings. [\#90](https://github.com/tj-django/django-extra-field-validation/issues/90)
70 |
71 | **Merged pull requests:**
72 |
73 | - Added support for generating CHANGELOG.md [\#104](https://github.com/tj-django/django-extra-field-validation/pull/104) ([jackton1](https://github.com/jackton1))
74 | - Updated docs [\#103](https://github.com/tj-django/django-extra-field-validation/pull/103) ([github-actions[bot]](https://github.com/apps/github-actions))
75 | - Update README.md [\#102](https://github.com/tj-django/django-extra-field-validation/pull/102) ([jackton1](https://github.com/jackton1))
76 | - Added docs. [\#101](https://github.com/tj-django/django-extra-field-validation/pull/101) ([jackton1](https://github.com/jackton1))
77 | - Update README.md [\#100](https://github.com/tj-django/django-extra-field-validation/pull/100) ([jackton1](https://github.com/jackton1))
78 | - Increase test coverage. [\#98](https://github.com/tj-django/django-extra-field-validation/pull/98) ([jackton1](https://github.com/jackton1))
79 | - Update and rename README.rst to README.md [\#97](https://github.com/tj-django/django-extra-field-validation/pull/97) ([jackton1](https://github.com/jackton1))
80 | - Fixed test [\#96](https://github.com/tj-django/django-extra-field-validation/pull/96) ([jackton1](https://github.com/jackton1))
81 | - Feature/resolve deprecation warning [\#94](https://github.com/tj-django/django-extra-field-validation/pull/94) ([jackton1](https://github.com/jackton1))
82 | - Develop [\#93](https://github.com/tj-django/django-extra-field-validation/pull/93) ([jackton1](https://github.com/jackton1))
83 | - Feature/remove pinned package versions [\#92](https://github.com/tj-django/django-extra-field-validation/pull/92) ([jackton1](https://github.com/jackton1))
84 | - Add a Codacy badge to README.rst [\#91](https://github.com/tj-django/django-extra-field-validation/pull/91) ([codacy-badger](https://github.com/codacy-badger))
85 | - Update setup.py [\#89](https://github.com/tj-django/django-extra-field-validation/pull/89) ([jackton1](https://github.com/jackton1))
86 | - Update README.rst [\#88](https://github.com/tj-django/django-extra-field-validation/pull/88) ([jackton1](https://github.com/jackton1))
87 | - Update dependency mock to v4 [\#87](https://github.com/tj-django/django-extra-field-validation/pull/87) ([renovate[bot]](https://github.com/apps/renovate))
88 | - Update dependency yamllint to v1.24.2 [\#85](https://github.com/tj-django/django-extra-field-validation/pull/85) ([renovate[bot]](https://github.com/apps/renovate))
89 | - Update dependency tox to v3.20.0 [\#84](https://github.com/tj-django/django-extra-field-validation/pull/84) ([renovate[bot]](https://github.com/apps/renovate))
90 | - Update dependency six to v1.15.0 [\#26](https://github.com/tj-django/django-extra-field-validation/pull/26) ([renovate[bot]](https://github.com/apps/renovate))
91 | - Update dependency isort to v4.3.21 [\#24](https://github.com/tj-django/django-extra-field-validation/pull/24) ([renovate[bot]](https://github.com/apps/renovate))
92 | - Update dependency future to v0.18.2 [\#23](https://github.com/tj-django/django-extra-field-validation/pull/23) ([renovate[bot]](https://github.com/apps/renovate))
93 | - Update README.rst [\#18](https://github.com/tj-django/django-extra-field-validation/pull/18) ([jackton1](https://github.com/jackton1))
94 | - Configure Renovate [\#17](https://github.com/tj-django/django-extra-field-validation/pull/17) ([renovate[bot]](https://github.com/apps/renovate))
95 |
96 | ## [v0.1.13](https://github.com/tj-django/django-extra-field-validation/tree/v0.1.13) (2020-02-23)
97 |
98 | [Full Changelog](https://github.com/tj-django/django-extra-field-validation/compare/v0.1.12...v0.1.13)
99 |
100 | ## [v0.1.12](https://github.com/tj-django/django-extra-field-validation/tree/v0.1.12) (2020-02-23)
101 |
102 | [Full Changelog](https://github.com/tj-django/django-extra-field-validation/compare/v0.1.11...v0.1.12)
103 |
104 | **Closed issues:**
105 |
106 | - Update the `long\_description\_content\_type` to text/x-rst [\#14](https://github.com/tj-django/django-extra-field-validation/issues/14)
107 |
108 | **Merged pull requests:**
109 |
110 | - Update README.rst [\#16](https://github.com/tj-django/django-extra-field-validation/pull/16) ([jackton1](https://github.com/jackton1))
111 |
112 | ## [v0.1.11](https://github.com/tj-django/django-extra-field-validation/tree/v0.1.11) (2019-04-16)
113 |
114 | [Full Changelog](https://github.com/tj-django/django-extra-field-validation/compare/v0.1.8...v0.1.11)
115 |
116 | **Merged pull requests:**
117 |
118 | - Develop [\#13](https://github.com/tj-django/django-extra-field-validation/pull/13) ([jackton1](https://github.com/jackton1))
119 |
120 | ## [v0.1.8](https://github.com/tj-django/django-extra-field-validation/tree/v0.1.8) (2019-04-16)
121 |
122 | [Full Changelog](https://github.com/tj-django/django-extra-field-validation/compare/v0.1.10...v0.1.8)
123 |
124 | **Merged pull requests:**
125 |
126 | - Update README.rst [\#11](https://github.com/tj-django/django-extra-field-validation/pull/11) ([jackton1](https://github.com/jackton1))
127 | - Fixed code style errors. [\#9](https://github.com/tj-django/django-extra-field-validation/pull/9) ([jackton1](https://github.com/jackton1))
128 |
129 | ## [v0.1.10](https://github.com/tj-django/django-extra-field-validation/tree/v0.1.10) (2019-01-29)
130 |
131 | [Full Changelog](https://github.com/tj-django/django-extra-field-validation/compare/v0.1.9...v0.1.10)
132 |
133 | ## [v0.1.9](https://github.com/tj-django/django-extra-field-validation/tree/v0.1.9) (2019-01-29)
134 |
135 | [Full Changelog](https://github.com/tj-django/django-extra-field-validation/compare/v0.1.7...v0.1.9)
136 |
137 | **Fixed bugs:**
138 |
139 | - No module named `dynamic\_validator.field\_validation` [\#7](https://github.com/tj-django/django-extra-field-validation/issues/7)
140 |
141 | **Merged pull requests:**
142 |
143 | - Updated the MANIFEST.in [\#8](https://github.com/tj-django/django-extra-field-validation/pull/8) ([jackton1](https://github.com/jackton1))
144 |
145 | ## [v0.1.7](https://github.com/tj-django/django-extra-field-validation/tree/v0.1.7) (2019-01-29)
146 |
147 | [Full Changelog](https://github.com/tj-django/django-extra-field-validation/compare/v0.1.6...v0.1.7)
148 |
149 | ## [v0.1.6](https://github.com/tj-django/django-extra-field-validation/tree/v0.1.6) (2019-01-29)
150 |
151 | [Full Changelog](https://github.com/tj-django/django-extra-field-validation/compare/v0.1.5...v0.1.6)
152 |
153 | ## [v0.1.5](https://github.com/tj-django/django-extra-field-validation/tree/v0.1.5) (2019-01-27)
154 |
155 | [Full Changelog](https://github.com/tj-django/django-extra-field-validation/compare/v0.1.4...v0.1.5)
156 |
157 | **Merged pull requests:**
158 |
159 | - Removed redundant i.e statements [\#5](https://github.com/tj-django/django-extra-field-validation/pull/5) ([jackton1](https://github.com/jackton1))
160 | - Updated Django version. [\#4](https://github.com/tj-django/django-extra-field-validation/pull/4) ([jackton1](https://github.com/jackton1))
161 | - Update README.rst [\#2](https://github.com/tj-django/django-extra-field-validation/pull/2) ([jackton1](https://github.com/jackton1))
162 |
163 | ## [v0.1.4](https://github.com/tj-django/django-extra-field-validation/tree/v0.1.4) (2018-12-09)
164 |
165 | [Full Changelog](https://github.com/tj-django/django-extra-field-validation/compare/v0.1.3...v0.1.4)
166 |
167 | ## [v0.1.3](https://github.com/tj-django/django-extra-field-validation/tree/v0.1.3) (2018-12-09)
168 |
169 | [Full Changelog](https://github.com/tj-django/django-extra-field-validation/compare/v0.1.2...v0.1.3)
170 |
171 | ## [v0.1.2](https://github.com/tj-django/django-extra-field-validation/tree/v0.1.2) (2018-12-09)
172 |
173 | [Full Changelog](https://github.com/tj-django/django-extra-field-validation/compare/v0.1.1...v0.1.2)
174 |
175 | ## [v0.1.1](https://github.com/tj-django/django-extra-field-validation/tree/v0.1.1) (2018-12-09)
176 |
177 | [Full Changelog](https://github.com/tj-django/django-extra-field-validation/compare/v0.1.0...v0.1.1)
178 |
179 | ## [v0.1.0](https://github.com/tj-django/django-extra-field-validation/tree/v0.1.0) (2018-12-09)
180 |
181 | [Full Changelog](https://github.com/tj-django/django-extra-field-validation/compare/v0.0.1...v0.1.0)
182 |
183 | ## [v0.0.1](https://github.com/tj-django/django-extra-field-validation/tree/v0.0.1) (2018-12-09)
184 |
185 | [Full Changelog](https://github.com/tj-django/django-extra-field-validation/compare/82382cb1beb5a4deaf24e444a7c541368394758c...v0.0.1)
186 |
187 |
188 |
189 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
190 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | jtonye@ymail.com.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/LICENSE-APACHE:
--------------------------------------------------------------------------------
1 | Copyright 2018 Tonye Jack
2 |
3 | Apache License
4 | Version 2.0, January 2004
5 | http://www.apache.org/licenses/
6 |
7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
8 |
9 | 1. Definitions.
10 |
11 | "License" shall mean the terms and conditions for use, reproduction,
12 | and distribution as defined by Sections 1 through 9 of this document.
13 |
14 | "Licensor" shall mean the copyright owner or entity authorized by
15 | the copyright owner that is granting the License.
16 |
17 | "Legal Entity" shall mean the union of the acting entity and all
18 | other entities that control, are controlled by, or are under common
19 | control with that entity. For the purposes of this definition,
20 | "control" means (i) the power, direct or indirect, to cause the
21 | direction or management of such entity, whether by contract or
22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
23 | outstanding shares, or (iii) beneficial ownership of such entity.
24 |
25 | "You" (or "Your") shall mean an individual or Legal Entity
26 | exercising permissions granted by this License.
27 |
28 | "Source" form shall mean the preferred form for making modifications,
29 | including but not limited to software source code, documentation
30 | source, and configuration files.
31 |
32 | "Object" form shall mean any form resulting from mechanical
33 | transformation or translation of a Source form, including but
34 | not limited to compiled object code, generated documentation,
35 | and conversions to other media types.
36 |
37 | "Work" shall mean the work of authorship, whether in Source or
38 | Object form, made available under the License, as indicated by a
39 | copyright notice that is included in or attached to the work
40 | (an example is provided in the Appendix below).
41 |
42 | "Derivative Works" shall mean any work, whether in Source or Object
43 | form, that is based on (or derived from) the Work and for which the
44 | editorial revisions, annotations, elaborations, or other modifications
45 | represent, as a whole, an original work of authorship. For the purposes
46 | of this License, Derivative Works shall not include works that remain
47 | separable from, or merely link (or bind by name) to the interfaces of,
48 | the Work and Derivative Works thereof.
49 |
50 | "Contribution" shall mean any work of authorship, including
51 | the original version of the Work and any modifications or additions
52 | to that Work or Derivative Works thereof, that is intentionally
53 | submitted to Licensor for inclusion in the Work by the copyright owner
54 | or by an individual or Legal Entity authorized to submit on behalf of
55 | the copyright owner. For the purposes of this definition, "submitted"
56 | means any form of electronic, verbal, or written communication sent
57 | to the Licensor or its representatives, including but not limited to
58 | communication on electronic mailing lists, source code control systems,
59 | and issue tracking systems that are managed by, or on behalf of, the
60 | Licensor for the purpose of discussing and improving the Work, but
61 | excluding communication that is conspicuously marked or otherwise
62 | designated in writing by the copyright owner as "Not a Contribution."
63 |
64 | "Contributor" shall mean Licensor and any individual or Legal Entity
65 | on behalf of whom a Contribution has been received by Licensor and
66 | subsequently incorporated within the Work.
67 |
68 | 2. Grant of Copyright License. Subject to the terms and conditions of
69 | this License, each Contributor hereby grants to You a perpetual,
70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
71 | copyright license to reproduce, prepare Derivative Works of,
72 | publicly display, publicly perform, sublicense, and distribute the
73 | Work and such Derivative Works in Source or Object form.
74 |
75 | 3. Grant of Patent License. Subject to the terms and conditions of
76 | this License, each Contributor hereby grants to You a perpetual,
77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
78 | (except as stated in this section) patent license to make, have made,
79 | use, offer to sell, sell, import, and otherwise transfer the Work,
80 | where such license applies only to those patent claims licensable
81 | by such Contributor that are necessarily infringed by their
82 | Contribution(s) alone or by combination of their Contribution(s)
83 | with the Work to which such Contribution(s) was submitted. If You
84 | institute patent litigation against any entity (including a
85 | cross-claim or counterclaim in a lawsuit) alleging that the Work
86 | or a Contribution incorporated within the Work constitutes direct
87 | or contributory patent infringement, then any patent licenses
88 | granted to You under this License for that Work shall terminate
89 | as of the date such litigation is filed.
90 |
91 | 4. Redistribution. You may reproduce and distribute copies of the
92 | Work or Derivative Works thereof in any medium, with or without
93 | modifications, and in Source or Object form, provided that You
94 | meet the following conditions:
95 |
96 | (a) You must give any other recipients of the Work or
97 | Derivative Works a copy of this License; and
98 |
99 | (b) You must cause any modified files to carry prominent notices
100 | stating that You changed the files; and
101 |
102 | (c) You must retain, in the Source form of any Derivative Works
103 | that You distribute, all copyright, patent, trademark, and
104 | attribution notices from the Source form of the Work,
105 | excluding those notices that do not pertain to any part of
106 | the Derivative Works; and
107 |
108 | (d) If the Work includes a "NOTICE" text file as part of its
109 | distribution, then any Derivative Works that You distribute must
110 | include a readable copy of the attribution notices contained
111 | within such NOTICE file, excluding those notices that do not
112 | pertain to any part of the Derivative Works, in at least one
113 | of the following places: within a NOTICE text file distributed
114 | as part of the Derivative Works; within the Source form or
115 | documentation, if provided along with the Derivative Works; or,
116 | within a display generated by the Derivative Works, if and
117 | wherever such third-party notices normally appear. The contents
118 | of the NOTICE file are for informational purposes only and
119 | do not modify the License. You may add Your own attribution
120 | notices within Derivative Works that You distribute, alongside
121 | or as an addendum to the NOTICE text from the Work, provided
122 | that such additional attribution notices cannot be construed
123 | as modifying the License.
124 |
125 | You may add Your own copyright statement to Your modifications and
126 | may provide additional or different license terms and conditions
127 | for use, reproduction, or distribution of Your modifications, or
128 | for any such Derivative Works as a whole, provided Your use,
129 | reproduction, and distribution of the Work otherwise complies with
130 | the conditions stated in this License.
131 |
132 | 5. Submission of Contributions. Unless You explicitly state otherwise,
133 | any Contribution intentionally submitted for inclusion in the Work
134 | by You to the Licensor shall be under the terms and conditions of
135 | this License, without any additional terms or conditions.
136 | Notwithstanding the above, nothing herein shall supersede or modify
137 | the terms of any separate license agreement you may have executed
138 | with Licensor regarding such Contributions.
139 |
140 | 6. Trademarks. This License does not grant permission to use the trade
141 | names, trademarks, service marks, or product names of the Licensor,
142 | except as required for reasonable and customary use in describing the
143 | origin of the Work and reproducing the content of the NOTICE file.
144 |
145 | 7. Disclaimer of Warranty. Unless required by applicable law or
146 | agreed to in writing, Licensor provides the Work (and each
147 | Contributor provides its Contributions) on an "AS IS" BASIS,
148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
149 | implied, including, without limitation, any warranties or conditions
150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
151 | PARTICULAR PURPOSE. You are solely responsible for determining the
152 | appropriateness of using or redistributing the Work and assume any
153 | risks associated with Your exercise of permissions under this License.
154 |
155 | 8. Limitation of Liability. In no event and under no legal theory,
156 | whether in tort (including negligence), contract, or otherwise,
157 | unless required by applicable law (such as deliberate and grossly
158 | negligent acts) or agreed to in writing, shall any Contributor be
159 | liable to You for damages, including any direct, indirect, special,
160 | incidental, or consequential damages of any character arising as a
161 | result of this License or out of the use or inability to use the
162 | Work (including but not limited to damages for loss of goodwill,
163 | work stoppage, computer failure or malfunction, or any and all
164 | other commercial damages or losses), even if such Contributor
165 | has been advised of the possibility of such damages.
166 |
167 | 9. Accepting Warranty or Additional Liability. While redistributing
168 | the Work or Derivative Works thereof, You may choose to offer,
169 | and charge a fee for, acceptance of support, warranty, indemnity,
170 | or other liability obligations and/or rights consistent with this
171 | License. However, in accepting such obligations, You may act only
172 | on Your own behalf and on Your sole responsibility, not on behalf
173 | of any other Contributor, and only if You agree to indemnify,
174 | defend, and hold each Contributor harmless for any liability
175 | incurred by, or claims asserted against, such Contributor by reason
176 | of your accepting any such warranty or additional liability.
177 |
178 | END OF TERMS AND CONDITIONS
179 |
--------------------------------------------------------------------------------
/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | Copyright (c) 2018 Tonye Jack
2 |
3 | MIT License
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | # added by check_manifest.py
2 | prune test*.py
3 | prune extra_validator/tests.py
4 | recursive-include extra_validator *.py
5 | include *.py
6 | exclude *.toml
7 | include *.txt
8 | exclude *.xml
9 | exclude .bumpversion.cfg
10 | exclude .coveragerc
11 | exclude Makefile
12 | exclude pytest.ini
13 | exclude tox.ini
14 | prune demo
15 | exclude manage.py
16 | exclude extra_validator/tests.py
17 | recursive-exclude django_extra_field_validation *.py
18 | include LICENSE-APACHE
19 | include LICENSE-MIT
20 | include README.rst
21 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # Self-Documented Makefile see https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
2 |
3 | .DEFAULT_GOAL := help
4 |
5 | PYTHON := /usr/bin/env python
6 | MANAGE_PY := $(PYTHON) manage.py
7 | PYTHON_PIP := /usr/bin/env pip
8 | PIP_COMPILE := /usr/bin/env pip-compile
9 | PART := patch
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-32s-\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
14 |
15 | .PHONY: help
16 |
17 | guard-%: ## Checks that env var is set else exits with non 0 mainly used in CI;
18 | @if [ -z '${${*}}' ]; then echo 'Environment variable $* not set' && exit 1; fi
19 |
20 | # --------------------------------------------------------
21 | # ------- Python package (pip) management commands -------
22 | # --------------------------------------------------------
23 |
24 | clean-build: ## Clean project build artifacts.
25 | @echo "Removing build assets..."
26 | @$(PYTHON) setup.py clean
27 | @rm -rf build/
28 | @rm -rf dist/
29 | @rm -rf *.egg-info
30 |
31 | install: clean-build ## Install project dependencies.
32 | @echo "Installing project in dependencies..."
33 | @$(PYTHON_PIP) install -r requirements.txt
34 |
35 | install-lint: pipconf clean-build ## Install lint extra dependencies.
36 | @echo "Installing lint extra requirements..."
37 | @$(PYTHON_PIP) install -e .'[lint]'
38 |
39 | install-test: clean-build ## Install test extra dependencies.
40 | @echo "Installing test extra requirements..."
41 | @$(PYTHON_PIP) install -e .'[test]'
42 |
43 | install-dev: clean-build ## Install development extra dependencies.
44 | @echo "Installing development requirements..."
45 | @$(PYTHON_PIP) install -e .'[development]' -r requirements.txt
46 |
47 | update-requirements: ## Updates the requirement.txt adding missing package dependencies
48 | @echo "Syncing the package requirements.txt..."
49 | @$(PIP_COMPILE)
50 |
51 | release-to-pypi: clean-build increase-version ## Release project to pypi
52 | @$(PYTHON_PIP) install -U twine wheel
53 | @$(PYTHON) setup.py sdist bdist_wheel
54 | @twine check dist/*
55 | @twine upload dist/*
56 | @git push --tags
57 | @git push
58 |
59 | # ----------------------------------------------------------
60 | # ---------- Upgrade project version (bump2version) --------
61 | # ----------------------------------------------------------
62 | increase-version: clean-build guard-PART ## Bump the project version (using the $PART env: defaults to 'patch').
63 | @echo "Increasing project '$(PART)' version..."
64 | @$(PYTHON_PIP) install -q -e .'[deploy]'
65 | @bump2version --verbose $(PART)
66 |
67 | # ----------------------------------------------------------
68 | # --------- Run project Test -------------------------------
69 | # ----------------------------------------------------------
70 | test:
71 | @$(MANAGE_PY) test
72 |
73 | tox: install-test ## Run tox test
74 | @tox
75 |
76 | clean-test-all: clean-build ## Clean build and test assets.
77 | @rm -rf .tox/
78 | @rm -rf .pytest_cache/
79 | @rm test.db
80 |
81 | # -----------------------------------------------------------
82 | # --------- Docs ---------------------------------------
83 | # -----------------------------------------------------------
84 | create-docs:
85 | @npx docsify init ./docs
86 |
87 | serve-docs:
88 | @npx docsify serve ./docs
89 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # django-extra-field-validation
2 |
3 |    [](https://pepy.tech/project/django-extra-field-validation)
4 |
5 | [](https://github.com/tj-django/django-extra-field-validation/actions/workflows/test.yml)
6 | [](https://www.codacy.com/gh/tj-django/django-extra-field-validation/dashboard?utm_source=github.com\&utm_medium=referral\&utm_content=tj-django/django-extra-field-validation\&utm_campaign=Badge_Grade) [](https://www.codacy.com/gh/tj-django/django-extra-field-validation/dashboard?utm_source=github.com\&utm_medium=referral\&utm_content=tj-django/django-extra-field-validation\&utm_campaign=Badge_Coverage)
7 | [](https://lgtm.com/projects/g/tj-django/django-extra-field-validation/alerts/) [](https://lgtm.com/projects/g/tj-django/django-extra-field-validation/context:python)
8 |
9 | ## Table of Contents
10 |
11 | * [Background](#background)
12 | * [Installation](#installation)
13 | * [Usage](#usage)
14 | * [Require all fields](#require-all-fields)
15 | * [Require at least one field in a collection](#require-at-least-one-field-in-a-collection)
16 | * [Optionally require at least one field in a collection](#optionally-require-at-least-one-field-in-a-collection)
17 | * [Conditionally require all fields](#conditionally-require-all-fields)
18 | * [Conditionally require at least one field in a collection](#conditionally-require-at-least-one-field-in-a-collection)
19 | * [Model Attributes](#model-attributes)
20 | * [License](#license)
21 | * [TODO's](#todos)
22 |
23 | ## Background
24 |
25 | This package aims to provide tools needed to define custom field validation logic which can be used independently or with
26 | django forms, test cases, API implementation or any model operation that requires saving data to the database.
27 |
28 | This can also be extended by defining check constraints if needed but currently validation
29 | will only be handled at the model level.
30 |
31 | ## Installation
32 |
33 | ```shell script
34 | pip install django-extra-field-validation
35 | ```
36 |
37 | ## Usage
38 |
39 | ### Require all fields
40 |
41 | ```py
42 |
43 | from django.db import models
44 | from extra_validator import FieldValidationMixin
45 |
46 |
47 | class TestModel(FieldValidationMixin, models.Model):
48 | amount = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True)
49 | fixed_price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
50 | percentage = models.DecimalField(max_digits=3, decimal_places=0, null=True, blank=True)
51 |
52 | REQUIRED_FIELDS = ['amount'] # Always requires an amount to create the instance.
53 | ```
54 |
55 | Example
56 |
57 | ```python
58 | In [1]: from decimal import Decimal
59 |
60 | In [2]: from demo.models import TestModel
61 |
62 | In [3]: TestModel.objects.create(fixed_price=Decimal('3.00'))
63 | ---------------------------------------------------------------------------
64 | ValueError Traceback (most recent call last)
65 | ...
66 |
67 | ValueError: {'amount': ValidationError([u'Please provide a value for: "amount".'])}
68 |
69 | ```
70 |
71 | ### Require at least one field in a collection
72 |
73 | ```py
74 |
75 | from django.db import models
76 | from extra_validator import FieldValidationMixin
77 |
78 |
79 | class TestModel(FieldValidationMixin, models.Model):
80 | amount = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True)
81 | fixed_price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
82 | percentage = models.DecimalField(max_digits=3, decimal_places=0, null=True, blank=True)
83 |
84 | REQUIRED_TOGGLE_FIELDS = [
85 | ['amount', 'fixed_price', 'percentage'], # Require only one of the following fields.
86 | ]
87 |
88 | ```
89 |
90 | Example
91 |
92 | ```python
93 | In [1]: from decimal import Decimal
94 |
95 | In [2]: from demo.models import TestModel
96 |
97 | In [3]: TestModel.objects.create(amount=Decimal('2.50'), fixed_price=Decimal('3.00'))
98 | ---------------------------------------------------------------------------
99 | ValueError Traceback (most recent call last)
100 | ...
101 |
102 | ValueError: {'fixed_price': ValidationError([u'Please provide only one of: Amount, Fixed price, Percentage'])}
103 |
104 | ```
105 |
106 | ### Optionally require at least one field in a collection
107 |
108 | ```py
109 |
110 | from django.db import models
111 | from extra_validator import FieldValidationMixin
112 |
113 |
114 | class TestModel(FieldValidationMixin, models.Model):
115 | amount = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True)
116 | fixed_price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
117 | percentage = models.DecimalField(max_digits=3, decimal_places=0, null=True, blank=True)
118 |
119 | OPTIONAL_TOGGLE_FIELDS = [
120 | ['fixed_price', 'percentage'] # Optionally validates that only fixed price/percentage are provided when present.
121 | ]
122 |
123 | ```
124 |
125 | Example
126 |
127 | ```python
128 | In [1]: from decimal import Decimal
129 |
130 | In [2]: from demo.models import TestModel
131 |
132 | In [3]: first_obj = TestModel.objects.create(amount=Decimal('2.0'))
133 |
134 | In [4]: second_obj = TestModel.objects.create(amount=Decimal('2.0'), fixed_price=Decimal('3.00'))
135 |
136 | In [5]: third_obj = TestModel.objects.create(amount=Decimal('2.0'), fixed_price=Decimal('3.00'), percentage=Decimal('10.0'))
137 | ---------------------------------------------------------------------------
138 | ValueError Traceback (most recent call last)
139 | ...
140 |
141 | ValueError: {'percentage': ValidationError([u'Please provide only one of: Fixed price, Percentage'])}
142 |
143 | ```
144 |
145 | ### Conditionally require all fields
146 |
147 | ```py
148 |
149 | from django.db import models
150 | from django.conf import settings
151 | from extra_validator import FieldValidationMixin
152 |
153 |
154 | class TestModel(FieldValidationMixin, models.Model):
155 | user = models.ForeignKey(settings.AUTH_USER_MODEL)
156 |
157 | amount = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True)
158 | fixed_price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
159 | percentage = models.DecimalField(max_digits=3, decimal_places=0, null=True, blank=True)
160 |
161 | CONDITIONAL_REQUIRED_FIELDS = [
162 | (
163 | lambda instance: instance.user.is_active, ['amount', 'percentage'],
164 | ),
165 | ]
166 |
167 | ```
168 |
169 | Example
170 |
171 | ```python
172 | In [1]: from decimal import Decimal
173 |
174 | in [2]: from django.contrib.auth import get_user_model
175 |
176 | In [3]: from demo.models import TestModel
177 |
178 | In [4]: user = get_user_model().objects.create(username='test', is_active=True)
179 |
180 | In [5]: first_obj = TestModel.objects.create(user=user, amount=Decimal('2.0'))
181 | ---------------------------------------------------------------------------
182 | ValueError Traceback (most recent call last)
183 | ...
184 |
185 | ValueError: {u'percentage': ValidationError([u'Please provide a value for: "percentage"'])}
186 |
187 | ```
188 |
189 | ### Conditionally require at least one field in a collection
190 |
191 | ```py
192 |
193 | from django.db import models
194 | from django.conf import settings
195 | from extra_validator import FieldValidationMixin
196 |
197 |
198 | class TestModel(FieldValidationMixin, models.Model):
199 | user = models.ForeignKey(settings.AUTH_USER_MODEL)
200 |
201 | amount = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True)
202 | fixed_price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
203 | percentage = models.DecimalField(max_digits=3, decimal_places=0, null=True, blank=True)
204 |
205 | CONDITIONAL_REQUIRED_TOGGLE_FIELDS = [
206 | (
207 | lambda instance: instance.user.is_active, ['fixed_price', 'percentage', 'amount'],
208 | ),
209 | ]
210 | ```
211 |
212 | Example
213 |
214 | ```python
215 | In [1]: from decimal import Decimal
216 |
217 | in [2]: from django.contrib.auth import get_user_model
218 |
219 | In [3]: from demo.models import TestModel
220 |
221 | In [4]: user = get_user_model().objects.create(username='test', is_active=True)
222 |
223 | In [5]: first_obj = TestModel.objects.create(user=user)
224 | ---------------------------------------------------------------------------
225 | ValueError Traceback (most recent call last)
226 | ...
227 |
228 | ValueError: {'__all__': ValidationError([u'Please provide a valid value for any of the following fields: Fixed price, Percentage, Amount'])}
229 |
230 | In [6]: second_obj = TestModel.objects.create(user=user, amount=Decimal('2'), fixed_price=Decimal('2'))
231 | ---------------------------------------------------------------------------
232 | ValueError Traceback (most recent call last)
233 | ...
234 |
235 | ValueError: {'__all__': ValidationError([u'Please provide only one of the following fields: Fixed price, Percentage, Amount'])}
236 | ```
237 |
238 | ## Model Attributes
239 |
240 | This is done using model attributes below.
241 |
242 | ```py
243 | # A list of required fields
244 | REQUIRED_FIELDS = []
245 |
246 | # A list of fields with at most one required.
247 | REQUIRED_TOGGLE_FIELDS = []
248 |
249 | # A list of field with at least one required.
250 | REQUIRED_MIN_FIELDS = []
251 |
252 | # Optional list of fields with at most one required.
253 | OPTIONAL_TOGGLE_FIELDS = []
254 |
255 | # Conditional field required list of tuples the condition a boolean or a callable.
256 | # [(lambda user: user.is_admin, ['first_name', 'last_name'])] : Both 'first_name' or 'last_name'
257 | # If condition is True ensure that all fields are set
258 | CONDITIONAL_REQUIRED_FIELDS = []
259 |
260 | # [(lambda user: user.is_admin, ['first_name', 'last_name'])] : Either 'first_name' or 'last_name'
261 | # If condition is True ensure that at most one field is set
262 | CONDITIONAL_REQUIRED_TOGGLE_FIELDS = []
263 |
264 | # [(lambda user: user.is_admin, ['first_name', 'last_name'])] : At least 'first_name' or 'last_name' provided or both
265 | # If condition is True ensure that at least one field is set
266 | CONDITIONAL_REQUIRED_MIN_FIELDS = []
267 |
268 | # [(lambda user: user.is_admin, ['first_name', 'last_name'])] : Both 'first_name' and 'last_name' isn't provided
269 | # If condition is True ensure none of the fields are provided
270 | CONDITIONAL_REQUIRED_EMPTY_FIELDS = []
271 |
272 | ```
273 |
274 | ## License
275 |
276 | django-extra-field-validation is distributed under the terms of both
277 |
278 | * [MIT License](https://choosealicense.com/licenses/mit)
279 | * [Apache License, Version 2.0](https://choosealicense.com/licenses/apache-2.0)
280 |
281 | at your option.
282 |
283 | ## TODO's
284 |
285 | * \[ ] Support `CONDITIONAL_NON_REQUIRED_TOGGLE_FIELDS`
286 | * \[ ] Support `CONDITIONAL_NON_REQUIRED_FIELDS`
287 | * \[ ] Move to support class and function based validators that use the instance object this should enable cross field model validation.
288 |
--------------------------------------------------------------------------------
/demo/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-extra-field-validation/4ca49fb0dd32ddea68565d892a7db87bf74dffa1/demo/__init__.py
--------------------------------------------------------------------------------
/demo/apps.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.apps import AppConfig
5 |
6 |
7 | class DemoConfig(AppConfig):
8 | name = "demo"
9 |
--------------------------------------------------------------------------------
/demo/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.17 on 2018-12-08 23:41
3 | from __future__ import unicode_literals
4 |
5 | import django.db.models.deletion
6 | from django.conf import settings
7 | from django.db import migrations, models
8 |
9 | import extra_validator.field_validation.validator
10 |
11 |
12 | class Migration(migrations.Migration):
13 |
14 | initial = True
15 |
16 | dependencies = [
17 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
18 | ]
19 |
20 | operations = [
21 | migrations.CreateModel(
22 | name="TestModel",
23 | fields=[
24 | (
25 | "id",
26 | models.AutoField(
27 | auto_created=True,
28 | primary_key=True,
29 | serialize=False,
30 | verbose_name="ID",
31 | ),
32 | ),
33 | (
34 | "amount",
35 | models.DecimalField(
36 | blank=True, decimal_places=2, max_digits=5, null=True
37 | ),
38 | ),
39 | (
40 | "fixed_price",
41 | models.DecimalField(
42 | blank=True, decimal_places=2, max_digits=7, null=True
43 | ),
44 | ),
45 | (
46 | "percentage",
47 | models.DecimalField(
48 | blank=True, decimal_places=0, max_digits=3, null=True
49 | ),
50 | ),
51 | (
52 | "user",
53 | models.ForeignKey(
54 | on_delete=django.db.models.deletion.CASCADE,
55 | to=settings.AUTH_USER_MODEL,
56 | ),
57 | ),
58 | ],
59 | bases=(
60 | extra_validator.field_validation.validator.FieldValidationMixin,
61 | models.Model,
62 | ),
63 | ),
64 | ]
65 |
--------------------------------------------------------------------------------
/demo/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-extra-field-validation/4ca49fb0dd32ddea68565d892a7db87bf74dffa1/demo/migrations/__init__.py
--------------------------------------------------------------------------------
/demo/models.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | import django
5 | from django.contrib.auth import get_user_model
6 | from django.db import models
7 | from six import python_2_unicode_compatible
8 |
9 | from extra_validator import FieldValidationMixin
10 |
11 | if django.VERSION <= (3, 0):
12 | from django.utils.translation import ugettext_noop as _
13 | else:
14 | from django.utils.translation import gettext_noop as _
15 |
16 | UserModel = get_user_model()
17 |
18 |
19 | @python_2_unicode_compatible
20 | class TestModel(FieldValidationMixin, models.Model):
21 | """Ensure that at least one of the following fields are provided."""
22 |
23 | user = models.ForeignKey(UserModel, on_delete=models.CASCADE)
24 | amount = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True)
25 | fixed_price = models.DecimalField(
26 | max_digits=7, decimal_places=2, null=True, blank=True
27 | )
28 | percentage = models.DecimalField(
29 | max_digits=3, decimal_places=0, null=True, blank=True
30 | )
31 |
32 | def __str__(self):
33 | return _(
34 | "{0}: (#{1}, {2}%, ${3})".format(
35 | self.__class__.__name__, self.amount, self.percentage, self.fixed_price
36 | )
37 | )
38 |
--------------------------------------------------------------------------------
/django_extra_field_validation/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-extra-field-validation/4ca49fb0dd32ddea68565d892a7db87bf74dffa1/django_extra_field_validation/__init__.py
--------------------------------------------------------------------------------
/django_extra_field_validation/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for django_extra_field_validation project.
3 |
4 | Generated by 'django-admin startproject' using Django 2.0.2.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/2.0/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/2.0/ref/settings/
11 | """
12 |
13 | import os
14 |
15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
17 |
18 |
19 | # Quick-start development settings - unsuitable for production
20 | # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/
21 |
22 | # SECURITY WARNING: keep the secret key used in production secret!
23 | SECRET_KEY = "8vb(_&-!#z=hk_7k6j5u6qvu_yz2fr_oisvev+yybt@$@_$4bk"
24 |
25 | # SECURITY WARNING: don't run with debug turned on in production!
26 | DEBUG = True
27 |
28 | ALLOWED_HOSTS = []
29 |
30 |
31 | # Application definition
32 |
33 | INSTALLED_APPS = [
34 | "django.contrib.admin",
35 | "django.contrib.auth",
36 | "django.contrib.contenttypes",
37 | "django.contrib.sessions",
38 | "django.contrib.messages",
39 | "django.contrib.staticfiles",
40 | "extra_validator",
41 | "demo",
42 | ]
43 |
44 | MIDDLEWARE = [
45 | "django.middleware.security.SecurityMiddleware",
46 | "django.contrib.sessions.middleware.SessionMiddleware",
47 | "django.middleware.common.CommonMiddleware",
48 | "django.middleware.csrf.CsrfViewMiddleware",
49 | "django.contrib.auth.middleware.AuthenticationMiddleware",
50 | "django.contrib.messages.middleware.MessageMiddleware",
51 | "django.middleware.clickjacking.XFrameOptionsMiddleware",
52 | ]
53 |
54 | TEMPLATES = [
55 | {
56 | "BACKEND": "django.template.backends.django.DjangoTemplates",
57 | "DIRS": [],
58 | "APP_DIRS": True,
59 | "OPTIONS": {
60 | "context_processors": [
61 | "django.template.context_processors.debug",
62 | "django.template.context_processors.request",
63 | "django.contrib.auth.context_processors.auth",
64 | "django.contrib.messages.context_processors.messages",
65 | ],
66 | },
67 | },
68 | ]
69 |
70 | WSGI_APPLICATION = "django_extra_field_validation.wsgi.application"
71 |
72 |
73 | # Database
74 | # https://docs.djangoproject.com/en/2.0/ref/settings/#databases
75 |
76 | DATABASES = {
77 | "default": {
78 | "ENGINE": "django.db.backends.sqlite3",
79 | "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
80 | }
81 | }
82 |
83 |
84 | # Password validation
85 | # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators
86 |
87 | AUTH_PASSWORD_VALIDATORS = [
88 | {
89 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
90 | },
91 | {
92 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
93 | },
94 | {
95 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
96 | },
97 | {
98 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
99 | },
100 | ]
101 |
102 |
103 | # Internationalization
104 | # https://docs.djangoproject.com/en/2.0/topics/i18n/
105 |
106 | TIME_ZONE = "UTC"
107 |
108 | USE_I18N = True
109 |
110 | USE_L10N = True
111 |
112 | USE_TZ = True
113 |
114 |
115 | # Static files (CSS, JavaScript, Images)
116 | # https://docs.djangoproject.com/en/2.0/howto/static-files/
117 |
118 | STATIC_URL = "/static/"
119 |
120 | SECURE_HSTS_SECONDS = 3600
121 |
122 | SECURE_HSTS_INCLUDE_SUBDOMAINS = True
123 |
124 | SECURE_CONTENT_TYPE_NOSNIFF = True
125 |
126 | SECURE_SSL_REDIRECT = os.getenv("SECURE_SSL_REDIRECT_ENABLED") != "False"
127 |
128 | SESSION_COOKIE_SECURE = os.getenv("SESSION_COOKIE_SECURE_ENABLED") != "False"
129 |
130 | CSRF_COOKIE_SECURE = os.getenv("CSRF_COOKIE_SECURE_ENABLED") != "False"
131 |
132 | SECURE_HSTS_PRELOAD = True
133 |
--------------------------------------------------------------------------------
/django_extra_field_validation/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for django_extra_field_validation project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault(
15 | "DJANGO_SETTINGS_MODULE", "django_extra_field_validation.settings"
16 | )
17 |
18 | application = get_wsgi_application()
19 |
--------------------------------------------------------------------------------
/docs/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dragon1207/django-extra-field-validation/4ca49fb0dd32ddea68565d892a7db87bf74dffa1/docs/.nojekyll
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # django-extra-field-validation
2 |
3 |    [](https://pepy.tech/project/django-extra-field-validation)
4 |
5 | [](https://github.com/tj-django/django-extra-field-validation/actions/workflows/test.yml)
6 | [](https://www.codacy.com/gh/tj-django/django-extra-field-validation/dashboard?utm_source=github.com\&utm_medium=referral\&utm_content=tj-django/django-extra-field-validation\&utm_campaign=Badge_Grade) [](https://www.codacy.com/gh/tj-django/django-extra-field-validation/dashboard?utm_source=github.com\&utm_medium=referral\&utm_content=tj-django/django-extra-field-validation\&utm_campaign=Badge_Coverage)
7 | [](https://lgtm.com/projects/g/tj-django/django-extra-field-validation/alerts/) [](https://lgtm.com/projects/g/tj-django/django-extra-field-validation/context:python)
8 |
9 | ## Table of Contents
10 |
11 | * [Background](#background)
12 | * [Installation](#installation)
13 | * [Usage](#usage)
14 | * [Require all fields](#require-all-fields)
15 | * [Require at least one field in a collection](#require-at-least-one-field-in-a-collection)
16 | * [Optionally require at least one field in a collection](#optionally-require-at-least-one-field-in-a-collection)
17 | * [Conditionally require all fields](#conditionally-require-all-fields)
18 | * [Conditionally require at least one field in a collection](#conditionally-require-at-least-one-field-in-a-collection)
19 | * [Model Attributes](#model-attributes)
20 | * [License](#license)
21 | * [TODO's](#todos)
22 |
23 | ## Background
24 |
25 | This package aims to provide tools needed to define custom field validation logic which can be used independently or with
26 | django forms, test cases, API implementation or any model operation that requires saving data to the database.
27 |
28 | This can also be extended by defining check constraints if needed but currently validation
29 | will only be handled at the model level.
30 |
31 | ## Installation
32 |
33 | ```shell script
34 | pip install django-extra-field-validation
35 | ```
36 |
37 | ## Usage
38 |
39 | ### Require all fields
40 |
41 | ```py
42 |
43 | from django.db import models
44 | from extra_validator import FieldValidationMixin
45 |
46 |
47 | class TestModel(FieldValidationMixin, models.Model):
48 | amount = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True)
49 | fixed_price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
50 | percentage = models.DecimalField(max_digits=3, decimal_places=0, null=True, blank=True)
51 |
52 | REQUIRED_FIELDS = ['amount'] # Always requires an amount to create the instance.
53 | ```
54 |
55 | Example
56 |
57 | ```python
58 | In [1]: from decimal import Decimal
59 |
60 | In [2]: from demo.models import TestModel
61 |
62 | In [3]: TestModel.objects.create(fixed_price=Decimal('3.00'))
63 | ---------------------------------------------------------------------------
64 | ValueError Traceback (most recent call last)
65 | ...
66 |
67 | ValueError: {'amount': ValidationError([u'Please provide a value for: "amount".'])}
68 |
69 | ```
70 |
71 | ### Require at least one field in a collection
72 |
73 | ```py
74 |
75 | from django.db import models
76 | from extra_validator import FieldValidationMixin
77 |
78 |
79 | class TestModel(FieldValidationMixin, models.Model):
80 | amount = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True)
81 | fixed_price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
82 | percentage = models.DecimalField(max_digits=3, decimal_places=0, null=True, blank=True)
83 |
84 | REQUIRED_TOGGLE_FIELDS = [
85 | ['amount', 'fixed_price', 'percentage'], # Require only one of the following fields.
86 | ]
87 |
88 | ```
89 |
90 | Example
91 |
92 | ```python
93 | In [1]: from decimal import Decimal
94 |
95 | In [2]: from demo.models import TestModel
96 |
97 | In [3]: TestModel.objects.create(amount=Decimal('2.50'), fixed_price=Decimal('3.00'))
98 | ---------------------------------------------------------------------------
99 | ValueError Traceback (most recent call last)
100 | ...
101 |
102 | ValueError: {'fixed_price': ValidationError([u'Please provide only one of: Amount, Fixed price, Percentage'])}
103 |
104 | ```
105 |
106 | ### Optionally require at least one field in a collection
107 |
108 | ```py
109 |
110 | from django.db import models
111 | from extra_validator import FieldValidationMixin
112 |
113 |
114 | class TestModel(FieldValidationMixin, models.Model):
115 | amount = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True)
116 | fixed_price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
117 | percentage = models.DecimalField(max_digits=3, decimal_places=0, null=True, blank=True)
118 |
119 | OPTIONAL_TOGGLE_FIELDS = [
120 | ['fixed_price', 'percentage'] # Optionally validates that only fixed price/percentage are provided when present.
121 | ]
122 |
123 | ```
124 |
125 | Example
126 |
127 | ```python
128 | In [1]: from decimal import Decimal
129 |
130 | In [2]: from demo.models import TestModel
131 |
132 | In [3]: first_obj = TestModel.objects.create(amount=Decimal('2.0'))
133 |
134 | In [4]: second_obj = TestModel.objects.create(amount=Decimal('2.0'), fixed_price=Decimal('3.00'))
135 |
136 | In [5]: third_obj = TestModel.objects.create(amount=Decimal('2.0'), fixed_price=Decimal('3.00'), percentage=Decimal('10.0'))
137 | ---------------------------------------------------------------------------
138 | ValueError Traceback (most recent call last)
139 | ...
140 |
141 | ValueError: {'percentage': ValidationError([u'Please provide only one of: Fixed price, Percentage'])}
142 |
143 | ```
144 |
145 | ### Conditionally require all fields
146 |
147 | ```py
148 |
149 | from django.db import models
150 | from django.conf import settings
151 | from extra_validator import FieldValidationMixin
152 |
153 |
154 | class TestModel(FieldValidationMixin, models.Model):
155 | user = models.ForeignKey(settings.AUTH_USER_MODEL)
156 |
157 | amount = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True)
158 | fixed_price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
159 | percentage = models.DecimalField(max_digits=3, decimal_places=0, null=True, blank=True)
160 |
161 | CONDITIONAL_REQUIRED_FIELDS = [
162 | (
163 | lambda instance: instance.user.is_active, ['amount', 'percentage'],
164 | ),
165 | ]
166 |
167 | ```
168 |
169 | Example
170 |
171 | ```python
172 | In [1]: from decimal import Decimal
173 |
174 | in [2]: from django.contrib.auth import get_user_model
175 |
176 | In [3]: from demo.models import TestModel
177 |
178 | In [4]: user = get_user_model().objects.create(username='test', is_active=True)
179 |
180 | In [5]: first_obj = TestModel.objects.create(user=user, amount=Decimal('2.0'))
181 | ---------------------------------------------------------------------------
182 | ValueError Traceback (most recent call last)
183 | ...
184 |
185 | ValueError: {u'percentage': ValidationError([u'Please provide a value for: "percentage"'])}
186 |
187 | ```
188 |
189 | ### Conditionally require at least one field in a collection
190 |
191 | ```py
192 |
193 | from django.db import models
194 | from django.conf import settings
195 | from extra_validator import FieldValidationMixin
196 |
197 |
198 | class TestModel(FieldValidationMixin, models.Model):
199 | user = models.ForeignKey(settings.AUTH_USER_MODEL)
200 |
201 | amount = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True)
202 | fixed_price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
203 | percentage = models.DecimalField(max_digits=3, decimal_places=0, null=True, blank=True)
204 |
205 | CONDITIONAL_REQUIRED_TOGGLE_FIELDS = [
206 | (
207 | lambda instance: instance.user.is_active, ['fixed_price', 'percentage', 'amount'],
208 | ),
209 | ]
210 | ```
211 |
212 | Example
213 |
214 | ```python
215 | In [1]: from decimal import Decimal
216 |
217 | in [2]: from django.contrib.auth import get_user_model
218 |
219 | In [3]: from demo.models import TestModel
220 |
221 | In [4]: user = get_user_model().objects.create(username='test', is_active=True)
222 |
223 | In [5]: first_obj = TestModel.objects.create(user=user)
224 | ---------------------------------------------------------------------------
225 | ValueError Traceback (most recent call last)
226 | ...
227 |
228 | ValueError: {'__all__': ValidationError([u'Please provide a valid value for any of the following fields: Fixed price, Percentage, Amount'])}
229 |
230 | In [6]: second_obj = TestModel.objects.create(user=user, amount=Decimal('2'), fixed_price=Decimal('2'))
231 | ---------------------------------------------------------------------------
232 | ValueError Traceback (most recent call last)
233 | ...
234 |
235 | ValueError: {'__all__': ValidationError([u'Please provide only one of the following fields: Fixed price, Percentage, Amount'])}
236 | ```
237 |
238 | ## Model Attributes
239 |
240 | This is done using model attributes below.
241 |
242 | ```py
243 | # A list of required fields
244 | REQUIRED_FIELDS = []
245 |
246 | # A list of fields with at most one required.
247 | REQUIRED_TOGGLE_FIELDS = []
248 |
249 | # A list of field with at least one required.
250 | REQUIRED_MIN_FIELDS = []
251 |
252 | # Optional list of fields with at most one required.
253 | OPTIONAL_TOGGLE_FIELDS = []
254 |
255 | # Conditional field required list of tuples the condition a boolean or a callable.
256 | # [(lambda user: user.is_admin, ['first_name', 'last_name'])] : Both 'first_name' or 'last_name'
257 | # If condition is True ensure that all fields are set
258 | CONDITIONAL_REQUIRED_FIELDS = []
259 |
260 | # [(lambda user: user.is_admin, ['first_name', 'last_name'])] : Either 'first_name' or 'last_name'
261 | # If condition is True ensure that at most one field is set
262 | CONDITIONAL_REQUIRED_TOGGLE_FIELDS = []
263 |
264 | # [(lambda user: user.is_admin, ['first_name', 'last_name'])] : At least 'first_name' or 'last_name' provided or both
265 | # If condition is True ensure that at least one field is set
266 | CONDITIONAL_REQUIRED_MIN_FIELDS = []
267 |
268 | # [(lambda user: user.is_admin, ['first_name', 'last_name'])] : Both 'first_name' and 'last_name' isn't provided
269 | # If condition is True ensure none of the fields are provided
270 | CONDITIONAL_REQUIRED_EMPTY_FIELDS = []
271 |
272 | ```
273 |
274 | ## License
275 |
276 | django-extra-field-validation is distributed under the terms of both
277 |
278 | * [MIT License](https://choosealicense.com/licenses/mit)
279 | * [Apache License, Version 2.0](https://choosealicense.com/licenses/apache-2.0)
280 |
281 | at your option.
282 |
283 | ## TODO's
284 |
285 | * \[ ] Support `CONDITIONAL_NON_REQUIRED_TOGGLE_FIELDS`
286 | * \[ ] Support `CONDITIONAL_NON_REQUIRED_FIELDS`
287 | * \[ ] Move to support class and function based validators that use the instance object this should enable cross field model validation.
288 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Document
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Please wait...
14 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/extra_validator/__init__.py:
--------------------------------------------------------------------------------
1 | """Top-level package for django-extra-field-validation."""
2 |
3 | __author__ = """Tonye Jack"""
4 | __email__ = "jtonye@ymail.com"
5 | __version__ = "1.1.1"
6 |
7 | from .field_validation import FieldValidationMixin
8 |
9 | __all__ = ["FieldValidationMixin"]
10 |
--------------------------------------------------------------------------------
/extra_validator/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class DynamicValidatorConfig(AppConfig):
5 | name = "extra_validator"
6 |
--------------------------------------------------------------------------------
/extra_validator/field_validation/__init__.py:
--------------------------------------------------------------------------------
1 | from .validator import FieldValidationMixin # noqa
2 |
--------------------------------------------------------------------------------
/extra_validator/field_validation/validator.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 |
3 | import django
4 | from django.core import validators
5 | from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
6 | from django.db import models
7 |
8 | if django.VERSION <= (3, 0):
9 | from django.utils.translation import ugettext as _
10 | else:
11 | from django.utils.translation import gettext as _
12 |
13 |
14 | def field_to_str(fields):
15 | return _(
16 | ", ".join(
17 | map(lambda fname: fname.replace("_", " ").strip().capitalize(), fields)
18 | ),
19 | )
20 |
21 |
22 | class FieldValidationMixin(object):
23 | # A list of required fields
24 | REQUIRED_FIELDS = []
25 | # A list of fields with at most one required.
26 | REQUIRED_TOGGLE_FIELDS = []
27 | # A list of field with at least one required.
28 | REQUIRED_MIN_FIELDS = []
29 | # Optional list of fields with at most one required.
30 | OPTIONAL_TOGGLE_FIELDS = []
31 |
32 | # Conditional field required list of tuples the condition a boolean or a callable.
33 | # (lambda o: o.offer_type != Offer.OfferType.OTHER.value, ['offer_detail_id', 'content_type'])
34 | # If condition is True ensure that all fields are set
35 | CONDITIONAL_REQUIRED_FIELDS = []
36 | # If condition is True ensure that at most one field is set
37 | CONDITIONAL_REQUIRED_TOGGLE_FIELDS = []
38 | # If condition is True ensure that at least one field is set
39 | CONDITIONAL_REQUIRED_MIN_FIELDS = []
40 | # If condition is True ensure none of the fields are provided
41 | CONDITIONAL_REQUIRED_EMPTY_FIELDS = []
42 |
43 | ERROR_HANDLERS = {"form": ValidationError}
44 |
45 | EMPTY_VALUES = list(validators.EMPTY_VALUES)
46 |
47 | @staticmethod
48 | def _error_as_dict(field, msg, code="required", error_class=ValidationError):
49 | return {field: error_class(_(msg), code=code)}
50 |
51 | def _validate_only_one_option(self, selections, found_keys):
52 | error_dict = {}
53 | for section in selections:
54 | provided_fields = []
55 | for key in found_keys:
56 | if key in section:
57 | provided_fields.append(key)
58 | if len(set(provided_fields)) > 1:
59 | msg = "Please provide only one of: {fields}".format(
60 | fields=field_to_str(section)
61 | )
62 | error_dict.update(
63 | self._error_as_dict(provided_fields[-1], msg, code="invalid")
64 | )
65 | break
66 | return error_dict
67 |
68 | def _clean_conditional_toggle_fields(self, exclude=None, context=None):
69 | if self.CONDITIONAL_REQUIRED_TOGGLE_FIELDS:
70 | return self._clean_conditional_fields(
71 | exclude,
72 | context,
73 | field_sets=self.CONDITIONAL_REQUIRED_TOGGLE_FIELDS,
74 | validate_one=True,
75 | )
76 |
77 | def _clean_conditional_min_fields(self, exclude=None, context=None):
78 | if self.CONDITIONAL_REQUIRED_MIN_FIELDS:
79 | return self._clean_conditional_fields(
80 | exclude,
81 | context,
82 | field_sets=self.CONDITIONAL_REQUIRED_MIN_FIELDS,
83 | at_least_one=True,
84 | )
85 |
86 | def _clean_conditional_empty_fields(self, exclude=None, context=None):
87 | if self.CONDITIONAL_REQUIRED_EMPTY_FIELDS:
88 | return self._clean_conditional_fields(
89 | exclude,
90 | context,
91 | field_sets=self.CONDITIONAL_REQUIRED_EMPTY_FIELDS,
92 | none_provided=True,
93 | )
94 |
95 | def _clean_conditional_fields(
96 | self,
97 | exclude=None,
98 | context=None,
99 | field_sets=(),
100 | validate_one=False,
101 | at_least_one=False,
102 | none_provided=False,
103 | ):
104 | error_class = self.ERROR_HANDLERS.get(context, ValueError)
105 | exclude = exclude or []
106 | errors = {}
107 | field_sets = field_sets or self.CONDITIONAL_REQUIRED_FIELDS
108 |
109 | if all([field_sets, isinstance(field_sets, (list, tuple))]):
110 | for condition, fields in field_sets:
111 | field_names = list(filter(lambda f: f not in exclude, fields))
112 | if field_names:
113 | is_valid_condition = (
114 | condition if isinstance(condition, bool) else False
115 | )
116 | if callable(condition):
117 | is_valid_condition = condition(self)
118 |
119 | field_value_map = {
120 | field_name: getattr(self, field_name)
121 | for field_name in field_names
122 | if getattr(self, field_name) not in self.EMPTY_VALUES
123 | }
124 |
125 | if is_valid_condition:
126 | if not field_value_map and not none_provided:
127 | if len(fields) > 1:
128 | msg = (
129 | "Please provide a valid value for the following fields: "
130 | "{fields}"
131 | if not validate_one
132 | else "Please provide a valid value for any of the following "
133 | "fields: {fields}".format(
134 | fields=field_to_str(fields)
135 | )
136 | )
137 | errors.update(
138 | self._error_as_dict(NON_FIELD_ERRORS, msg)
139 | )
140 | else:
141 | field = fields[0]
142 | msg = 'Please provide a value for: "{field}"'.format(
143 | field=field
144 | )
145 | errors.update(self._error_as_dict(field, msg))
146 | break
147 |
148 | if field_value_map and none_provided:
149 | msg = "Please omit changes to the following fields: {fields}".format(
150 | fields=field_to_str(fields)
151 | )
152 | errors.update(self._error_as_dict(NON_FIELD_ERRORS, msg))
153 | break
154 |
155 | missing_fields = [
156 | field_name
157 | for field_name in fields
158 | if field_name not in field_value_map.keys()
159 | ]
160 |
161 | if not validate_one and not at_least_one and not none_provided:
162 | for missing_field in missing_fields:
163 | msg = 'Please provide a value for: "{missing_field}"'.format(
164 | missing_field=missing_field
165 | )
166 | errors.update(self._error_as_dict(missing_field, msg))
167 |
168 | elif validate_one and len(fields) - 1 != len(
169 | list(missing_fields)
170 | ):
171 | msg = (
172 | "Please provide only one of the following fields: "
173 | "{fields}".format(fields=field_to_str(fields))
174 | )
175 | errors.update(self._error_as_dict(NON_FIELD_ERRORS, msg))
176 |
177 | if errors:
178 | raise error_class(errors)
179 |
180 | def _clean_required_and_optional_fields(self, exclude=None, context=None):
181 | """Provide extra validation for fields that are required and single selection fields."""
182 | exclude = exclude or []
183 | if any(
184 | [
185 | self.REQUIRED_TOGGLE_FIELDS,
186 | self.REQUIRED_MIN_FIELDS,
187 | self.REQUIRED_FIELDS,
188 | self.OPTIONAL_TOGGLE_FIELDS,
189 | ]
190 | ):
191 | error_class = self.ERROR_HANDLERS.get(context, ValueError)
192 | found = []
193 | errors = {}
194 | optional = []
195 |
196 | for f in self._meta.fields:
197 | if f.name not in exclude:
198 | raw_value = getattr(self, f.attname)
199 | if f.name in self.REQUIRED_FIELDS:
200 | if raw_value in f.empty_values and not f.has_default():
201 | msg = 'Please provide a value for: "{field_name}".'.format(
202 | field_name=f.name
203 | )
204 | errors.update(self._error_as_dict(f.name, msg))
205 |
206 | for required_min_field in self.REQUIRED_MIN_FIELDS:
207 | # Multiple selection of at least one required.
208 | if f.name in required_min_field:
209 | if raw_value not in f.empty_values:
210 | found.append({f.name: raw_value})
211 | elif raw_value in f.empty_values and f.has_default():
212 | if (
213 | isinstance(f, models.CharField)
214 | and f.get_default() not in f.empty_values
215 | ):
216 | found.append({f.name: f.get_default()})
217 |
218 | for required_toggle_field in self.REQUIRED_TOGGLE_FIELDS:
219 | # Single selection of at most one required.
220 | if f.name in required_toggle_field:
221 | if raw_value not in f.empty_values:
222 | found.append({f.name: raw_value})
223 | elif raw_value in f.empty_values and f.has_default():
224 | if (
225 | isinstance(f, models.CharField)
226 | and f.get_default() not in f.empty_values
227 | ):
228 | found.append({f.name: f.get_default()})
229 |
230 | for optional_toggle_field in self.OPTIONAL_TOGGLE_FIELDS:
231 | if (
232 | f.name in optional_toggle_field
233 | and raw_value not in f.empty_values
234 | ):
235 | optional.append({f.name: raw_value})
236 |
237 | if self.REQUIRED_MIN_FIELDS:
238 | if not found:
239 | fields_str = "\n, ".join(
240 | [field_to_str(fields) for fields in self.REQUIRED_MIN_FIELDS]
241 | )
242 | fields_str = (
243 | "\n {fields}".format(fields=fields_str)
244 | if len(self.REQUIRED_MIN_FIELDS) > 1
245 | else fields_str
246 | )
247 | msg = "Please provide a valid value for any of the following fields: {fields}".format(
248 | fields=fields_str
249 | )
250 | errors.update(self._error_as_dict(NON_FIELD_ERRORS, msg))
251 |
252 | if self.REQUIRED_TOGGLE_FIELDS:
253 | if not found:
254 | fields_str = "\n, ".join(
255 | [field_to_str(fields) for fields in self.REQUIRED_TOGGLE_FIELDS]
256 | )
257 | fields_str = (
258 | "\n {fields}".format(fields=fields_str)
259 | if len(self.REQUIRED_TOGGLE_FIELDS) > 1
260 | else fields_str
261 | )
262 | msg = "Please provide a valid value for any of the following fields: {fields}".format(
263 | fields=fields_str
264 | )
265 | errors.update(self._error_as_dict(NON_FIELD_ERRORS, msg))
266 | else:
267 | found_keys = [k for item in found for k in item.keys()]
268 | errors.update(
269 | self._validate_only_one_option(
270 | self.REQUIRED_TOGGLE_FIELDS, found_keys
271 | ),
272 | )
273 |
274 | if self.OPTIONAL_TOGGLE_FIELDS:
275 | if optional:
276 | optional_keys = [k for item in optional for k in item.keys()]
277 | errors.update(
278 | self._validate_only_one_option(
279 | self.OPTIONAL_TOGGLE_FIELDS, optional_keys
280 | ),
281 | )
282 |
283 | if errors:
284 | raise error_class(errors)
285 |
286 | def clean_fields(self, exclude=None):
287 | self._clean_conditional_toggle_fields(exclude=exclude, context="form")
288 | self._clean_conditional_min_fields(exclude=exclude, context="form")
289 | self._clean_conditional_empty_fields(exclude=exclude, context="form")
290 | self._clean_conditional_fields(exclude=exclude, context="form")
291 | self._clean_required_and_optional_fields(exclude=exclude, context="form")
292 | return super(FieldValidationMixin, self).clean_fields(exclude=exclude)
293 |
294 | def save(self, *args, **kwargs):
295 | self._clean_conditional_toggle_fields()
296 | self._clean_conditional_min_fields()
297 | self._clean_conditional_empty_fields()
298 | self._clean_conditional_fields()
299 | self._clean_required_and_optional_fields()
300 | return super(FieldValidationMixin, self).save(*args, **kwargs)
301 |
--------------------------------------------------------------------------------
/extra_validator/tests.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth import get_user_model
2 | from django.test import TestCase
3 |
4 | User = get_user_model()
5 |
6 |
7 | class FieldValidationTestCase(TestCase):
8 | @classmethod
9 | def setUpTestData(cls):
10 | cls.super_user = User.objects.create(
11 | username="super-test-user", is_superuser=True
12 | )
13 | cls.user = User.objects.create(username="test-user")
14 |
15 | def test_conditional_required_field_raises_exception_when_missing(self):
16 | from demo.models import TestModel
17 |
18 | TestModel.CONDITIONAL_REQUIRED_FIELDS = [
19 | (
20 | lambda instance: instance.user.is_active,
21 | ["percentage"],
22 | ),
23 | ]
24 |
25 | with self.assertRaises(ValueError):
26 | TestModel.objects.create(user=self.user)
27 |
28 | def test_conditional_required_field_is_valid(self):
29 | from demo.models import TestModel
30 |
31 | TestModel.CONDITIONAL_REQUIRED_FIELDS = [
32 | (
33 | lambda instance: instance.user.is_active,
34 | ["percentage"],
35 | ),
36 | ]
37 |
38 | TestModel.objects.create(user=self.user, percentage=25)
39 |
40 | def test_conditional_required_toggle_field_raises_exception_when_missing(self):
41 | from demo.models import TestModel
42 |
43 | TestModel.CONDITIONAL_REQUIRED_TOGGLE_FIELDS = [
44 | (
45 | lambda instance: instance.user.is_active,
46 | ["fixed_price", "percentage", "amount"],
47 | ),
48 | ]
49 |
50 | with self.assertRaises(ValueError):
51 | TestModel.objects.create(user=self.user)
52 |
53 | def test_conditional_required_toggle_field_raises_exception_with_2_fields(
54 | self,
55 | ):
56 | from demo.models import TestModel
57 |
58 | TestModel.CONDITIONAL_REQUIRED_TOGGLE_FIELDS = [
59 | (
60 | lambda instance: instance.user.is_active,
61 | ["fixed_price", "percentage", "amount"],
62 | ),
63 | ]
64 |
65 | with self.assertRaises(ValueError):
66 | TestModel.objects.create(user=self.user, percentage=25, fixed_price=10)
67 |
68 | def test_conditional_required_toggle_field_is_valid(self):
69 | from demo.models import TestModel
70 |
71 | TestModel.CONDITIONAL_REQUIRED_TOGGLE_FIELDS = [
72 | (
73 | lambda instance: instance.user.is_active,
74 | ["fixed_price", "percentage", "amount"],
75 | ),
76 | ]
77 |
78 | TestModel.objects.create(user=self.user, percentage=25)
79 |
80 | def test_required_fields_raises_exception(self):
81 | from demo.models import TestModel
82 |
83 | TestModel.REQUIRED_FIELDS = ["percentage"]
84 |
85 | with self.assertRaises(ValueError):
86 | TestModel.objects.create(user=self.user)
87 |
88 | def test_providing_a_required_field_saves_the_instance(self):
89 | from demo.models import TestModel
90 |
91 | TestModel.REQUIRED_FIELDS = ["percentage"]
92 |
93 | obj = TestModel.objects.create(user=self.user, percentage=25)
94 |
95 | self.assertEqual(obj.percentage, 25)
96 |
97 | def test_providing_more_than_one_required_field_raises_an_error(self):
98 | from demo.models import TestModel
99 |
100 | TestModel.REQUIRED_FIELDS = ["percentage", "fixed_price"]
101 |
102 | with self.assertRaises(ValueError):
103 | TestModel.objects.create(user=self.user, percentage=25, fixed_price=10)
104 |
105 | def test_optional_required_fields_is_valid(self):
106 | from demo.models import TestModel
107 |
108 | TestModel.REQUIRED_TOGGLE_FIELDS = ["fixed_price", "percentage", "amount"]
109 |
110 | TestModel.objects.create(user=self.user, percentage=25)
111 |
112 | def test_optional_required_fields_raised_exception_when_invalid(self):
113 | from demo.models import TestModel
114 |
115 | TestModel.REQUIRED_TOGGLE_FIELDS = ["fixed_price", "percentage", "amount"]
116 |
117 | with self.assertRaises(ValueError):
118 | TestModel.objects.create(user=self.user)
119 | TestModel.objects.create(user=self.user, percentage=25, amount=25)
120 | TestModel.objects.create(user=self.user, fixed_price=25, amount=25)
121 |
122 | def test_optional_toggle_fields_is_valid(self):
123 | from demo.models import TestModel
124 |
125 | TestModel.OPTIONAL_TOGGLE_FIELDS = ["fixed_price", "percentage", "amount"]
126 |
127 | TestModel.objects.create(user=self.user, percentage=25)
128 |
129 | def test_optional_toggle_fields_raised_exception_when_invalid(self):
130 | from demo.models import TestModel
131 |
132 | TestModel.OPTIONAL_TOGGLE_FIELDS = ["fixed_price", "percentage", "amount"]
133 |
134 | with self.assertRaises(ValueError):
135 | TestModel.objects.create(user=self.user, percentage=25, amount=25)
136 | TestModel.objects.create(user=self.user, fixed_price=25, amount=25)
137 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | from future.utils import raise_from
6 |
7 | if __name__ == "__main__":
8 | os.environ.setdefault(
9 | "DJANGO_SETTINGS_MODULE", "django_extra_field_validation.settings"
10 | )
11 | try:
12 | from django.core.management import execute_from_command_line
13 | except ImportError as exc:
14 | raise_from(
15 | ImportError(
16 | "Couldn't import Django. Are you sure it's installed and "
17 | "available on your PYTHONPATH environment variable? Did you "
18 | "forget to activate a virtual environment?"
19 | ),
20 | exc,
21 | )
22 | execute_from_command_line(sys.argv)
23 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [metadata]
2 | name = 'django-extra-field-validation'
3 | version = '0.0.1'
4 | description = ''
5 | author = 'Tonye Jack'
6 | author_email = 'jtonye@ymail.com'
7 | license = 'MIT/Apache-2.0'
8 | url = 'https://github.com/tj-django/django-extra-field-validation.git'
9 |
10 | [requires]
11 | python_version = ['2.7', '3.5', '3.6', 'pypy', 'pypy3']
12 |
13 | [build-system]
14 | requires = ['setuptools', 'wheel']
15 |
16 | [tool.hatch.commands]
17 | prerelease = 'hatch build'
18 |
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | DJANGO_SETTINGS_MODULE = django_extra_field_validation.settings
3 | python_files = tests.py test_*.py *_tests.py
4 | testpaths = extra_validator demo
5 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "config:base"
4 | ],
5 | "enabled": true,
6 | "prHourlyLimit": 10,
7 | "prConcurrentLimit": 5,
8 | "rebaseWhen": "behind-base-branch",
9 | "addLabels": [
10 | "dependencies"
11 | ],
12 | "assignees": [
13 | "jackton1"
14 | ],
15 | "assignAutomerge": true,
16 | "dependencyDashboard": true,
17 | "dependencyDashboardAutoclose": true,
18 | "lockFileMaintenance": {
19 | "enabled": true,
20 | "automerge": true
21 | },
22 | "packageRules": [
23 | {
24 | "matchUpdateTypes": ["major", "minor", "patch", "pin", "digest"],
25 | "automerge": true,
26 | "rebaseWhen": "behind-base-branch",
27 | "addLabels": [
28 | "automerge"
29 | ]
30 | },
31 | {
32 | "description": "docker images",
33 | "matchLanguages": [
34 | "docker"
35 | ],
36 | "matchUpdateTypes": ["major", "minor", "patch", "pin", "digest"],
37 | "rebaseWhen": "behind-base-branch",
38 | "addLabels": [
39 | "automerge"
40 | ],
41 | "automerge": true
42 | }
43 | ]
44 | }
45 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | #
2 | # This file is autogenerated by pip-compile
3 | # To update, run:
4 | #
5 | # pip-compile
6 | #
7 | future==0.18.2 # via django-extra-field-validation (setup.py)
8 | six==1.16.0 # via django-extra-field-validation (setup.py)
9 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import io
2 | import os
3 |
4 | from setuptools import find_packages, setup
5 |
6 | install_requires = ["future", "six"]
7 |
8 | test_requires = [
9 | "tox",
10 | "pytest-django",
11 | "pluggy>=0.7",
12 | "mock",
13 | "codacy-coverage",
14 | ]
15 |
16 | deploy_requires = [
17 | "readme_renderer[md]",
18 | "bump2version==1.0.1",
19 | ]
20 |
21 | lint_requires = [
22 | "flake8",
23 | "yamllint",
24 | "isort",
25 | ]
26 |
27 | local_dev_requires = [
28 | "Django>=1.11.18",
29 | "pip-tools",
30 | "check-manifest",
31 | ]
32 |
33 | extras_require = {
34 | "development": [
35 | local_dev_requires,
36 | install_requires,
37 | test_requires,
38 | lint_requires,
39 | ],
40 | "test": test_requires,
41 | "lint": lint_requires,
42 | "deploy": deploy_requires,
43 | "tox": local_dev_requires,
44 | }
45 |
46 | BASE_DIR = os.path.dirname(__file__)
47 | README_PATH = os.path.join(BASE_DIR, "README.md")
48 |
49 | if os.path.isfile(README_PATH):
50 | with io.open(README_PATH, encoding="utf-8") as f:
51 | LONG_DESCRIPTION = f.read()
52 | else:
53 | LONG_DESCRIPTION = ""
54 |
55 |
56 | setup(
57 | name="django-extra-field-validation",
58 | version="1.1.1",
59 | description="Extra django field validation.",
60 | python_requires=">=2.6",
61 | long_description=LONG_DESCRIPTION,
62 | long_description_content_type="text/markdown",
63 | author="Tonye Jack",
64 | author_email="jtonye@ymail.com",
65 | maintainer="Tonye Jack",
66 | maintainer_email="jtonye@ymail.com",
67 | url="https://github.com/tj-django/django-extra-field-validation.git",
68 | license="MIT/Apache-2.0",
69 | keywords=[
70 | "django",
71 | "model validation",
72 | "django models",
73 | "django object validation",
74 | "field validation",
75 | "conditional validation",
76 | "cross field validation",
77 | "django validation",
78 | "django validators",
79 | "django custom validation",
80 | ],
81 | classifiers=[
82 | "Development Status :: 5 - Production/Stable",
83 | "Intended Audience :: Developers",
84 | "License :: OSI Approved :: MIT License",
85 | "License :: OSI Approved :: Apache Software License",
86 | "Natural Language :: English",
87 | "Topic :: Internet :: WWW/HTTP",
88 | "Operating System :: OS Independent",
89 | "Programming Language :: Python :: 2.7",
90 | "Programming Language :: Python :: 3.5",
91 | "Programming Language :: Python :: 3.6",
92 | "Programming Language :: Python :: 3.7",
93 | "Programming Language :: Python :: 3.8",
94 | "Programming Language :: Python :: 3.9",
95 | "Programming Language :: Python :: Implementation :: CPython",
96 | "Programming Language :: Python :: Implementation :: PyPy",
97 | "Framework :: Django :: 1.11",
98 | "Framework :: Django :: 2.0",
99 | "Framework :: Django :: 2.1",
100 | "Framework :: Django :: 2.2",
101 | "Framework :: Django :: 3.0",
102 | "Framework :: Django :: 3.1",
103 | ],
104 | install_requires=install_requires,
105 | tests_require=["coverage", "pytest"],
106 | extras_require=extras_require,
107 | packages=find_packages(
108 | exclude=["*.tests", "*.tests.*", "tests.*", "tests*", "demo", "manage.*"]
109 | ),
110 | )
111 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | minversion = 3.1.2
3 | skip_missing_interpreters = true
4 |
5 | envlist =
6 | py27-{1.11},
7 | py35-{1.11,2.0,2.1,2.2},
8 | py36-{2.0,2.1,2.2,3.0,3.1},
9 | py37-{2.0,2.1,2.2,3.0,3.1},
10 | py38-{2.0,2.1,2.2,3.0,3.1},
11 | py39-{2.0,2.1,2.2,3.0,3.1},
12 | pypy-{1.11},
13 | pypy3-{1.11,2.0,2.1,2.2,3.0,3.1}
14 |
15 | [gh-actions]
16 | python =
17 | 2.7: py27
18 | 3.5: py35
19 | 3.6: py36
20 | 3.7: py37
21 | 3.8: py38
22 | 3.9: py39
23 | pypy-2.7: pypy
24 | pypy-3.6: pypy3
25 | pypy-3.7: pypy3
26 |
27 |
28 | [testenv]
29 | extras = tox
30 | passenv = CODACY_PROJECT_TOKEN *
31 | deps =
32 | 1.11: Django>=1.11,<2.0
33 | 2.0: Django>=2.0,<2.1
34 | 2.1: Django>=2.1,<2.2
35 | 2.2: Django>=2.2,<2.3
36 | 3.0: Django>=3.0,<3.1
37 | 3.1: Django>=3.1,<3.2
38 | 3.2: Django>=3.2,<3.3
39 | main: https://github.com/django/django/archive/main.tar.gz
40 | coverage
41 | pytest-django
42 | codacy-coverage
43 | usedevelop = False
44 | commands =
45 | python -c "import django; print(django.VERSION)"
46 | coverage run -m pytest
47 | coverage report -m
48 | coverage xml
49 | - python-codacy-coverage -r coverage.xml
50 |
51 |
52 | [flake8]
53 | max-line-length = 120
54 |
--------------------------------------------------------------------------------