├── .coveragerc ├── .coveralls.yml ├── .env ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .pre-commit-hooks.yaml ├── .secrets.baseline ├── CHANGELOG.rst ├── CREDITS.rst ├── LICENSE_GPL2.0.txt ├── LICENSE_LGPL_2.1.txt ├── MANIFEST.in ├── README.rst ├── django_mongoengine_filter ├── __init__.py ├── compat.py ├── fields.py ├── filters.py ├── filterset.py ├── locale │ └── fr │ │ └── LC_MESSAGES │ │ └── django.po ├── views.py └── widgets.py ├── docker-compose.yml ├── docs ├── Makefile ├── changelog.rst ├── conf.py ├── conf.py.distrib ├── documentation.rst ├── filters.rst ├── index.rst ├── index.rst.distrib ├── make.bat ├── recipes.rst ├── usage.rst └── widgets.rst ├── pyproject.toml ├── pytest.ini ├── requirements.txt ├── requirements ├── code_style.in ├── code_style.txt ├── common.in ├── common.txt ├── debug.in ├── debug.txt ├── dev.in ├── dev.txt ├── django_2_2.in ├── django_2_2.txt ├── django_3_0.in ├── django_3_0.txt ├── django_3_1.in ├── django_3_1.txt ├── django_3_2.in ├── django_3_2.txt ├── django_4_0.in ├── django_4_0.txt ├── django_4_1.in ├── django_4_1.txt ├── docs.in ├── docs.txt ├── documentation.in ├── documentation.txt ├── test.in ├── test.txt ├── testing.in └── testing.txt ├── scripts ├── black.sh ├── build_docs.sh ├── clean_up.sh ├── compile_requirements.sh ├── create_dirs.sh ├── install.sh ├── isort.sh ├── make_release.sh ├── migrate.sh ├── rebuild_docs.sh ├── runserver.sh └── uninstall.sh ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── dfm_app │ ├── __init__.py │ ├── constants.py │ ├── documents.py │ ├── filters.py │ ├── templates │ │ └── dfm_app │ │ │ └── person_list.html │ └── views.py ├── factories │ ├── __init__.py │ └── person.py ├── manage.py ├── settings │ ├── __init__.py │ ├── base.py │ ├── dev.py │ ├── helpers.py │ └── test.py ├── test_filters.py └── urls.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | relative_files = True 3 | omit = 4 | tests/* 5 | .tox/* 6 | 7 | [report] 8 | show_missing = True 9 | exclude_lines = 10 | pragma: no cover 11 | # raise NotImplementedError 12 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: github-actions 2 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | GITHUB_TOKEN 2 | COVERALLS_REPO_TOKEN 3 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | # ************************************* 7 | # ************* Pre-commit ************ 8 | # ************************************* 9 | pre-commit: 10 | name: pre-commit ${{ matrix.python-version }} - ${{ matrix.os }} 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | fail-fast: true 14 | max-parallel: 4 15 | matrix: 16 | os: 17 | - ubuntu-22.04 18 | python-version: 19 | - "3.10" 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v4 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Install detect-secrets 27 | run: pip install detect-secrets 28 | - name: Run pre-commit 29 | uses: pre-commit/action@v3.0.0 30 | 31 | # ************************************* 32 | # ************* Main tests ************ 33 | # ************************************* 34 | test: 35 | needs: pre-commit 36 | name: test ${{ matrix.python-version }} - ${{ matrix.os }} 37 | runs-on: ${{ matrix.os }} 38 | strategy: 39 | fail-fast: false 40 | max-parallel: 4 41 | matrix: 42 | os: 43 | - ubuntu-22.04 44 | # - Windows 45 | # - MacOs 46 | mongodb-version: ['5.0'] 47 | python-version: 48 | - "3.11" 49 | - "3.10" 50 | - "3.9" 51 | - "3.8" 52 | - "3.7" 53 | steps: 54 | - uses: actions/checkout@v3 55 | - name: Set up Python ${{ matrix.python-version }} 56 | uses: actions/setup-python@v4 57 | with: 58 | python-version: ${{ matrix.python-version }} 59 | - name: Start MongoDB 60 | uses: supercharge/mongodb-github-action@1.8.0 61 | with: 62 | mongodb-version: ${{ matrix.mongodb-version }} 63 | - name: Install Dependencies 64 | run: | 65 | python -m pip install --upgrade pip 66 | - name: Install tox 67 | run: python -m pip install tox-gh-actions 68 | - name: Run test suite 69 | run: tox -r 70 | env: 71 | PYTEST_ADDOPTS: "-vv --durations=10" 72 | - name: Coveralls 73 | id: coveralls-setup 74 | continue-on-error: true 75 | uses: AndreMiras/coveralls-python-action@develop 76 | with: 77 | parallel: true 78 | flag-name: Run Tests 79 | 80 | # ************************************* 81 | # ************** Coveralls ************ 82 | # ************************************* 83 | coveralls_finish: 84 | name: coveralls_finish 85 | needs: test 86 | runs-on: ubuntu-latest 87 | steps: 88 | - name: Install dependencies 89 | run: | 90 | python -m pip install pyyaml 91 | - name: Coveralls Finished 92 | id: coveralls-finish 93 | continue-on-error: true 94 | # if: steps.coveralls-setup.outcome == 'success' 95 | env: 96 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 97 | GITHUB_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 98 | uses: AndreMiras/coveralls-python-action@develop 99 | with: 100 | parallel-finished: true 101 | debug: True 102 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .hg/ 10 | .hgtags 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | .idea/ 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .pytest_cache/ 47 | builddocs/ 48 | builddocs.zip 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | 57 | # Sphinx documentation 58 | docs/_build/ 59 | 60 | # PyBuilder 61 | target/ 62 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: "^docs/|/migrations/" 2 | default_stages: [ commit, push ] 3 | default_language_version: 4 | python: python3 5 | 6 | repos: 7 | 8 | - repo: local 9 | hooks: 10 | - id: detect-secrets 11 | name: Detect secrets 12 | language: python 13 | entry: detect-secrets-hook 14 | args: ['--baseline', '.secrets.baseline'] 15 | 16 | - repo: https://github.com/pre-commit/pre-commit-hooks 17 | rev: v4.4.0 18 | hooks: 19 | - id: trailing-whitespace 20 | exclude: "data/" 21 | - id: end-of-file-fixer 22 | - id: check-yaml 23 | - id: check-toml 24 | - id: check-added-large-files 25 | - id: debug-statements 26 | - id: check-merge-conflict 27 | 28 | - repo: https://github.com/psf/black 29 | rev: 23.1.0 30 | hooks: 31 | - id: black 32 | name: black 33 | files: . 34 | args: [ "--config", "pyproject.toml" ] 35 | 36 | - repo: https://github.com/pycqa/isort 37 | rev: 5.12.0 38 | hooks: 39 | - id: isort 40 | name: isort 41 | files: . 42 | args: [ "--settings-path", "pyproject.toml", "--profile=black" ] 43 | 44 | - repo: https://github.com/charliermarsh/ruff-pre-commit 45 | rev: v0.0.252 46 | hooks: 47 | - id: ruff 48 | name: lint 49 | files: . 50 | args: [ "--config", "pyproject.toml" ] 51 | 52 | # - repo: https://github.com/asottile/pyupgrade 53 | # rev: v3.2.0 54 | # hooks: 55 | # - id: pyupgrade 56 | # args: [ --py310-plus ] 57 | # 58 | # - repo: https://github.com/adamchainz/django-upgrade 59 | # rev: 1.11.0 60 | # hooks: 61 | # - id: django-upgrade 62 | # args: [ --target-version, "3.2" ] 63 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: detect-secrets 2 | name: Detect secrets 3 | description: Detects high entropy strings that are likely to be passwords. 4 | entry: detect-secrets-hook 5 | language: python 6 | # for backward compatibility 7 | files: .* 8 | -------------------------------------------------------------------------------- /.secrets.baseline: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.4.0", 3 | "plugins_used": [ 4 | { 5 | "name": "ArtifactoryDetector" 6 | }, 7 | { 8 | "name": "AWSKeyDetector" 9 | }, 10 | { 11 | "name": "AzureStorageKeyDetector" 12 | }, 13 | { 14 | "name": "Base64HighEntropyString", 15 | "limit": 4.5 16 | }, 17 | { 18 | "name": "BasicAuthDetector" 19 | }, 20 | { 21 | "name": "CloudantDetector" 22 | }, 23 | { 24 | "name": "DiscordBotTokenDetector" 25 | }, 26 | { 27 | "name": "GitHubTokenDetector" 28 | }, 29 | { 30 | "name": "HexHighEntropyString", 31 | "limit": 3.0 32 | }, 33 | { 34 | "name": "IbmCloudIamDetector" 35 | }, 36 | { 37 | "name": "IbmCosHmacDetector" 38 | }, 39 | { 40 | "name": "JwtTokenDetector" 41 | }, 42 | { 43 | "name": "KeywordDetector", 44 | "keyword_exclude": "" 45 | }, 46 | { 47 | "name": "MailchimpDetector" 48 | }, 49 | { 50 | "name": "NpmDetector" 51 | }, 52 | { 53 | "name": "PrivateKeyDetector" 54 | }, 55 | { 56 | "name": "SendGridDetector" 57 | }, 58 | { 59 | "name": "SlackDetector" 60 | }, 61 | { 62 | "name": "SoftlayerDetector" 63 | }, 64 | { 65 | "name": "SquareOAuthDetector" 66 | }, 67 | { 68 | "name": "StripeDetector" 69 | }, 70 | { 71 | "name": "TwilioKeyDetector" 72 | } 73 | ], 74 | "filters_used": [ 75 | { 76 | "path": "detect_secrets.filters.allowlist.is_line_allowlisted" 77 | }, 78 | { 79 | "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", 80 | "min_level": 2 81 | }, 82 | { 83 | "path": "detect_secrets.filters.heuristic.is_indirect_reference" 84 | }, 85 | { 86 | "path": "detect_secrets.filters.heuristic.is_likely_id_string" 87 | }, 88 | { 89 | "path": "detect_secrets.filters.heuristic.is_lock_file" 90 | }, 91 | { 92 | "path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string" 93 | }, 94 | { 95 | "path": "detect_secrets.filters.heuristic.is_potential_uuid" 96 | }, 97 | { 98 | "path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign" 99 | }, 100 | { 101 | "path": "detect_secrets.filters.heuristic.is_sequential_string" 102 | }, 103 | { 104 | "path": "detect_secrets.filters.heuristic.is_swagger_file" 105 | }, 106 | { 107 | "path": "detect_secrets.filters.heuristic.is_templated_secret" 108 | } 109 | ], 110 | "results": { 111 | "docs/conf.py": [ 112 | { 113 | "type": "Secret Keyword", 114 | "filename": "docs/conf.py", 115 | "hashed_secret": "e8af0e18ff4805f4efd84f58b0fa69e3780f35a4", 116 | "is_verified": true, 117 | "line_number": 44 118 | } 119 | ], 120 | "docs/conf.py.distrib": [ 121 | { 122 | "type": "Secret Keyword", 123 | "filename": "docs/conf.py.distrib", 124 | "hashed_secret": "e8af0e18ff4805f4efd84f58b0fa69e3780f35a4", 125 | "is_verified": true, 126 | "line_number": 44 127 | } 128 | ], 129 | "tests/settings/base.py": [ 130 | { 131 | "type": "Secret Keyword", 132 | "filename": "tests/settings/base.py", 133 | "hashed_secret": "e8af0e18ff4805f4efd84f58b0fa69e3780f35a4", 134 | "is_verified": true, 135 | "line_number": 5 136 | } 137 | ] 138 | }, 139 | "generated_at": "2022-12-24T23:17:29Z" 140 | } 141 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Release history and notes 2 | ========================= 3 | `Sequence based identifiers 4 | `_ 5 | are used for versioning (schema follows below): 6 | 7 | .. code-block:: text 8 | 9 | major.minor[.revision] 10 | 11 | - It's always safe to upgrade within the same minor version (for example, from 12 | 0.3 to 0.3.4). 13 | - Minor version changes might be backwards incompatible. Read the 14 | release notes carefully before upgrading (for example, when upgrading from 15 | 0.3.4 to 0.4). 16 | - All backwards incompatible changes are mentioned in this document. 17 | 18 | 0.4.2 19 | ----- 20 | 2023-05-24 21 | 22 | - Fix `AttributeError: 'BaseFilterSet' object has no attribute 'is_valid'` issue. 23 | 24 | 0.4.1 25 | ----- 26 | 2023-02-23 27 | 28 | - Fix issue with adding ``help_text``. 29 | 30 | 0.4.0 31 | ----- 32 | 2022-12-24 33 | 34 | - Drop support for Python < 3.7. 35 | - Drop support for Django < 2.2. 36 | - Tested against Python 3.9, 3.10 and 3.11. 37 | - Tested against Django 3.1, 3.2, 4.0 and 4.1. 38 | - Apply ``black``, ``isort`` and ``ruff``. 39 | - Fix GitHub CI. 40 | 41 | 0.3.5 42 | ----- 43 | 2020-03-23 44 | 45 | - Tested against Python 3.8. 46 | - Tested against Django 3.0. 47 | 48 | 0.3.4 49 | ----- 50 | 2019-04-04 51 | 52 | - Using lazy queries where possible. 53 | 54 | 0.3.3 55 | ----- 56 | 2019-04-02 57 | 58 | - Tested against Django 2.2. 59 | 60 | 0.3.2 61 | ----- 62 | 2019-04-01 63 | 64 | - Fixes in class-based views. 65 | - Addition to docs. 66 | 67 | 0.3.1 68 | ----- 69 | 2019-03-26 70 | 71 | - More tests. 72 | - Addition to docs. 73 | 74 | 0.3 75 | --- 76 | 2019-03-25 77 | 78 | *Got status beta* 79 | 80 | .. note:: 81 | 82 | Namespace changed from `django_filters_mongoengine` to 83 | `django_mongoengine_filter`. Modify your imports accordingly. 84 | 85 | - Clean up. 86 | - Added docs, manifest, tox. 87 | 88 | 0.2 89 | --- 90 | 2019-03-25 91 | 92 | - Working method filters. 93 | 94 | 0.1 95 | --- 96 | 2019-03-25 97 | 98 | - Initial alpha release. 99 | -------------------------------------------------------------------------------- /CREDITS.rst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barseghyanartur/django-mongoengine-filter/cf9a83e47377b8b4276d19f3d71379d5ea1af77b/CREDITS.rst -------------------------------------------------------------------------------- /LICENSE_GPL2.0.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barseghyanartur/django-mongoengine-filter/cf9a83e47377b8b4276d19f3d71379d5ea1af77b/LICENSE_GPL2.0.txt -------------------------------------------------------------------------------- /LICENSE_LGPL_2.1.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barseghyanartur/django-mongoengine-filter/cf9a83e47377b8b4276d19f3d71379d5ea1af77b/LICENSE_LGPL_2.1.txt -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include CHANGELOG.rst 3 | include CREDITS.rst 4 | include LICENSE_GPL2.0.txt 5 | include LICENSE_LGPL_2.1.txt 6 | recursive-include requirements * 7 | recursive-include tests * 8 | recursive-include docs * 9 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ========================= 2 | django-mongoengine-filter 3 | ========================= 4 | ``django-mongoengine-filter`` is a reusable Django application for allowing 5 | users to filter `mongoengine querysets`_ dynamically. It's very similar to 6 | popular ``django-filter`` library and is designed to be used as a drop-in 7 | replacement (as much as it's possible) strictly tied to ``MongoEngine``. 8 | 9 | Full documentation on `Read the docs`_. 10 | 11 | .. image:: https://img.shields.io/pypi/v/django-mongoengine-filter.svg 12 | :target: https://pypi.python.org/pypi/django-mongoengine-filter 13 | :alt: PyPI Version 14 | 15 | .. image:: https://img.shields.io/pypi/pyversions/django-mongoengine-filter.svg 16 | :target: https://pypi.python.org/pypi/django-mongoengine-filter/ 17 | :alt: Supported Python versions 18 | 19 | .. image:: https://github.com/barseghyanartur/django-mongoengine-filter/workflows/test/badge.svg 20 | :target: https://github.com/barseghyanartur/django-mongoengine-filter/actions 21 | :alt: Build Status 22 | 23 | .. image:: https://readthedocs.org/projects/django-mongoengine-filter/badge/?version=latest 24 | :target: http://django-mongoengine-filter.readthedocs.io/en/latest/?badge=latest 25 | :alt: Documentation Status 26 | 27 | .. image:: https://img.shields.io/badge/license-GPL--2.0--only%20OR%20LGPL--2.1--or--later-blue.svg 28 | :target: https://github.com/barseghyanartur/django-mongoengine-filter/#License 29 | :alt: GPL-2.0-only OR LGPL-2.1-or-later 30 | 31 | .. image:: https://coveralls.io/repos/github/barseghyanartur/django-mongoengine-filter/badge.svg?branch=master 32 | :target: https://coveralls.io/github/barseghyanartur/django-mongoengine-filter?branch=master 33 | :alt: Coverage 34 | 35 | Requirements 36 | ============ 37 | * Python 3.7, 3.8, 3.9, 3.10 or 3.11. 38 | * MongoDB 3.x, 4.x, 5.x. 39 | * Django 2.2, 3.0, 3.1, 3.2, 4.0 or 4.1. 40 | 41 | Installation 42 | ============ 43 | Install using pip: 44 | 45 | .. code-block:: sh 46 | 47 | pip install django-mongoengine-filter 48 | 49 | Or latest development version: 50 | 51 | .. code-block:: sh 52 | 53 | pip install https://github.com/barseghyanartur/django-mongoengine-filter/archive/master.zip 54 | 55 | Usage 56 | ===== 57 | **Sample document** 58 | 59 | .. code-block:: python 60 | 61 | from mongoengine import fields, document 62 | from .constants import PROFILE_TYPES, PROFILE_TYPE_FREE, GENDERS, GENDER_MALE 63 | 64 | class Person(document.Document): 65 | 66 | name = fields.StringField( 67 | required=True, 68 | max_length=255, 69 | default="Robot", 70 | verbose_name="Name" 71 | ) 72 | age = fields.IntField(required=True, verbose_name="Age") 73 | num_fingers = fields.IntField( 74 | required=False, 75 | verbose_name="Number of fingers" 76 | ) 77 | profile_type = fields.StringField( 78 | required=False, 79 | blank=False, 80 | null=False, 81 | choices=PROFILE_TYPES, 82 | default=PROFILE_TYPE_FREE, 83 | ) 84 | gender = fields.StringField( 85 | required=False, 86 | blank=False, 87 | null=False, 88 | choices=GENDERS, 89 | default=GENDER_MALE 90 | ) 91 | 92 | def __str__(self): 93 | return self.name 94 | 95 | **Sample filter** 96 | 97 | .. code-block:: python 98 | 99 | import django_mongoengine_filter 100 | 101 | class PersonFilter(django_mongoengine_filter.FilterSet): 102 | 103 | profile_type = django_mongoengine_filter.StringFilter() 104 | ten_fingers = django_mongoengine_filter.MethodFilter( 105 | action="ten_fingers_filter" 106 | ) 107 | 108 | class Meta: 109 | model = Person 110 | fields = ["profile_type", "ten_fingers"] 111 | 112 | def ten_fingers_filter(self, queryset, name, value): 113 | if value == 'yes': 114 | return queryset.filter(num_fingers=10) 115 | return queryset 116 | 117 | **Sample view** 118 | 119 | With function-based views: 120 | 121 | .. code-block:: python 122 | 123 | def person_list(request): 124 | filter = PersonFilter(request.GET, queryset=Person.objects) 125 | return render(request, "dfm_app/person_list.html", {"object_list": filter.qs}) 126 | 127 | Or class-based views: 128 | 129 | .. code-block:: python 130 | 131 | from django_mongoengine_filter.views import FilterView 132 | 133 | class PersonListView(FilterView): 134 | 135 | filterset_class = PersonFilter 136 | template_name = "dfm_app/person_list.html" 137 | 138 | **Sample template** 139 | 140 | .. code-block:: html 141 | 142 | 147 | 148 | **Sample requests** 149 | 150 | - GET /persons/ 151 | - GET /persons/?profile_type=free&gender=male 152 | - GET /persons/?profile_type=free&gender=female 153 | - GET /persons/?profile_type=member&gender=female 154 | - GET /persons/?ten_fingers=yes 155 | 156 | Development 157 | =========== 158 | Testing 159 | ------- 160 | To run tests in your working environment type: 161 | 162 | .. code-block:: sh 163 | 164 | pytest -vrx 165 | 166 | To test with all supported Python versions type: 167 | 168 | .. code-block:: sh 169 | 170 | tox 171 | 172 | Running MongoDB 173 | --------------- 174 | The easiest way is to run it via Docker: 175 | 176 | .. code-block:: sh 177 | 178 | docker pull mongo:latest 179 | docker run -p 27017:27017 mongo:latest 180 | 181 | Writing documentation 182 | --------------------- 183 | Keep the following hierarchy. 184 | 185 | .. code-block:: text 186 | 187 | ===== 188 | title 189 | ===== 190 | 191 | header 192 | ====== 193 | 194 | sub-header 195 | ---------- 196 | 197 | sub-sub-header 198 | ~~~~~~~~~~~~~~ 199 | 200 | sub-sub-sub-header 201 | ^^^^^^^^^^^^^^^^^^ 202 | 203 | sub-sub-sub-sub-header 204 | ++++++++++++++++++++++ 205 | 206 | sub-sub-sub-sub-sub-header 207 | ************************** 208 | 209 | License 210 | ======= 211 | GPL-2.0-only OR LGPL-2.1-or-later 212 | 213 | Support 214 | ======= 215 | For any security issues contact me at the e-mail given in the `Author`_ section. 216 | 217 | For overall issues, go to `GitHub `_. 218 | 219 | Author 220 | ====== 221 | Artur Barseghyan 222 | 223 | .. _`mongoengine querysets`: http://mongoengine-odm.readthedocs.org/apireference.html#module-mongoengine.queryset 224 | .. _`read the docs`: https://django-mongoengine-filter.readthedocs.org/ 225 | -------------------------------------------------------------------------------- /django_mongoengine_filter/__init__.py: -------------------------------------------------------------------------------- 1 | from .filters import * # noqa 2 | from .filterset import FilterSet 3 | 4 | VERSION = (0, 4, 2) 5 | 6 | __title__ = "django-mongoengine-filter" 7 | __version__ = ".".join([str(_i) for _i in VERSION]) 8 | __author__ = "Artur Barseghyan " 9 | __copyright__ = "2019-2023 Artur Barseghyan" 10 | __license__ = "GPL 2.0/LGPL 2.1" 11 | __all__ = ( # noqa 12 | "AllValuesFilter", 13 | "BooleanFilter", 14 | "ChoiceFilter", 15 | "DateFilter", 16 | "DateRangeFilter", 17 | "DateTimeFilter", 18 | "Filter", 19 | "FilterSet", 20 | "MethodFilter", 21 | "ModelChoiceFilter", 22 | "ModelMultipleChoiceFilter", 23 | "MultipleChoiceFilter", 24 | "NumberFilter", 25 | "RangeFilter", 26 | "StringFilter", 27 | "TimeFilter", 28 | ) 29 | -------------------------------------------------------------------------------- /django_mongoengine_filter/compat.py: -------------------------------------------------------------------------------- 1 | __all__ = ("QUERY_TERMS",) 2 | 3 | # Valid query types (a set is used for speedy lookups). These are (currently) 4 | # considered SQL-specific; other storage systems may choose to use different 5 | # lookup types. 6 | QUERY_TERMS = { 7 | "exact", 8 | "iexact", 9 | "contains", 10 | "icontains", 11 | "gt", 12 | "gte", 13 | "lt", 14 | "lte", 15 | "in", 16 | "startswith", 17 | "istartswith", 18 | "endswith", 19 | "iendswith", 20 | "range", 21 | "year", 22 | "month", 23 | "day", 24 | "week_day", 25 | "hour", 26 | "minute", 27 | "second", 28 | "isnull", 29 | "search", 30 | "regex", 31 | "iregex", 32 | } 33 | -------------------------------------------------------------------------------- /django_mongoengine_filter/fields.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from collections import namedtuple 4 | 5 | from django import forms 6 | 7 | from .widgets import LookupTypeWidget, RangeWidget 8 | 9 | __all__ = ("Lookup", "LookupTypeField", "RangeField") 10 | 11 | 12 | class RangeField(forms.MultiValueField): 13 | widget = RangeWidget 14 | 15 | def __init__(self, *args, **kwargs): 16 | fields = (forms.DecimalField(), forms.DecimalField()) 17 | super(RangeField, self).__init__(fields, *args, **kwargs) 18 | 19 | def compress(self, data_list): 20 | if data_list: 21 | return slice(*data_list) 22 | return None 23 | 24 | 25 | Lookup = namedtuple("Lookup", ("value", "lookup_type")) 26 | 27 | 28 | class LookupTypeField(forms.MultiValueField): 29 | def __init__(self, field, lookup_choices, *args, **kwargs): 30 | fields = (field, forms.ChoiceField(choices=lookup_choices)) 31 | defaults = {"widgets": [f.widget for f in fields]} 32 | widget = LookupTypeWidget(**defaults) 33 | kwargs["widget"] = widget 34 | super(LookupTypeField, self).__init__(fields, *args, **kwargs) 35 | 36 | def compress(self, data_list): 37 | if len(data_list) == 2: 38 | return Lookup( 39 | value=data_list[0], lookup_type=data_list[1] or "exact" 40 | ) 41 | return Lookup(value=None, lookup_type="exact") 42 | -------------------------------------------------------------------------------- /django_mongoengine_filter/filters.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from datetime import timedelta 4 | 5 | import six 6 | from django import forms 7 | from django.utils.timezone import now 8 | from django.utils.translation import gettext_lazy as _ 9 | 10 | from .fields import Lookup, LookupTypeField, RangeField 11 | 12 | try: 13 | from django.db.models.sql.constants import QUERY_TERMS 14 | except ImportError: 15 | from .compat import QUERY_TERMS 16 | 17 | 18 | __all__ = [ 19 | "Filter", 20 | "StringFilter", 21 | "BooleanFilter", 22 | "ChoiceFilter", 23 | "MultipleChoiceFilter", 24 | "DateFilter", 25 | "DateTimeFilter", 26 | "TimeFilter", 27 | "ModelChoiceFilter", 28 | "ModelMultipleChoiceFilter", 29 | "NumberFilter", 30 | "RangeFilter", 31 | "DateRangeFilter", 32 | "AllValuesFilter", 33 | "MethodFilter", 34 | ] 35 | 36 | 37 | LOOKUP_TYPES = sorted(QUERY_TERMS) 38 | 39 | 40 | class Filter(object): 41 | creation_counter = 0 42 | field_class = forms.Field 43 | 44 | def __init__( 45 | self, 46 | name=None, 47 | label=None, 48 | widget=None, 49 | action=None, 50 | lookup_type="exact", 51 | required=False, 52 | distinct=False, 53 | exclude=False, 54 | **kwargs, 55 | ): 56 | self.name = name 57 | self.label = label 58 | if action: 59 | self.filter = action 60 | self.lookup_type = lookup_type 61 | self.widget = widget 62 | self.required = required 63 | self.extra = kwargs 64 | self.distinct = distinct 65 | self.exclude = exclude 66 | 67 | self.creation_counter = Filter.creation_counter 68 | Filter.creation_counter += 1 69 | 70 | @property 71 | def field(self): 72 | if not hasattr(self, "_field"): 73 | help_text = ( 74 | _("This is an exclusion filter") 75 | if self.exclude 76 | else self.extra.pop("help_text", "") 77 | ) 78 | if self.lookup_type is None or isinstance( 79 | self.lookup_type, (list, tuple) 80 | ): 81 | if self.lookup_type is None: 82 | lookup = [(x, x) for x in LOOKUP_TYPES] 83 | else: 84 | lookup = [ 85 | (x, x) for x in LOOKUP_TYPES if x in self.lookup_type 86 | ] 87 | self._field = LookupTypeField( 88 | self.field_class( 89 | required=self.required, widget=self.widget, **self.extra 90 | ), 91 | lookup, 92 | required=self.required, 93 | label=self.label, 94 | help_text=help_text, 95 | ) 96 | else: 97 | self._field = self.field_class( 98 | required=self.required, 99 | label=self.label, 100 | widget=self.widget, 101 | help_text=help_text, 102 | **self.extra, 103 | ) 104 | 105 | return self._field 106 | 107 | def filter(self, qs, value): 108 | if isinstance(value, Lookup): 109 | lookup = six.text_type(value.lookup_type) 110 | value = value.value 111 | else: 112 | lookup = self.lookup_type 113 | if value in ([], (), {}, None, ""): 114 | return qs 115 | lookup_or_exclude = lookup if not self.exclude else "ne" 116 | qs = qs.filter(**{"%s__%s" % (self.name, lookup_or_exclude): value}) 117 | if self.distinct: 118 | qs = qs.distinct() 119 | return qs 120 | 121 | 122 | class StringFilter(Filter): 123 | field_class = forms.CharField 124 | 125 | 126 | class BooleanFilter(Filter): 127 | field_class = forms.NullBooleanField 128 | 129 | def filter(self, qs, value): 130 | if value is not None: 131 | return qs.filter(**{self.name: value}) 132 | return qs 133 | 134 | 135 | class ChoiceFilter(Filter): 136 | field_class = forms.ChoiceField 137 | 138 | 139 | class MultipleChoiceFilter(Filter): 140 | """ 141 | This filter preforms an OR query on the selected options. 142 | """ 143 | 144 | field_class = forms.MultipleChoiceField 145 | 146 | def filter(self, qs, value): 147 | value = value or () 148 | if not value or len(value) == len(self.field.choices): 149 | return qs 150 | return qs.filter(**{"%s__in" % self.name: value}) 151 | 152 | 153 | class DateFilter(Filter): 154 | field_class = forms.DateField 155 | 156 | 157 | class DateTimeFilter(Filter): 158 | field_class = forms.DateTimeField 159 | 160 | 161 | class TimeFilter(Filter): 162 | field_class = forms.TimeField 163 | 164 | 165 | class ModelChoiceFilter(Filter): 166 | field_class = forms.ModelChoiceField 167 | 168 | 169 | class ModelMultipleChoiceFilter(MultipleChoiceFilter): 170 | field_class = forms.ModelMultipleChoiceField 171 | 172 | 173 | class NumberFilter(Filter): 174 | field_class = forms.DecimalField 175 | 176 | 177 | class RangeFilter(Filter): 178 | field_class = RangeField 179 | 180 | def filter(self, qs, value): 181 | if value: 182 | start_lookup = "%s__gte" % self.name 183 | stop_lookup = "%s__lte" % self.name 184 | return qs.filter( 185 | **{start_lookup: value.start, stop_lookup: value.stop} 186 | ) 187 | return qs 188 | 189 | 190 | _truncate = lambda dt: dt.replace(hour=0, minute=0, second=0) # noqa 191 | 192 | 193 | class DateRangeFilter(ChoiceFilter): 194 | options = { 195 | "": (_("Any date"), lambda qs, name: qs.all()), 196 | 1: ( 197 | _("Today"), 198 | lambda qs, name: qs.filter( 199 | **{ 200 | "%s__year" % name: now().year, 201 | "%s__month" % name: now().month, 202 | "%s__day" % name: now().day, 203 | } 204 | ), 205 | ), 206 | 2: ( 207 | _("Past 7 days"), 208 | lambda qs, name: qs.filter( 209 | **{ 210 | "%s__gte" % name: _truncate(now() - timedelta(days=7)), 211 | "%s__lt" % name: _truncate(now() + timedelta(days=1)), 212 | } 213 | ), 214 | ), 215 | 3: ( 216 | _("This month"), 217 | lambda qs, name: qs.filter( 218 | **{ 219 | "%s__year" % name: now().year, 220 | "%s__month" % name: now().month, 221 | } 222 | ), 223 | ), 224 | 4: ( 225 | _("This year"), 226 | lambda qs, name: qs.filter(**{"%s__year" % name: now().year}), 227 | ), 228 | } 229 | 230 | def __init__(self, *args, **kwargs): 231 | kwargs["choices"] = [ 232 | (key, value[0]) for key, value in six.iteritems(self.options) 233 | ] 234 | super(DateRangeFilter, self).__init__(*args, **kwargs) 235 | 236 | def filter(self, qs, value): 237 | try: 238 | value = int(value) 239 | except (ValueError, TypeError): 240 | value = "" 241 | return self.options[value][1](qs, self.name) 242 | 243 | 244 | class AllValuesFilter(ChoiceFilter): 245 | @property 246 | def field(self): 247 | qs = self.model.objects.distinct() 248 | qs = qs.order_by(self.name).values_list(self.name, flat=True) 249 | self.extra["choices"] = [(o, o) for o in qs] 250 | return super(AllValuesFilter, self).field 251 | 252 | 253 | class MethodFilter(Filter): 254 | """ 255 | This filter will allow you to run a method that exists on the 256 | filterset class 257 | """ 258 | 259 | def __init__(self, *args, **kwargs): 260 | # Get the action out of the kwargs 261 | action = kwargs.get("action", None) 262 | 263 | # If the action is a string store the action and set the action to our 264 | # own filter method, so it can be backwards compatible and work as 265 | # expected, the parent will still treat it as a filter that has an 266 | # action 267 | self.parent_action = "" 268 | text_types = (str, six.text_type) 269 | if type(action) in text_types: 270 | self.parent_action = str(action) 271 | kwargs.update({"action": self.filter}) 272 | 273 | # Call the parent 274 | super(MethodFilter, self).__init__(*args, **kwargs) 275 | 276 | def filter(self, qs, value): 277 | """ 278 | This filter method will act as a proxy for the actual method we want 279 | to call. It will try to find the method on the parent filterset, if 280 | not it defaults to just returning the queryset 281 | """ 282 | parent = getattr(self, "parent", None) 283 | parent_filter_method = getattr(parent, self.parent_action, None) 284 | if parent_filter_method is not None: 285 | return parent_filter_method(qs, self.name, value) 286 | return qs 287 | -------------------------------------------------------------------------------- /django_mongoengine_filter/filterset.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from collections import OrderedDict 4 | from copy import deepcopy 5 | 6 | import mongoengine 7 | import six 8 | from django import forms 9 | from django.core.validators import EMPTY_VALUES 10 | from django.db import models 11 | from django.db.models.constants import LOOKUP_SEP 12 | from django.utils.text import capfirst 13 | from django.utils.translation import gettext as _ 14 | from mongoengine.errors import LookUpError 15 | from mongoengine.fields import EmbeddedDocumentField, ListField 16 | 17 | from .filters import ( # DateFilter,; TimeFilter, 18 | BooleanFilter, 19 | ChoiceFilter, 20 | DateTimeFilter, 21 | Filter, 22 | ModelChoiceFilter, 23 | ModelMultipleChoiceFilter, 24 | NumberFilter, 25 | StringFilter, 26 | ) 27 | 28 | ORDER_BY_FIELD = "o" 29 | 30 | __all__ = ( 31 | "BaseFilterSet", 32 | "FILTER_FOR_DBFIELD_DEFAULTS", 33 | "filters_for_model", 34 | "FilterSet", 35 | "filterset_factory", 36 | "FilterSetMetaclass", 37 | "FilterSetOptions", 38 | "get_declared_filters", 39 | "get_model_field", 40 | "ORDER_BY_FIELD", 41 | ) 42 | 43 | 44 | def get_declared_filters(bases, attrs, with_base_filters=True): 45 | filters = [] 46 | for filter_name, obj in list(attrs.items()): 47 | if isinstance(obj, Filter): 48 | obj = attrs.pop(filter_name) 49 | if getattr(obj, "name", None) is None: 50 | obj.name = filter_name 51 | filters.append((filter_name, obj)) 52 | filters.sort(key=lambda x: x[1].creation_counter) 53 | 54 | if with_base_filters: 55 | for base in bases[::-1]: 56 | if hasattr(base, "base_filters"): 57 | filters = list(base.base_filters.items()) + filters 58 | else: 59 | for base in bases[::-1]: 60 | if hasattr(base, "declared_filters"): 61 | filters = list(base.declared_filters.items()) + filters 62 | 63 | return OrderedDict(filters) 64 | 65 | 66 | def get_model_field(model, f): 67 | parts = f.split(LOOKUP_SEP) 68 | member = None 69 | if len(parts) == 1: 70 | try: 71 | return model._lookup_field(f)[0] 72 | except LookUpError: 73 | return None 74 | for part in parts[:-1]: 75 | try: 76 | member = model._lookup_field(part)[0].lookup_member(parts[-1]) 77 | except LookUpError: 78 | return None 79 | if isinstance(member, (EmbeddedDocumentField, ListField)): 80 | model = member 81 | return member 82 | 83 | 84 | def filters_for_model( 85 | model, 86 | fields=None, 87 | exclude=None, 88 | filter_for_field=None, 89 | filter_for_reverse_field=None, 90 | ): 91 | field_dict = OrderedDict() 92 | opts = model._meta 93 | if fields is None: 94 | fields = [ 95 | f.name 96 | for f in sorted(opts.fields + opts.many_to_many) 97 | if not isinstance(f, models.AutoField) 98 | ] 99 | for f in fields: 100 | field = get_model_field(model, f) 101 | if field is None: 102 | field_dict[f] = None 103 | continue 104 | if isinstance(fields, dict): 105 | # Create a filter for each lookup type. 106 | for lookup_type in fields[f]: 107 | filter_ = filter_for_field(field, f, lookup_type) 108 | if filter_: 109 | filter_name = f 110 | # Don't add "exact" to filter names 111 | if lookup_type != "exact": 112 | filter_name = f + LOOKUP_SEP + lookup_type 113 | field_dict[filter_name] = filter_ 114 | else: 115 | filter_ = filter_for_field(field, f) 116 | if filter_: 117 | field_dict[f] = filter_ 118 | return field_dict 119 | 120 | 121 | class FilterSetOptions: 122 | def __init__(self, options=None): 123 | self.model = getattr(options, "model", None) 124 | self.fields = getattr(options, "fields", None) 125 | self.exclude = getattr(options, "exclude", None) 126 | self.order_by = getattr(options, "order_by", False) 127 | self.form = getattr(options, "form", forms.Form) 128 | 129 | 130 | class FilterSetMetaclass(type): 131 | def __new__(cls, name, bases, attrs): 132 | try: 133 | parents = [b for b in bases if issubclass(b, FilterSet)] 134 | except NameError: 135 | # We are defining FilterSet itself here 136 | parents = None 137 | declared_filters = get_declared_filters(bases, attrs, False) 138 | new_class = super(FilterSetMetaclass, cls).__new__( 139 | cls, name, bases, attrs 140 | ) 141 | 142 | if not parents: 143 | return new_class 144 | 145 | opts = new_class._meta = FilterSetOptions( 146 | getattr(new_class, "Meta", None) 147 | ) 148 | if opts.model: 149 | filters = filters_for_model( 150 | opts.model, 151 | opts.fields, 152 | opts.exclude, 153 | new_class.filter_for_field, 154 | new_class.filter_for_reverse_field, 155 | ) 156 | filters.update(declared_filters) 157 | else: 158 | filters = declared_filters 159 | 160 | if None in filters.values(): 161 | raise TypeError( 162 | "Meta.fields contains a field that isn't defined " 163 | "on this FilterSet" 164 | ) 165 | 166 | new_class.declared_filters = declared_filters 167 | new_class.base_filters = filters 168 | return new_class 169 | 170 | 171 | FILTER_FOR_DBFIELD_DEFAULTS = { 172 | mongoengine.UUIDField: {"filter_class": NumberFilter}, 173 | mongoengine.StringField: {"filter_class": StringFilter}, 174 | mongoengine.BooleanField: {"filter_class": BooleanFilter}, 175 | mongoengine.DateTimeField: {"filter_class": DateTimeFilter}, 176 | mongoengine.DecimalField: {"filter_class": NumberFilter}, 177 | mongoengine.IntField: {"filter_class": NumberFilter}, 178 | mongoengine.FloatField: {"filter_class": NumberFilter}, 179 | mongoengine.EmailField: {"filter_class": StringFilter}, 180 | mongoengine.URLField: {"filter_class": StringFilter}, 181 | } 182 | 183 | 184 | class BaseFilterSet: 185 | filter_overrides = {} 186 | order_by_field = ORDER_BY_FIELD 187 | strict = True 188 | 189 | def __init__( 190 | self, 191 | data=None, 192 | queryset=None, 193 | *, 194 | request=None, 195 | prefix=None, 196 | strict=None, 197 | ): 198 | self.is_bound = data is not None 199 | self.data = data or {} 200 | if queryset is None: 201 | queryset = self._meta.model.objects 202 | self.queryset = queryset 203 | self.request = request 204 | self.form_prefix = prefix 205 | if strict is not None: 206 | self.strict = strict 207 | 208 | self.filters = deepcopy(self.base_filters) 209 | # propagate the model being used through the filters 210 | for filter_ in self.filters.values(): 211 | filter_.model = self._meta.model 212 | 213 | # Apply the parent to the filters, this will allow the filters to 214 | # access the filterset 215 | for filter_key, filter_ in six.iteritems(self.filters): 216 | filter_.parent = self 217 | 218 | def __iter__(self): 219 | for obj in self.qs: 220 | yield obj 221 | 222 | def __len__(self): 223 | return len(self.qs) 224 | 225 | def __getitem__(self, key): 226 | return self.qs[key] 227 | 228 | def is_valid(self): 229 | return self.is_bound and self.form.is_valid() 230 | 231 | @property 232 | def qs(self): 233 | if not hasattr(self, "_qs"): 234 | valid = self.is_bound and self.form.is_valid() 235 | 236 | if self.strict and self.is_bound and not valid: 237 | self._qs = self.queryset.none() 238 | return self._qs 239 | 240 | # start with all the results and filter from there 241 | qs = self.queryset.all() 242 | for name, filter_ in six.iteritems(self.filters): 243 | value = None 244 | if valid: 245 | value = self.form.cleaned_data[name] 246 | else: 247 | raw_value = self.form[name].value() 248 | try: 249 | value = self.form.fields[name].clean(raw_value) 250 | except forms.ValidationError: 251 | # for invalid values either: 252 | # strictly "apply" filter yielding no results 253 | # and get out of here 254 | if self.strict: 255 | self._qs = self.queryset.none() 256 | return self._qs 257 | else: # or ignore this filter altogether 258 | pass 259 | 260 | if value is not None: # valid & clean data 261 | qs = filter_.filter(qs, value) 262 | 263 | if self._meta.order_by: 264 | order_field = self.form.fields[self.order_by_field] 265 | data = self.form[self.order_by_field].data 266 | ordered_value = None 267 | try: 268 | ordered_value = order_field.clean(data) 269 | except forms.ValidationError: 270 | pass 271 | 272 | if ordered_value in EMPTY_VALUES and self.strict: 273 | ordered_value = self.form.fields[ 274 | self.order_by_field 275 | ].choices[0][0] 276 | 277 | if ordered_value: 278 | qs = qs.order_by(*self.get_order_by(ordered_value)) 279 | 280 | self._qs = qs 281 | 282 | return self._qs 283 | 284 | def count(self): 285 | return self.qs.count() 286 | 287 | @property 288 | def form(self): 289 | if not hasattr(self, "_form"): 290 | fields = OrderedDict( 291 | [ 292 | (name, filter_.field) 293 | for name, filter_ in six.iteritems(self.filters) 294 | ] 295 | ) 296 | fields[self.order_by_field] = self.ordering_field 297 | Form = type( 298 | str("%sForm" % self.__class__.__name__), 299 | (self._meta.form,), 300 | fields, 301 | ) 302 | if self.is_bound: 303 | self._form = Form(self.data, prefix=self.form_prefix) 304 | else: 305 | self._form = Form(prefix=self.form_prefix) 306 | return self._form 307 | 308 | def get_ordering_field(self): 309 | if self._meta.order_by: 310 | if isinstance(self._meta.order_by, (list, tuple)): 311 | if isinstance(self._meta.order_by[0], (list, tuple)): 312 | # e.g. (('field', 'Display name'), ...) 313 | choices = [(f[0], f[1]) for f in self._meta.order_by] 314 | else: 315 | choices = [ 316 | ( 317 | f, 318 | _("%s (descending)" % capfirst(f[1:])) 319 | if f[0] == "-" 320 | else capfirst(f), 321 | ) 322 | for f in self._meta.order_by 323 | ] 324 | else: 325 | # add asc and desc field names 326 | # use the filter's label if provided 327 | choices = [] 328 | for f, fltr in self.filters.items(): 329 | choices.extend( 330 | [ 331 | (fltr.name or f, fltr.label or capfirst(f)), 332 | ( 333 | "-%s" % (fltr.name or f), 334 | _( 335 | "%s (descending)" 336 | % (fltr.label or capfirst(f)) 337 | ), 338 | ), 339 | ] 340 | ) 341 | return forms.ChoiceField( 342 | label="Ordering", required=False, choices=choices 343 | ) 344 | 345 | @property 346 | def ordering_field(self): 347 | if not hasattr(self, "_ordering_field"): 348 | self._ordering_field = self.get_ordering_field() 349 | return self._ordering_field 350 | 351 | def get_order_by(self, order_choice): 352 | return [order_choice] 353 | 354 | @classmethod 355 | def filter_for_field(cls, f, name, lookup_type="exact"): 356 | filter_for_field = dict(FILTER_FOR_DBFIELD_DEFAULTS) 357 | filter_for_field.update(cls.filter_overrides) 358 | default = { 359 | "name": name, 360 | "label": capfirst( 361 | f.verbose_name if getattr(f, "verbose_name", None) else f.name 362 | ), 363 | "lookup_type": lookup_type, 364 | } 365 | 366 | if f.choices: 367 | default["choices"] = f.choices 368 | return ChoiceFilter(**default) 369 | 370 | data = filter_for_field.get(f.__class__) 371 | if data is None: 372 | # could be a derived field, inspect parents 373 | for class_ in f.__class__.mro(): 374 | # skip if class_ is models.Field or object 375 | # 1st item in mro() is original class 376 | if class_ in (f.__class__, models.Field, object): 377 | continue 378 | data = filter_for_field.get(class_) 379 | if data: 380 | break 381 | if data is None: 382 | return 383 | filter_class = data.get("filter_class") 384 | default.update(data.get("extra", lambda f: {})(f)) 385 | if filter_class is not None: 386 | return filter_class(**default) 387 | 388 | @classmethod 389 | def filter_for_reverse_field(cls, f, name): 390 | rel = f.field.rel 391 | queryset = f.model.objects 392 | default = { 393 | "name": name, 394 | "label": capfirst(rel.related_name), 395 | "queryset": queryset, 396 | } 397 | if rel.multiple: 398 | return ModelMultipleChoiceFilter(**default) 399 | else: 400 | return ModelChoiceFilter(**default) 401 | 402 | 403 | class FilterSet(six.with_metaclass(FilterSetMetaclass, BaseFilterSet)): 404 | pass 405 | 406 | 407 | def filterset_factory(model): 408 | meta = type(str("Meta"), (object,), {"model": model}) 409 | filterset = type( 410 | str("%sFilterSet" % model._meta.object_name), 411 | (FilterSet,), 412 | {"Meta": meta}, 413 | ) 414 | return filterset 415 | -------------------------------------------------------------------------------- /django_mongoengine_filter/locale/fr/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barseghyanartur/django-mongoengine-filter/cf9a83e47377b8b4276d19f3d71379d5ea1af77b/django_mongoengine_filter/locale/fr/LC_MESSAGES/django.po -------------------------------------------------------------------------------- /django_mongoengine_filter/views.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.core.exceptions import ImproperlyConfigured 4 | from django.views.generic import View 5 | from django.views.generic.list import ( 6 | MultipleObjectMixin, 7 | MultipleObjectTemplateResponseMixin, 8 | ) 9 | 10 | from .filterset import filterset_factory 11 | 12 | __all__ = ("BaseFilterView", "FilterMixin", "FilterView", "object_filter") 13 | 14 | 15 | class FilterMixin: 16 | """ 17 | A mixin that provides a way to show and handle a FilterSet in a request. 18 | """ 19 | 20 | filterset_class = None 21 | 22 | def get_filterset_class(self): 23 | """ 24 | Returns the filterset class to use in this view 25 | """ 26 | if self.filterset_class: 27 | return self.filterset_class 28 | elif self.model: 29 | return filterset_factory(self.model) 30 | else: 31 | msg = "'%s' must define 'filterset_class' or 'model'" 32 | raise ImproperlyConfigured(msg % self.__class__.__name__) 33 | 34 | def get_filterset(self, filterset_class): 35 | """ 36 | Returns an instance of the filterset to be used in this view. 37 | """ 38 | kwargs = self.get_filterset_kwargs(filterset_class) 39 | return filterset_class(**kwargs) 40 | 41 | def get_filterset_kwargs(self, filterset_class): 42 | """ 43 | Returns the keyword arguments for instanciating the filterset. 44 | """ 45 | kwargs = {"data": self.request.GET or None} 46 | try: 47 | kwargs.update({"queryset": self.get_queryset()}) 48 | except ImproperlyConfigured: 49 | # ignore the error here if the filterset has a model defined 50 | # to acquire a queryset from 51 | if filterset_class._meta.model is None: 52 | msg = ( 53 | "'%s' does not define a 'model' and the view '%s' does " 54 | "not return a valid queryset from 'get_queryset'. You " 55 | "must fix one of them." 56 | ) 57 | args = (filterset_class.__name__, self.__class__.__name__) 58 | raise ImproperlyConfigured(msg % args) 59 | return kwargs 60 | 61 | 62 | class BaseFilterView(FilterMixin, MultipleObjectMixin, View): 63 | def get(self, request, *args, **kwargs): 64 | filterset_class = self.get_filterset_class() 65 | self.filterset = self.get_filterset(filterset_class) 66 | self.object_list = self.filterset.qs 67 | context = self.get_context_data( 68 | filter=self.filterset, object_list=self.object_list 69 | ) 70 | return self.render_to_response(context) 71 | 72 | 73 | class FilterView(MultipleObjectTemplateResponseMixin, BaseFilterView): 74 | """ 75 | Render some list of objects with filter, set by `self.model` or 76 | `self.queryset`. 77 | `self.queryset` can actually be any iterable of items, not just a queryset. 78 | """ 79 | 80 | template_name_suffix = "_filter" 81 | 82 | 83 | def object_filter( 84 | request, 85 | model=None, 86 | queryset=None, 87 | template_name=None, 88 | extra_context=None, 89 | context_processors=None, 90 | filter_class=None, 91 | ): 92 | class ECFilterView(FilterView): 93 | """Handle the extra_context from the functional object_filter view""" 94 | 95 | def get_context_data(self, **kwargs): 96 | context = super(ECFilterView, self).get_context_data(**kwargs) 97 | extra_context = self.kwargs.get("extra_context") or {} 98 | for k, v in extra_context.items(): 99 | if callable(v): 100 | v = v() 101 | context[k] = v 102 | return context 103 | 104 | kwargs = dict( 105 | model=model, 106 | queryset=queryset, 107 | template_name=template_name, 108 | filterset_class=filter_class, 109 | ) 110 | view = ECFilterView.as_view(**kwargs) 111 | return view(request, extra_context=extra_context) 112 | -------------------------------------------------------------------------------- /django_mongoengine_filter/widgets.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from itertools import chain 4 | 5 | try: 6 | from urllib.parse import urlencode 7 | except ImportError: 8 | from urllib import urlencode # noqa 9 | 10 | from django import forms 11 | from django.db.models.fields import BLANK_CHOICE_DASH 12 | 13 | try: 14 | from django.forms.widgets import flatatt 15 | except ImportError: 16 | from django.forms.utils import flatatt 17 | 18 | from django.utils.encoding import force_str as force_text 19 | from django.utils.safestring import mark_safe 20 | from django.utils.translation import gettext as _ 21 | 22 | __all__ = ("LinkWidget", "LookupTypeWidget", "RangeWidget") 23 | 24 | 25 | class LinkWidget(forms.Widget): 26 | def __init__(self, attrs=None, choices=()): 27 | super(LinkWidget, self).__init__(attrs) 28 | 29 | self.choices = choices 30 | 31 | def value_from_datadict(self, data, files, name): 32 | value = super(LinkWidget, self).value_from_datadict(data, files, name) 33 | self.data = data 34 | return value 35 | 36 | def render(self, name, value, attrs=None, choices=()): 37 | if not hasattr(self, "data"): 38 | self.data = {} 39 | if value is None: 40 | value = "" 41 | final_attrs = self.build_attrs(attrs) 42 | output = ["" % flatatt(final_attrs)] 43 | options = self.render_options(choices, [value], name) 44 | if options: 45 | output.append(options) 46 | output.append("") 47 | return mark_safe("\n".join(output)) 48 | 49 | def render_options(self, choices, selected_choices, name): 50 | selected_choices = set(force_text(v) for v in selected_choices) 51 | output = [] 52 | for option_value, option_label in chain(self.choices, choices): 53 | if isinstance(option_label, (list, tuple)): 54 | for option in option_label: 55 | output.append( 56 | self.render_option(name, selected_choices, *option) 57 | ) 58 | else: 59 | output.append( 60 | self.render_option( 61 | name, selected_choices, option_value, option_label 62 | ) 63 | ) 64 | return "\n".join(output) 65 | 66 | def render_option(self, name, selected_choices, option_value, option_label): 67 | option_value = force_text(option_value) 68 | if option_label == BLANK_CHOICE_DASH[0][1]: 69 | option_label = _("All") 70 | data = self.data.copy() 71 | data[name] = option_value 72 | selected = data == self.data or option_value in selected_choices 73 | try: 74 | url = data.urlencode() 75 | except AttributeError: 76 | url = urlencode(data) 77 | return self.option_string() % { 78 | "attrs": selected and ' class="selected"' or "", 79 | "query_string": url, 80 | "label": force_text(option_label), 81 | } 82 | 83 | def option_string(self): 84 | return '
  • %(label)s
  • ' 85 | 86 | 87 | class RangeWidget(forms.MultiWidget): 88 | def __init__(self, attrs=None): 89 | widgets = (forms.TextInput(attrs=attrs), forms.TextInput(attrs=attrs)) 90 | super(RangeWidget, self).__init__(widgets, attrs) 91 | 92 | def decompress(self, value): 93 | if value: 94 | return [value.start, value.stop] 95 | return [None, None] 96 | 97 | def format_output(self, rendered_widgets): 98 | return "-".join(rendered_widgets) 99 | 100 | 101 | class LookupTypeWidget(forms.MultiWidget): 102 | def decompress(self, value): 103 | if value is None: 104 | return [None, None] 105 | return value 106 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | 5 | mongo: 6 | image: mongo:5 7 | restart: always 8 | environment: 9 | # - MONGO_INITDB_ROOT_USERNAME=root 10 | # - MONGO_INITDB_ROOT_PASSWORD=test 11 | - MONGO_DATA_DIR=/var/db 12 | - MONGO_LOG_DIR=/var/logs 13 | volumes: 14 | - ./var/db:/var/db 15 | - ./var/logs:/var/logs 16 | ports: 17 | - 27017:27017 18 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barseghyanartur/django-mongoengine-filter/cf9a83e47377b8b4276d19f3d71379d5ea1af77b/docs/Makefile -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGELOG.rst 2 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # ska documentation build configuration file, created by 4 | # sphinx-quickstart on Sun Oct 13 00:13:46 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import os 15 | import sys 16 | 17 | # If extensions (or modules to document with autodoc) are in another directory, 18 | # add these directories to sys.path here. If the directory is relative to the 19 | # documentation root, use os.path.abspath to make it absolute, like shown here. 20 | # sys.path.insert(0, os.path.abspath('.')) 21 | 22 | sys.path.insert(0, os.path.abspath("..")) 23 | sys.path.insert(0, os.path.abspath("../tests")) 24 | try: 25 | import django_mongoengine_filter 26 | from tests import settings as docs_settings 27 | 28 | version = django_mongoengine_filter.__version__ 29 | project = django_mongoengine_filter.__title__ 30 | copyright = django_mongoengine_filter.__copyright__ 31 | except Exception: 32 | version = "0.1" 33 | project = "django-filter-mongoengine" 34 | copyright = "2019, Artur Barseghyan " 35 | 36 | # -- Django configuration ------------------------------------------------------ 37 | os.environ["DJANGO_SETTINGS_MODULE"] = "settings.dev" 38 | import django 39 | 40 | django.setup() 41 | # -- General configuration ----------------------------------------------------- 42 | 43 | # If your documentation needs a minimal Sphinx version, state it here. 44 | # needs_sphinx = '1.0' 45 | 46 | # Add any Sphinx extension module names here, as strings. They can be extensions 47 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 48 | extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode"] 49 | 50 | # Add any paths that contain templates here, relative to this directory. 51 | templates_path = ["_templates"] 52 | 53 | # The suffix of source filenames. 54 | source_suffix = ".rst" 55 | 56 | # The encoding of source files. 57 | # source_encoding = 'utf-8-sig' 58 | 59 | # The master toctree document. 60 | master_doc = "index" 61 | 62 | # General information about the project. 63 | 64 | # The version info for the project you're documenting, acts as replacement for 65 | # |version| and |release|, also used in various other places throughout the 66 | # built documents. 67 | # 68 | # The short X.Y version. 69 | # version = '0.1' 70 | # The full version, including alpha/beta/rc tags. 71 | release = version 72 | 73 | # The language for content autogenerated by Sphinx. Refer to documentation 74 | # for a list of supported languages. 75 | # language = None 76 | 77 | # There are two options for replacing |today|: either, you set today to some 78 | # non-false value, then it is used: 79 | # today = '' 80 | # Else, today_fmt is used as the format for a strftime call. 81 | # today_fmt = '%B %d, %Y' 82 | 83 | # List of patterns, relative to source directory, that match files and 84 | # directories to ignore when looking for source files. 85 | exclude_patterns = ["_build"] 86 | 87 | # The reST default role (used for this markup: `text`) to use for all documents. 88 | # default_role = None 89 | 90 | # If true, '()' will be appended to :func: etc. cross-reference text. 91 | # add_function_parentheses = True 92 | 93 | # If true, the current module name will be prepended to all description 94 | # unit titles (such as .. function::). 95 | # add_module_names = True 96 | 97 | # If true, sectionauthor and moduleauthor directives will be shown in the 98 | # output. They are ignored by default. 99 | # show_authors = False 100 | 101 | # The name of the Pygments (syntax highlighting) style to use. 102 | pygments_style = "sphinx" 103 | 104 | # A list of ignored prefixes for module index sorting. 105 | # modindex_common_prefix = [] 106 | 107 | # If true, keep warnings as "system message" paragraphs in the built documents. 108 | # keep_warnings = False 109 | 110 | 111 | # -- Options for HTML output --------------------------------------------------- 112 | 113 | # The theme to use for HTML and HTML Help pages. See the documentation for 114 | # a list of builtin themes. 115 | html_theme = "default" 116 | 117 | # Theme options are theme-specific and customize the look and feel of a theme 118 | # further. For a list of options available for each theme, see the 119 | # documentation. 120 | # html_theme_options = {} 121 | 122 | # Add any paths that contain custom themes here, relative to this directory. 123 | # html_theme_path = [] 124 | 125 | # The name for this set of Sphinx documents. If None, it defaults to 126 | # " v documentation". 127 | # html_title = None 128 | 129 | # A shorter title for the navigation bar. Default is the same as html_title. 130 | # html_short_title = None 131 | 132 | # The name of an image file (relative to this directory) to place at the top 133 | # of the sidebar. 134 | # html_logo = None 135 | 136 | # The name of an image file (within the static path) to use as favicon of the 137 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 138 | # pixels large. 139 | # html_favicon = None 140 | 141 | # Add any paths that contain custom static files (such as style sheets) here, 142 | # relative to this directory. They are copied after the builtin static files, 143 | # so a file named "default.css" will overwrite the builtin "default.css". 144 | html_static_path = ["_static"] 145 | 146 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 147 | # using the given strftime format. 148 | # html_last_updated_fmt = '%b %d, %Y' 149 | 150 | # If true, SmartyPants will be used to convert quotes and dashes to 151 | # typographically correct entities. 152 | # html_use_smartypants = True 153 | 154 | # Custom sidebar templates, maps document names to template names. 155 | # html_sidebars = {} 156 | 157 | # Additional templates that should be rendered to pages, maps page names to 158 | # template names. 159 | # html_additional_pages = {} 160 | 161 | # If false, no module index is generated. 162 | # html_domain_indices = True 163 | 164 | # If false, no index is generated. 165 | # html_use_index = True 166 | 167 | # If true, the index is split into individual pages for each letter. 168 | # html_split_index = False 169 | 170 | # If true, links to the reST sources are added to the pages. 171 | # html_show_sourcelink = True 172 | 173 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 174 | # html_show_sphinx = True 175 | 176 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 177 | # html_show_copyright = True 178 | 179 | # If true, an OpenSearch description file will be output, and all pages will 180 | # contain a tag referring to it. The value of this option must be the 181 | # base URL from which the finished HTML is served. 182 | # html_use_opensearch = '' 183 | 184 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 185 | # html_file_suffix = None 186 | 187 | # Output file base name for HTML help builder. 188 | htmlhelp_basename = "skadoc" 189 | 190 | 191 | # -- Options for LaTeX output -------------------------------------------------- 192 | 193 | latex_elements = { 194 | # The paper size ('letterpaper' or 'a4paper'). 195 | # 'papersize': 'letterpaper', 196 | # The font size ('10pt', '11pt' or '12pt'). 197 | # 'pointsize': '10pt', 198 | # Additional stuff for the LaTeX preamble. 199 | # 'preamble': '', 200 | } 201 | 202 | # Grouping the document tree into LaTeX files. List of tuples 203 | # (source start file, target name, title, author, documentclass [howto/manual]). 204 | latex_documents = [ 205 | ( 206 | "index", 207 | "django-mongoengine-filter.tex", 208 | "django-mongoengine-filter Documentation", 209 | "Artur Barseghyan \\textless{}" 210 | "artur.barseghyan@gmail.com\\textgreater{}", 211 | "manual", 212 | ) 213 | ] 214 | 215 | # The name of an image file (relative to this directory) to place at the top of 216 | # the title page. 217 | # latex_logo = None 218 | 219 | # For "manual" documents, if this is true, then toplevel headings are parts, 220 | # not chapters. 221 | # latex_use_parts = False 222 | 223 | # If true, show page references after internal links. 224 | # latex_show_pagerefs = False 225 | 226 | # If true, show URL addresses after external links. 227 | # latex_show_urls = False 228 | 229 | # Documents to append as an appendix to all manuals. 230 | # latex_appendices = [] 231 | 232 | # If false, no module index is generated. 233 | # latex_domain_indices = True 234 | 235 | 236 | # -- Options for manual page output -------------------------------------------- 237 | 238 | # One entry per manual page. List of tuples 239 | # (source start file, name, description, authors, manual section). 240 | man_pages = [ 241 | ( 242 | "index", 243 | "django-mongoengine-filter", 244 | "django-mongoengine-filter Documentation", 245 | ["Artur Barseghyan "], 246 | 1, 247 | ) 248 | ] 249 | 250 | # If true, show URL addresses after external links. 251 | # man_show_urls = False 252 | 253 | 254 | # -- Options for Texinfo output ------------------------------------------------ 255 | 256 | # Grouping the document tree into Texinfo files. List of tuples 257 | # (source start file, target name, title, author, 258 | # dir menu entry, description, category) 259 | texinfo_documents = [ 260 | ( 261 | "index", 262 | "django-mongoengine-filter", 263 | "ska Documentation", 264 | "Artur Barseghyan ", 265 | "django-mongoengine-filter", 266 | "django-filter for MongoEngine.", 267 | "Miscellaneous", 268 | ) 269 | ] 270 | 271 | # Documents to append as an appendix to all manuals. 272 | # texinfo_appendices = [] 273 | 274 | # If false, no module index is generated. 275 | # texinfo_domain_indices = True 276 | 277 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 278 | # texinfo_show_urls = 'footnote' 279 | 280 | # If true, do not generate a @detailmenu in the "Top" node's menu. 281 | # texinfo_no_detailmenu = False 282 | 283 | 284 | # -- Options for Epub output --------------------------------------------------- 285 | 286 | # Bibliographic Dublin Core info. 287 | epub_title = "django-mongoengine-filter" 288 | epub_author = "Artur Barseghyan " 289 | epub_publisher = "Artur Barseghyan " 290 | epub_copyright = "2019, Artur Barseghyan " 291 | 292 | # The language of the text. It defaults to the language option 293 | # or en if the language is not set. 294 | # epub_language = '' 295 | 296 | # The scheme of the identifier. Typical schemes are ISBN or URL. 297 | # epub_scheme = '' 298 | 299 | # The unique identifier of the text. This can be a ISBN number 300 | # or the project homepage. 301 | # epub_identifier = '' 302 | 303 | # A unique identification for the text. 304 | # epub_uid = '' 305 | 306 | # A tuple containing the cover image and cover page html template filenames. 307 | # epub_cover = () 308 | 309 | # A sequence of (type, uri, title) tuples for the guide element of content.opf. 310 | # epub_guide = () 311 | 312 | # HTML files that should be inserted before the pages created by sphinx. 313 | # The format is a list of tuples containing the path and title. 314 | # epub_pre_files = [] 315 | 316 | # HTML files shat should be inserted after the pages created by sphinx. 317 | # The format is a list of tuples containing the path and title. 318 | # epub_post_files = [] 319 | 320 | # A list of files that should not be packed into the epub file. 321 | # epub_exclude_files = [] 322 | 323 | # The depth of the table of contents in toc.ncx. 324 | # epub_tocdepth = 3 325 | 326 | # Allow duplicate toc entries. 327 | # epub_tocdup = True 328 | 329 | # Fix unsupported image types using the PIL. 330 | # epub_fix_images = False 331 | 332 | # Scale large images. 333 | # epub_max_image_width = 0 334 | 335 | # If 'no', URL addresses will not be shown. 336 | # epub_show_urls = 'inline' 337 | 338 | # If false, no index is generated. 339 | # epub_use_index = True 340 | -------------------------------------------------------------------------------- /docs/conf.py.distrib: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # ska documentation build configuration file, created by 4 | # sphinx-quickstart on Sun Oct 13 00:13:46 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import os 15 | import sys 16 | 17 | # If extensions (or modules to document with autodoc) are in another directory, 18 | # add these directories to sys.path here. If the directory is relative to the 19 | # documentation root, use os.path.abspath to make it absolute, like shown here. 20 | # sys.path.insert(0, os.path.abspath('.')) 21 | 22 | sys.path.insert(0, os.path.abspath("..")) 23 | sys.path.insert(0, os.path.abspath("../tests")) 24 | try: 25 | import django_mongoengine_filter 26 | from tests import settings as docs_settings 27 | 28 | version = django_mongoengine_filter.__version__ 29 | project = django_mongoengine_filter.__title__ 30 | copyright = django_mongoengine_filter.__copyright__ 31 | except Exception: 32 | version = "0.1" 33 | project = "django-filter-mongoengine" 34 | copyright = "2019, Artur Barseghyan " 35 | 36 | # -- Django configuration ------------------------------------------------------ 37 | os.environ["DJANGO_SETTINGS_MODULE"] = "settings.dev" 38 | import django 39 | 40 | django.setup() 41 | # -- General configuration ----------------------------------------------------- 42 | 43 | # If your documentation needs a minimal Sphinx version, state it here. 44 | # needs_sphinx = '1.0' 45 | 46 | # Add any Sphinx extension module names here, as strings. They can be extensions 47 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 48 | extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode"] 49 | 50 | # Add any paths that contain templates here, relative to this directory. 51 | templates_path = ["_templates"] 52 | 53 | # The suffix of source filenames. 54 | source_suffix = ".rst" 55 | 56 | # The encoding of source files. 57 | # source_encoding = 'utf-8-sig' 58 | 59 | # The master toctree document. 60 | master_doc = "index" 61 | 62 | # General information about the project. 63 | 64 | # The version info for the project you're documenting, acts as replacement for 65 | # |version| and |release|, also used in various other places throughout the 66 | # built documents. 67 | # 68 | # The short X.Y version. 69 | # version = '0.1' 70 | # The full version, including alpha/beta/rc tags. 71 | release = version 72 | 73 | # The language for content autogenerated by Sphinx. Refer to documentation 74 | # for a list of supported languages. 75 | # language = None 76 | 77 | # There are two options for replacing |today|: either, you set today to some 78 | # non-false value, then it is used: 79 | # today = '' 80 | # Else, today_fmt is used as the format for a strftime call. 81 | # today_fmt = '%B %d, %Y' 82 | 83 | # List of patterns, relative to source directory, that match files and 84 | # directories to ignore when looking for source files. 85 | exclude_patterns = ["_build"] 86 | 87 | # The reST default role (used for this markup: `text`) to use for all documents. 88 | # default_role = None 89 | 90 | # If true, '()' will be appended to :func: etc. cross-reference text. 91 | # add_function_parentheses = True 92 | 93 | # If true, the current module name will be prepended to all description 94 | # unit titles (such as .. function::). 95 | # add_module_names = True 96 | 97 | # If true, sectionauthor and moduleauthor directives will be shown in the 98 | # output. They are ignored by default. 99 | # show_authors = False 100 | 101 | # The name of the Pygments (syntax highlighting) style to use. 102 | pygments_style = "sphinx" 103 | 104 | # A list of ignored prefixes for module index sorting. 105 | # modindex_common_prefix = [] 106 | 107 | # If true, keep warnings as "system message" paragraphs in the built documents. 108 | # keep_warnings = False 109 | 110 | 111 | # -- Options for HTML output --------------------------------------------------- 112 | 113 | # The theme to use for HTML and HTML Help pages. See the documentation for 114 | # a list of builtin themes. 115 | html_theme = "default" 116 | 117 | # Theme options are theme-specific and customize the look and feel of a theme 118 | # further. For a list of options available for each theme, see the 119 | # documentation. 120 | # html_theme_options = {} 121 | 122 | # Add any paths that contain custom themes here, relative to this directory. 123 | # html_theme_path = [] 124 | 125 | # The name for this set of Sphinx documents. If None, it defaults to 126 | # " v documentation". 127 | # html_title = None 128 | 129 | # A shorter title for the navigation bar. Default is the same as html_title. 130 | # html_short_title = None 131 | 132 | # The name of an image file (relative to this directory) to place at the top 133 | # of the sidebar. 134 | # html_logo = None 135 | 136 | # The name of an image file (within the static path) to use as favicon of the 137 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 138 | # pixels large. 139 | # html_favicon = None 140 | 141 | # Add any paths that contain custom static files (such as style sheets) here, 142 | # relative to this directory. They are copied after the builtin static files, 143 | # so a file named "default.css" will overwrite the builtin "default.css". 144 | html_static_path = ["_static"] 145 | 146 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 147 | # using the given strftime format. 148 | # html_last_updated_fmt = '%b %d, %Y' 149 | 150 | # If true, SmartyPants will be used to convert quotes and dashes to 151 | # typographically correct entities. 152 | # html_use_smartypants = True 153 | 154 | # Custom sidebar templates, maps document names to template names. 155 | # html_sidebars = {} 156 | 157 | # Additional templates that should be rendered to pages, maps page names to 158 | # template names. 159 | # html_additional_pages = {} 160 | 161 | # If false, no module index is generated. 162 | # html_domain_indices = True 163 | 164 | # If false, no index is generated. 165 | # html_use_index = True 166 | 167 | # If true, the index is split into individual pages for each letter. 168 | # html_split_index = False 169 | 170 | # If true, links to the reST sources are added to the pages. 171 | # html_show_sourcelink = True 172 | 173 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 174 | # html_show_sphinx = True 175 | 176 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 177 | # html_show_copyright = True 178 | 179 | # If true, an OpenSearch description file will be output, and all pages will 180 | # contain a tag referring to it. The value of this option must be the 181 | # base URL from which the finished HTML is served. 182 | # html_use_opensearch = '' 183 | 184 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 185 | # html_file_suffix = None 186 | 187 | # Output file base name for HTML help builder. 188 | htmlhelp_basename = "skadoc" 189 | 190 | 191 | # -- Options for LaTeX output -------------------------------------------------- 192 | 193 | latex_elements = { 194 | # The paper size ('letterpaper' or 'a4paper'). 195 | # 'papersize': 'letterpaper', 196 | # The font size ('10pt', '11pt' or '12pt'). 197 | # 'pointsize': '10pt', 198 | # Additional stuff for the LaTeX preamble. 199 | # 'preamble': '', 200 | } 201 | 202 | # Grouping the document tree into LaTeX files. List of tuples 203 | # (source start file, target name, title, author, documentclass [howto/manual]). 204 | latex_documents = [ 205 | ( 206 | "index", 207 | "django-mongoengine-filter.tex", 208 | "django-mongoengine-filter Documentation", 209 | "Artur Barseghyan \\textless{}" 210 | "artur.barseghyan@gmail.com\\textgreater{}", 211 | "manual", 212 | ) 213 | ] 214 | 215 | # The name of an image file (relative to this directory) to place at the top of 216 | # the title page. 217 | # latex_logo = None 218 | 219 | # For "manual" documents, if this is true, then toplevel headings are parts, 220 | # not chapters. 221 | # latex_use_parts = False 222 | 223 | # If true, show page references after internal links. 224 | # latex_show_pagerefs = False 225 | 226 | # If true, show URL addresses after external links. 227 | # latex_show_urls = False 228 | 229 | # Documents to append as an appendix to all manuals. 230 | # latex_appendices = [] 231 | 232 | # If false, no module index is generated. 233 | # latex_domain_indices = True 234 | 235 | 236 | # -- Options for manual page output -------------------------------------------- 237 | 238 | # One entry per manual page. List of tuples 239 | # (source start file, name, description, authors, manual section). 240 | man_pages = [ 241 | ( 242 | "index", 243 | "django-mongoengine-filter", 244 | "django-mongoengine-filter Documentation", 245 | ["Artur Barseghyan "], 246 | 1, 247 | ) 248 | ] 249 | 250 | # If true, show URL addresses after external links. 251 | # man_show_urls = False 252 | 253 | 254 | # -- Options for Texinfo output ------------------------------------------------ 255 | 256 | # Grouping the document tree into Texinfo files. List of tuples 257 | # (source start file, target name, title, author, 258 | # dir menu entry, description, category) 259 | texinfo_documents = [ 260 | ( 261 | "index", 262 | "django-mongoengine-filter", 263 | "ska Documentation", 264 | "Artur Barseghyan ", 265 | "django-mongoengine-filter", 266 | "django-filter for MongoEngine.", 267 | "Miscellaneous", 268 | ) 269 | ] 270 | 271 | # Documents to append as an appendix to all manuals. 272 | # texinfo_appendices = [] 273 | 274 | # If false, no module index is generated. 275 | # texinfo_domain_indices = True 276 | 277 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 278 | # texinfo_show_urls = 'footnote' 279 | 280 | # If true, do not generate a @detailmenu in the "Top" node's menu. 281 | # texinfo_no_detailmenu = False 282 | 283 | 284 | # -- Options for Epub output --------------------------------------------------- 285 | 286 | # Bibliographic Dublin Core info. 287 | epub_title = "django-mongoengine-filter" 288 | epub_author = "Artur Barseghyan " 289 | epub_publisher = "Artur Barseghyan " 290 | epub_copyright = "2019, Artur Barseghyan " 291 | 292 | # The language of the text. It defaults to the language option 293 | # or en if the language is not set. 294 | # epub_language = '' 295 | 296 | # The scheme of the identifier. Typical schemes are ISBN or URL. 297 | # epub_scheme = '' 298 | 299 | # The unique identifier of the text. This can be a ISBN number 300 | # or the project homepage. 301 | # epub_identifier = '' 302 | 303 | # A unique identification for the text. 304 | # epub_uid = '' 305 | 306 | # A tuple containing the cover image and cover page html template filenames. 307 | # epub_cover = () 308 | 309 | # A sequence of (type, uri, title) tuples for the guide element of content.opf. 310 | # epub_guide = () 311 | 312 | # HTML files that should be inserted before the pages created by sphinx. 313 | # The format is a list of tuples containing the path and title. 314 | # epub_pre_files = [] 315 | 316 | # HTML files shat should be inserted after the pages created by sphinx. 317 | # The format is a list of tuples containing the path and title. 318 | # epub_post_files = [] 319 | 320 | # A list of files that should not be packed into the epub file. 321 | # epub_exclude_files = [] 322 | 323 | # The depth of the table of contents in toc.ncx. 324 | # epub_tocdepth = 3 325 | 326 | # Allow duplicate toc entries. 327 | # epub_tocdup = True 328 | 329 | # Fix unsupported image types using the PIL. 330 | # epub_fix_images = False 331 | 332 | # Scale large images. 333 | # epub_max_image_width = 0 334 | 335 | # If 'no', URL addresses will not be shown. 336 | # epub_show_urls = 'inline' 337 | 338 | # If false, no index is generated. 339 | # epub_use_index = True 340 | -------------------------------------------------------------------------------- /docs/documentation.rst: -------------------------------------------------------------------------------- 1 | 2 | Documentation 3 | ============= 4 | Contents: 5 | 6 | .. toctree:: 7 | :maxdepth: 20 8 | 9 | index 10 | filters 11 | widgets 12 | usage 13 | recipes 14 | changelog 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | -------------------------------------------------------------------------------- /docs/filters.rst: -------------------------------------------------------------------------------- 1 | Filter Reference 2 | ================ 3 | 4 | This is a reference document with a list of the filters and their arguments. 5 | 6 | Filters 7 | ------- 8 | 9 | ``CharFilter`` 10 | ~~~~~~~~~~~~~~ 11 | 12 | This filter does simple character matches, used with ``CharField`` and 13 | ``TextField`` by default. 14 | 15 | ``BooleanFilter`` 16 | ~~~~~~~~~~~~~~~~~ 17 | 18 | This filter matches a boolean, either ``True`` or ``False``, used with 19 | ``BooleanField`` and ``NullBooleanField`` by default. 20 | 21 | ``ChoiceFilter`` 22 | ~~~~~~~~~~~~~~~~ 23 | 24 | This filter matches an item of any type by choices, used with any field that 25 | has ``choices``. 26 | 27 | ``MultipleChoiceFilter`` 28 | ~~~~~~~~~~~~~~~~~~~~~~~~ 29 | 30 | The same as ``ChoiceFilter`` except the user can select multiple items and it 31 | selects the OR of all the choices. 32 | 33 | ``DateFilter`` 34 | ~~~~~~~~~~~~~~ 35 | 36 | Matches on a date. Used with ``DateField`` by default. 37 | 38 | ``DateTimeFilter`` 39 | ~~~~~~~~~~~~~~~~~~ 40 | 41 | Matches on a date and time. Used with ``DateTimeField`` by default. 42 | 43 | ``TimeFilter`` 44 | ~~~~~~~~~~~~~~ 45 | 46 | Matches on a time. Used with ``TimeField`` by default. 47 | 48 | ``ModelChoiceFilter`` 49 | ~~~~~~~~~~~~~~~~~~~~~ 50 | 51 | Similar to a ``ChoiceFilter`` except it works with related models, used for 52 | ``ForeignKey`` by default. 53 | 54 | ``ModelMultipleChoiceFilter`` 55 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 56 | 57 | Similar to a ``MultipleChoiceFilter`` except it works with related models, used 58 | for ``ManyToManyField`` by default. 59 | 60 | ``NumberFilter`` 61 | ~~~~~~~~~~~~~~~~ 62 | 63 | Filters based on a numerical value, used with ``IntegerField``, ``FloatField``, 64 | and ``DecimalField`` by default. 65 | 66 | ``RangeFilter`` 67 | ~~~~~~~~~~~~~~~ 68 | 69 | Filters where a value is between two numerical values. 70 | 71 | ``DateRangeFilter`` 72 | ~~~~~~~~~~~~~~~~~~~ 73 | 74 | Filter similar to the admin changelist date one, it has a number of common 75 | selections for working with date fields. 76 | 77 | ``AllValuesFilter`` 78 | ~~~~~~~~~~~~~~~~~~~ 79 | 80 | This is a ``ChoiceFilter`` whose choices are the current values in the 81 | database. So if in the DB for the given field you have values of 5, 7, and 9 82 | each of those is present as an option. This is similar to the default behavior 83 | of the admin. 84 | 85 | Core Arguments 86 | -------------- 87 | 88 | ``name`` 89 | ~~~~~~~~ 90 | 91 | The name of the field this filter is supposed to filter on, if this is not 92 | provided it automatically becomes the filter's name on the ``FilterSet``. 93 | 94 | ``label`` 95 | ~~~~~~~~~ 96 | 97 | The label as it will apear in the HTML, analogous to a form field's label 98 | argument. 99 | 100 | ``widget`` 101 | ~~~~~~~~~~ 102 | 103 | The django.form Widget class which will represent the ``Filter``. In addition 104 | to the widgets that are included with Django that you can use there are 105 | additional ones that django-filter provides which may be useful: 106 | 107 | * ``django_filters.widgets.LinkWidget`` -- this displays the options in a 108 | manner similar to the way the Django Admin does, as a series of links. 109 | The link for the selected option will have ``class="selected"``. 110 | 111 | ``action`` 112 | ~~~~~~~~~~ 113 | 114 | An optional callable that tells the filter how to handle the queryset. It 115 | receives a ``QuerySet`` and the value to filter on and should return a 116 | ``Queryset`` that is filtered appropriately. 117 | 118 | ``lookup_type`` 119 | ~~~~~~~~~~~~~~~ 120 | 121 | The type of lookup that should be performed using the Django ORM. All the 122 | normal options are allowed, and should be provided as a string. You can also 123 | provide either ``None`` or a ``list`` or a ``tuple``. If ``None`` is provided, 124 | then the user can select the lookup type from all the ones available in the 125 | Django ORM. If a ``list`` or ``tuple`` is provided, then the user can select 126 | from those options. 127 | 128 | ``distinct`` 129 | ~~~~~~~~~~~~ 130 | 131 | A boolean value that specifies whether the Filter will use distinct on the 132 | queryset. This option can be used to eliminate duplicate results when using 133 | filters that span related models. Defaults to ``False``. 134 | 135 | ``exclude`` 136 | ~~~~~~~~~~~ 137 | 138 | A boolean value that specifies whether the Filter should use ``filter`` 139 | or ``exclude`` on the queryset. Defaults to ``False``. 140 | 141 | 142 | ``**kwargs`` 143 | ~~~~~~~~~~~~ 144 | 145 | Any extra keyword arguments will be provided to the accompanying form Field. 146 | This can be used to provide arguments like ``choices`` or ``queryset``. 147 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | .. include:: documentation.rst 3 | -------------------------------------------------------------------------------- /docs/index.rst.distrib: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | .. include:: documentation.rst 3 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barseghyanartur/django-mongoengine-filter/cf9a83e47377b8b4276d19f3d71379d5ea1af77b/docs/make.bat -------------------------------------------------------------------------------- /docs/recipes.rst: -------------------------------------------------------------------------------- 1 | Recipes 2 | ======= 3 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | Using django-mongoengine-filter 2 | =============================== 3 | 4 | django-mongoengine-filter provides a simple way to filter down a queryset based on 5 | parameters a user provides. Say we have a ``Product`` model and we want to let 6 | our users filter which products they see on a list page. 7 | 8 | The model 9 | --------- 10 | 11 | Let's start with our model: 12 | 13 | .. code-block:: python 14 | 15 | from django.db import models 16 | 17 | class Product(models.Model): 18 | name = models.CharField(max_length=255) 19 | price = models.DecimalField() 20 | description = models.TextField() 21 | release_date = models.DateField() 22 | manufacturer = models.ForeignKey(Manufacturer) 23 | 24 | The filter 25 | ---------- 26 | 27 | We have a number of fields and we want to let our users filter based on the 28 | price or the release_date. We create a ``FilterSet`` for this: 29 | 30 | .. code-block:: python 31 | 32 | import django_mongoengine_filter 33 | 34 | class ProductFilter(django_mongoengine_filter.FilterSet): 35 | class Meta: 36 | model = Product 37 | fields = ['price', 'release_date'] 38 | 39 | 40 | As you can see this uses a very similar API to Django's ``ModelForm``. Just 41 | like with a ``ModelForm`` we can also override filters, or add new ones using a 42 | declarative syntax: 43 | 44 | .. code-block:: python 45 | 46 | import django_filters 47 | 48 | class ProductFilter(django_mongoengine_filter.FilterSet): 49 | price = django_filters.NumberFilter(lookup_type='lt') 50 | class Meta: 51 | model = Product 52 | fields = ['price', 'release_date'] 53 | 54 | Filters take a ``lookup_type`` argument which specifies what lookup type to 55 | use with Django's ORM. So here when a user entered a price it would show all 56 | Products with a price less than that. 57 | 58 | **It's quite common to forget to set lookup type for `CharField`s/`TextField`s 59 | and wonder why search for "foo" doesn't return result for "foobar". It's because 60 | default lookup type is exact text, but you probably want `icontains` lookup 61 | field.** 62 | 63 | Items in the ``fields`` sequence in the ``Meta`` class may include 64 | "relationship paths" using Django's ``__`` syntax to filter on fields on a 65 | related model: 66 | 67 | .. code-block:: python 68 | 69 | class ProductFilter(django_mongoengine_filter.FilterSet): 70 | class Meta: 71 | model = Product 72 | fields = ['manufacturer__country'] 73 | 74 | Filters also take any arbitrary keyword arguments which get passed onto the 75 | ``django.forms.Field`` initializer. These extra keyword arguments get stored 76 | in ``Filter.extra``, so it's possible to override the initializer of a 77 | ``FilterSet`` to add extra ones: 78 | 79 | .. code-block:: python 80 | 81 | class ProductFilter(django_mongoengine_filter.FilterSet): 82 | class Meta: 83 | model = Product 84 | fields = ['manufacturer'] 85 | 86 | def __init__(self, *args, **kwargs): 87 | super(ProductFilter, self).__init__(*args, **kwargs) 88 | self.filters['manufacturer'].extra.update( 89 | {'empty_label': 'All Manufacturers'}) 90 | 91 | Like ``django.contrib.admin.ModelAdmin`` does it is possible to override 92 | default filters for all the models fields of the same kind using 93 | ``filter_overrides``: 94 | 95 | .. code-block:: python 96 | 97 | class ProductFilter(django_mongoengine_filter.FilterSet): 98 | filter_overrides = { 99 | models.CharField: { 100 | 'filter_class': django_filters.CharFilter, 101 | 'extra': lambda f: { 102 | 'lookup_type': 'icontains', 103 | } 104 | } 105 | } 106 | 107 | class Meta: 108 | model = Product 109 | fields = ['name'] 110 | 111 | The view 112 | -------- 113 | 114 | Now we need to write a view: 115 | 116 | .. code-block:: python 117 | 118 | def product_list(request): 119 | f = ProductFilter(request.GET, queryset=Product.objects) 120 | return render_to_response('my_app/template.html', {'filter': f}) 121 | 122 | If a queryset argument isn't provided then all the items in the default manager 123 | of the model will be used. 124 | 125 | The URL conf 126 | ------------ 127 | 128 | We need a URL pattern to call the view: 129 | 130 | .. code-block:: python 131 | 132 | re_path(r'^list$', views.product_list) 133 | 134 | The template 135 | ------------ 136 | 137 | And lastly we need a template: 138 | 139 | .. code-block:: html 140 | 141 | {% extends "base.html" %} 142 | 143 | {% block content %} 144 |
    145 | {{ filter.form.as_p }} 146 | 147 |
    148 | {% for obj in filter %} 149 | {{ obj.name }} - ${{ obj.price }}
    150 | {% endfor %} 151 | {% endblock %} 152 | 153 | And that's all there is to it! The ``form`` attribute contains a normal 154 | Django form, and when we iterate over the ``FilterSet`` we get the objects in 155 | the resulting queryset. 156 | 157 | Other Meta options 158 | ------------------ 159 | 160 | Ordering using ``order_by`` 161 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 162 | 163 | You can allow the user to control ordering by providing the 164 | ``order_by`` argument in the Filter's Meta class. ``order_by`` can be either a 165 | ``list`` or ``tuple`` of field names, in which case those are the options, or 166 | it can be a ``bool`` which, if True, indicates that all fields that 167 | the user can filter on can also be sorted on. An example or ordering using a list: 168 | 169 | .. code-block:: python 170 | 171 | import django_filters 172 | 173 | class ProductFilter(django_filters.FilterSet): 174 | 175 | price = django_filters.NumberFilter(lookup_type='lt') 176 | 177 | class Meta: 178 | model = Product 179 | fields = ['price', 'release_date'] 180 | order_by = ['price'] 181 | 182 | If you want to control the display of items in ``order_by``, you can set it to 183 | a list or tuple of 2-tuples in the format ``(field_name, display_name)``. 184 | This lets you override the displayed names for your ordering fields: 185 | 186 | .. code-block:: python 187 | 188 | order_by = ( 189 | ('name', 'Company Name'), 190 | ('average_rating', 'Stars'), 191 | ) 192 | 193 | Note that the default query parameter name used for ordering is ``o``. You 194 | can override this by setting an ``order_by_field`` attribute on the 195 | ``FilterSet`` class to the string value you would like to use. 196 | 197 | Custom Forms using ``form`` 198 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 199 | 200 | The inner ``Meta`` class also takes an optional ``form`` argument. This is a 201 | form class from which ``FilterSet.form`` will subclass. This works similar to 202 | the ``form`` option on a ``ModelAdmin.`` 203 | 204 | Non-Meta options 205 | ---------------- 206 | 207 | Note that these options do not go in the Meta class, they are specified directly 208 | in your FilterSet class. 209 | 210 | ``strict`` 211 | ~~~~~~~~~~ 212 | 213 | The ``strict`` option controls whether results are returned when an invalid 214 | value is specified by the user for any filter field. By default, ``strict`` is 215 | set to ``True`` meaning that an empty queryset is returned if any field contains 216 | an invalid value. You can loosen this behavior by setting ``strict`` to 217 | ``False`` which will effectively ignore a filter field if its value is invalid. 218 | 219 | Overriding ``FilterSet`` methods 220 | -------------------------------- 221 | 222 | ``get_ordering_field()`` 223 | ~~~~~~~~~~~~~~~~~~~~~~~~ 224 | 225 | If you want to use a custom widget, or in any other way override the ordering 226 | field you can override the ``get_ordering_field()`` method on a ``FilterSet``. 227 | This method just needs to return a Form Field. 228 | 229 | Ordering on multiple fields, or other complex orderings can be achieved by 230 | overriding the ``Filterset.get_order_by()`` method. This is passed the selected 231 | ``order_by`` value, and is expected to return an iterable of values to pass to 232 | ``QuerySet.order_by``. For example, to sort a ``User`` table by last name, then 233 | first name: 234 | 235 | .. code-block:: python 236 | 237 | class UserFilter(django_filters.FilterSet): 238 | class Meta: 239 | order_by = ( 240 | ('username', 'Username'), 241 | ('last_name', 'Last Name') 242 | ) 243 | 244 | def get_order_by(self, order_value): 245 | if order_value == 'last_name': 246 | return ['last_name', 'first_name'] 247 | return super(UserFilter, self).get_order_by(order_value) 248 | 249 | Generic View 250 | ------------ 251 | 252 | In addition to the above usage there is also a class-based generic view 253 | included in django-filter, which lives at ``django_filters.views.FilterView``. 254 | You must provide either a ``model`` or ``filterset_class`` argument, similar to 255 | ``ListView`` in Django itself: 256 | 257 | .. code-block:: python 258 | 259 | # urls.py 260 | from django.urls import re_path 261 | from django_filters.views import FilterView 262 | from myapp.models import Product 263 | 264 | urlpatterns = [ 265 | re_path(r'^list/$', FilterView.as_view(model=Product)), 266 | ] 267 | 268 | You must provide a template at ``/_filter.html`` which gets the 269 | context parameter ``filter``. Additionally, the context will contain 270 | ``object_list`` which holds the filtered queryset. 271 | 272 | A legacy functional generic view is still included in django-filter, although 273 | its use is deprecated. It can be found at 274 | ``django_filters.views.object_filter``. You must provide the same arguments 275 | to it as the class based view: 276 | 277 | .. code-block:: python 278 | 279 | # urls.py 280 | from django.urls import re_path 281 | from myapp.models import Product 282 | 283 | urlpatterns = [ 284 | re_path(r'^list/$', 'django_filters.views.object_filter', {'model': Product}), 285 | ] 286 | 287 | The needed template and its context variables will also be the same as the 288 | class-based view above. 289 | -------------------------------------------------------------------------------- /docs/widgets.rst: -------------------------------------------------------------------------------- 1 | Widget Reference 2 | ================ 3 | 4 | This is a reference document with a list of the provided widgets and their 5 | arguments. 6 | 7 | ``LinkWidget`` 8 | ~~~~~~~~~~~~~~ 9 | 10 | This widget renders each option as a link, instead of an actual . It has 11 | one method that you can override for additional customization. 12 | ``option_string()`` should return a string with 3 Python keyword argument 13 | placeholders:: 14 | 15 | 1. ``attrs``: This is a string with all the attributes that will be on the 16 | final ```` tag. 17 | 2. ``query_string``: This is the query string for use in the ``href`` 18 | option on the ```` element. 19 | 3. ``label``: This is the text to be displayed to the user. 20 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # Example configuration for Black. 2 | 3 | # NOTE: you have to use single-quoted strings in TOML for regular expressions. 4 | # It's the equivalent of r-strings in Python. Multiline strings are treated as 5 | # verbose regular expressions by Black. Use [ ] to denote a significant space 6 | # character. 7 | 8 | [tool.black] 9 | line-length = 80 10 | target-version = ['py37', 'py38', 'py39', 'py310', 'py311'] 11 | include = '\.pyi?$' 12 | extend-exclude = ''' 13 | /( 14 | # The following are specific to Black, you probably don't want those. 15 | | blib2to3 16 | | tests/data 17 | | profiling 18 | | migrations 19 | )/ 20 | ''' 21 | 22 | # Build system information below. 23 | # NOTE: You don't need this in your own Black configuration. 24 | [build-system] 25 | requires = ["setuptools>=41.0", "setuptools-scm", "wheel"] 26 | build-backend = "setuptools.build_meta" 27 | 28 | [tool.isort] 29 | profile = "black" 30 | multi_line_output = 3 31 | include_trailing_comma = true 32 | force_grid_wrap = 0 33 | use_parentheses = true 34 | ensure_newline_before_comments = true 35 | line_length = 80 36 | known_first_party = "django_mongoengine_filter" 37 | known_third_party = ["django", "factory"] 38 | skip = ["wsgi.py"] 39 | skip_glob = [".tox/**"] 40 | 41 | [tool.ruff] 42 | line-length = 80 43 | 44 | # Enable Pyflakes `E` and `F` codes by default. 45 | select = ["E", "F"] 46 | ignore = [] 47 | 48 | # Exclude a variety of commonly ignored directories. 49 | exclude = [ 50 | ".bzr", 51 | ".direnv", 52 | ".eggs", 53 | ".git", 54 | ".hg", 55 | ".mypy_cache", 56 | ".nox", 57 | ".pants.d", 58 | ".ruff_cache", 59 | ".svn", 60 | ".tox", 61 | ".venv", 62 | "__pypackages__", 63 | "_build", 64 | "buck-out", 65 | "build", 66 | "dist", 67 | "node_modules", 68 | "venv", 69 | "manage.py", 70 | ] 71 | per-file-ignores = {} 72 | 73 | # Allow unused variables when underscore-prefixed. 74 | dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" 75 | 76 | # Assume Python 3.10. 77 | target-version = "py310" 78 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | norecursedirs= 3 | *.egg 4 | .hg 5 | .git 6 | .tox 7 | .env 8 | _sass 9 | build 10 | dist 11 | migrations 12 | python_files = 13 | test_*.py 14 | tests.py 15 | python_paths = 16 | . 17 | tests 18 | DJANGO_SETTINGS_MODULE=tests.settings 19 | addopts= 20 | --cov=django_mongoengine_filter 21 | --ignore=.tox 22 | --ignore=requirements 23 | --ignore=var 24 | --ignore=releases 25 | --cov-report=html 26 | --cov-report=term 27 | --cov-report=annotate 28 | --cov-append 29 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -r dev.in 2 | -------------------------------------------------------------------------------- /requirements/code_style.in: -------------------------------------------------------------------------------- 1 | black 2 | pydocstyle 3 | pylint 4 | ruff 5 | isort 6 | pre-commit 7 | check-manifest 8 | restview 9 | -------------------------------------------------------------------------------- /requirements/code_style.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # pip-compile requirements/code_style.in 6 | # 7 | astroid==2.12.13 8 | # via pylint 9 | black==22.12.0 10 | # via -r requirements/code_style.in 11 | bleach==5.0.1 12 | # via readme-renderer 13 | build==0.9.0 14 | # via check-manifest 15 | cfgv==3.3.1 16 | # via pre-commit 17 | check-manifest==0.49 18 | # via -r requirements/code_style.in 19 | click==8.1.3 20 | # via black 21 | dill==0.3.6 22 | # via pylint 23 | distlib==0.3.6 24 | # via virtualenv 25 | docutils==0.19 26 | # via 27 | # readme-renderer 28 | # restview 29 | filelock==3.8.2 30 | # via virtualenv 31 | identify==2.5.11 32 | # via pre-commit 33 | isort==5.11.4 34 | # via 35 | # -r requirements/code_style.in 36 | # pylint 37 | lazy-object-proxy==1.8.0 38 | # via astroid 39 | mccabe==0.7.0 40 | # via pylint 41 | mypy-extensions==0.4.3 42 | # via black 43 | nodeenv==1.7.0 44 | # via pre-commit 45 | packaging==22.0 46 | # via build 47 | pathspec==0.10.3 48 | # via black 49 | pep517==0.13.0 50 | # via build 51 | platformdirs==2.6.0 52 | # via 53 | # black 54 | # pylint 55 | # virtualenv 56 | pre-commit==2.20.0 57 | # via -r requirements/code_style.in 58 | pydocstyle==6.1.1 59 | # via -r requirements/code_style.in 60 | pygments==2.13.0 61 | # via 62 | # readme-renderer 63 | # restview 64 | pylint==2.15.9 65 | # via -r requirements/code_style.in 66 | pyyaml==6.0 67 | # via pre-commit 68 | readme-renderer==37.3 69 | # via restview 70 | restview==3.0.0 71 | # via -r requirements/code_style.in 72 | ruff==0.0.193 73 | # via -r requirements/code_style.in 74 | six==1.16.0 75 | # via bleach 76 | snowballstemmer==2.2.0 77 | # via pydocstyle 78 | toml==0.10.2 79 | # via pre-commit 80 | tomli==2.0.1 81 | # via 82 | # black 83 | # build 84 | # check-manifest 85 | # pep517 86 | # pylint 87 | tomlkit==0.11.6 88 | # via pylint 89 | virtualenv==20.17.1 90 | # via pre-commit 91 | webencodings==0.5.1 92 | # via bleach 93 | wrapt==1.14.1 94 | # via astroid 95 | 96 | # The following packages are considered to be unsafe in a requirements file: 97 | # setuptools 98 | -------------------------------------------------------------------------------- /requirements/common.in: -------------------------------------------------------------------------------- 1 | mongoengine>=0.24 2 | -------------------------------------------------------------------------------- /requirements/common.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # pip-compile requirements/common.in 6 | # 7 | dnspython==2.2.1 8 | # via pymongo 9 | mongoengine==0.24.2 10 | # via -r requirements/common.in 11 | pymongo==4.3.3 12 | # via mongoengine 13 | -------------------------------------------------------------------------------- /requirements/debug.in: -------------------------------------------------------------------------------- 1 | ipython 2 | ipdb 3 | -------------------------------------------------------------------------------- /requirements/debug.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # pip-compile requirements/debug.in 6 | # 7 | asttokens==2.2.1 8 | # via stack-data 9 | backcall==0.2.0 10 | # via ipython 11 | decorator==5.1.1 12 | # via 13 | # ipdb 14 | # ipython 15 | executing==1.2.0 16 | # via stack-data 17 | ipdb==0.13.11 18 | # via -r requirements/debug.in 19 | ipython==8.7.0 20 | # via 21 | # -r requirements/debug.in 22 | # ipdb 23 | jedi==0.18.2 24 | # via ipython 25 | matplotlib-inline==0.1.6 26 | # via ipython 27 | parso==0.8.3 28 | # via jedi 29 | pexpect==4.8.0 30 | # via ipython 31 | pickleshare==0.7.5 32 | # via ipython 33 | prompt-toolkit==3.0.36 34 | # via ipython 35 | ptyprocess==0.7.0 36 | # via pexpect 37 | pure-eval==0.2.2 38 | # via stack-data 39 | pygments==2.13.0 40 | # via ipython 41 | six==1.16.0 42 | # via asttokens 43 | stack-data==0.6.2 44 | # via ipython 45 | tomli==2.0.1 46 | # via ipdb 47 | traitlets==5.8.0 48 | # via 49 | # ipython 50 | # matplotlib-inline 51 | wcwidth==0.2.5 52 | # via prompt-toolkit 53 | -------------------------------------------------------------------------------- /requirements/dev.in: -------------------------------------------------------------------------------- 1 | -r django_3_2.in 2 | -r code_style.in 3 | -r docs.in 4 | -------------------------------------------------------------------------------- /requirements/dev.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # pip-compile requirements/dev.in 6 | # 7 | alabaster==0.7.12 8 | # via sphinx 9 | asgiref==3.6.0 10 | # via django 11 | astroid==2.12.13 12 | # via pylint 13 | attrs==22.2.0 14 | # via pytest 15 | babel==2.11.0 16 | # via sphinx 17 | beautifulsoup4==4.11.1 18 | # via -r requirements/test.in 19 | black==22.12.0 20 | # via -r requirements/code_style.in 21 | bleach==5.0.1 22 | # via readme-renderer 23 | build==0.9.0 24 | # via check-manifest 25 | cachetools==5.2.0 26 | # via tox 27 | certifi==2022.12.7 28 | # via requests 29 | cfgv==3.3.1 30 | # via pre-commit 31 | chardet==5.1.0 32 | # via tox 33 | charset-normalizer==2.1.1 34 | # via requests 35 | check-manifest==0.49 36 | # via -r requirements/code_style.in 37 | click==8.1.3 38 | # via black 39 | colorama==0.4.6 40 | # via tox 41 | coverage[toml]==7.0.1 42 | # via pytest-cov 43 | dill==0.3.6 44 | # via pylint 45 | distlib==0.3.6 46 | # via virtualenv 47 | django==3.2.16 48 | # via -r requirements/django_3_2.in 49 | dnspython==2.2.1 50 | # via pymongo 51 | docutils==0.19 52 | # via 53 | # -r requirements/docs.in 54 | # readme-renderer 55 | # restview 56 | # sphinx 57 | factory-boy==3.2.1 58 | # via -r requirements/test.in 59 | faker==15.3.4 60 | # via 61 | # -r requirements/test.in 62 | # factory-boy 63 | filelock==3.8.2 64 | # via 65 | # tox 66 | # virtualenv 67 | identify==2.5.11 68 | # via pre-commit 69 | idna==3.4 70 | # via requests 71 | imagesize==1.4.1 72 | # via sphinx 73 | iniconfig==1.1.1 74 | # via pytest 75 | isort==5.11.4 76 | # via 77 | # -r requirements/code_style.in 78 | # pylint 79 | jinja2==3.1.2 80 | # via 81 | # -r requirements/docs.in 82 | # sphinx 83 | lazy-object-proxy==1.8.0 84 | # via astroid 85 | markupsafe==2.1.1 86 | # via 87 | # -r requirements/docs.in 88 | # jinja2 89 | mccabe==0.7.0 90 | # via pylint 91 | mongoengine==0.24.2 92 | # via -r requirements/common.in 93 | mypy-extensions==0.4.3 94 | # via black 95 | nodeenv==1.7.0 96 | # via pre-commit 97 | packaging==22.0 98 | # via 99 | # build 100 | # pyproject-api 101 | # pytest 102 | # sphinx 103 | # tox 104 | pathspec==0.10.3 105 | # via black 106 | pep517==0.13.0 107 | # via build 108 | platformdirs==2.6.0 109 | # via 110 | # black 111 | # pylint 112 | # tox 113 | # virtualenv 114 | pluggy==1.0.0 115 | # via 116 | # pytest 117 | # tox 118 | pre-commit==2.20.0 119 | # via -r requirements/code_style.in 120 | py==1.11.0 121 | # via pytest 122 | pydocstyle==6.1.1 123 | # via -r requirements/code_style.in 124 | pygments==2.13.0 125 | # via 126 | # readme-renderer 127 | # restview 128 | # sphinx 129 | pylint==2.15.9 130 | # via -r requirements/code_style.in 131 | pymongo==4.3.3 132 | # via mongoengine 133 | pyproject-api==1.2.1 134 | # via tox 135 | pytest==6.2.5 136 | # via 137 | # -r requirements/test.in 138 | # pytest-cov 139 | # pytest-django 140 | # pytest-ordering 141 | # pytest-pythonpath 142 | pytest-cov==4.0.0 143 | # via -r requirements/test.in 144 | pytest-django==4.5.2 145 | # via -r requirements/test.in 146 | pytest-ordering==0.6 147 | # via -r requirements/test.in 148 | pytest-pythonpath==0.7.4 149 | # via -r requirements/test.in 150 | python-dateutil==2.8.2 151 | # via faker 152 | pytz==2022.7 153 | # via 154 | # babel 155 | # django 156 | pyyaml==6.0 157 | # via pre-commit 158 | readme-renderer==37.3 159 | # via restview 160 | requests==2.28.1 161 | # via sphinx 162 | restview==3.0.0 163 | # via -r requirements/code_style.in 164 | ruff==0.0.193 165 | # via -r requirements/code_style.in 166 | six==1.16.0 167 | # via 168 | # bleach 169 | # python-dateutil 170 | snowballstemmer==2.2.0 171 | # via 172 | # pydocstyle 173 | # sphinx 174 | soupsieve==2.3.2.post1 175 | # via beautifulsoup4 176 | sphinx==5.3.0 177 | # via -r requirements/docs.in 178 | sphinxcontrib-applehelp==1.0.2 179 | # via sphinx 180 | sphinxcontrib-devhelp==1.0.2 181 | # via sphinx 182 | sphinxcontrib-htmlhelp==2.0.0 183 | # via sphinx 184 | sphinxcontrib-jsmath==1.0.1 185 | # via sphinx 186 | sphinxcontrib-qthelp==1.0.3 187 | # via sphinx 188 | sphinxcontrib-serializinghtml==1.1.5 189 | # via sphinx 190 | sqlparse==0.4.3 191 | # via django 192 | toml==0.10.2 193 | # via 194 | # pre-commit 195 | # pytest 196 | tomli==2.0.1 197 | # via 198 | # black 199 | # build 200 | # check-manifest 201 | # coverage 202 | # pep517 203 | # pylint 204 | # pyproject-api 205 | # tox 206 | tomlkit==0.11.6 207 | # via pylint 208 | tox==4.0.16 209 | # via -r requirements/test.in 210 | urllib3==1.26.13 211 | # via requests 212 | virtualenv==20.17.1 213 | # via 214 | # pre-commit 215 | # tox 216 | webencodings==0.5.1 217 | # via bleach 218 | wrapt==1.14.1 219 | # via astroid 220 | 221 | # The following packages are considered to be unsafe in a requirements file: 222 | # setuptools 223 | -------------------------------------------------------------------------------- /requirements/django_2_2.in: -------------------------------------------------------------------------------- 1 | -r common.in 2 | -r test.in 3 | 4 | Django>=2.2,<3.0 5 | -------------------------------------------------------------------------------- /requirements/django_2_2.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # pip-compile requirements/django_2_2.in 6 | # 7 | attrs==22.2.0 8 | # via pytest 9 | beautifulsoup4==4.11.1 10 | # via -r requirements/test.in 11 | cachetools==5.2.0 12 | # via tox 13 | chardet==5.1.0 14 | # via tox 15 | colorama==0.4.6 16 | # via tox 17 | coverage[toml]==7.0.1 18 | # via pytest-cov 19 | distlib==0.3.6 20 | # via virtualenv 21 | django==2.2.28 22 | # via -r requirements/django_2_2.in 23 | dnspython==2.2.1 24 | # via pymongo 25 | factory-boy==3.2.1 26 | # via -r requirements/test.in 27 | faker==15.3.4 28 | # via 29 | # -r requirements/test.in 30 | # factory-boy 31 | filelock==3.8.2 32 | # via 33 | # tox 34 | # virtualenv 35 | iniconfig==1.1.1 36 | # via pytest 37 | mongoengine==0.24.2 38 | # via -r requirements/common.in 39 | packaging==22.0 40 | # via 41 | # pyproject-api 42 | # pytest 43 | # tox 44 | platformdirs==2.6.0 45 | # via 46 | # tox 47 | # virtualenv 48 | pluggy==1.0.0 49 | # via 50 | # pytest 51 | # tox 52 | py==1.11.0 53 | # via pytest 54 | pymongo==4.3.3 55 | # via mongoengine 56 | pyproject-api==1.2.1 57 | # via tox 58 | pytest==6.2.5 59 | # via 60 | # -r requirements/test.in 61 | # pytest-cov 62 | # pytest-django 63 | # pytest-ordering 64 | # pytest-pythonpath 65 | pytest-cov==4.0.0 66 | # via -r requirements/test.in 67 | pytest-django==4.5.2 68 | # via -r requirements/test.in 69 | pytest-ordering==0.6 70 | # via -r requirements/test.in 71 | pytest-pythonpath==0.7.4 72 | # via -r requirements/test.in 73 | python-dateutil==2.8.2 74 | # via faker 75 | pytz==2022.7 76 | # via django 77 | six==1.16.0 78 | # via python-dateutil 79 | soupsieve==2.3.2.post1 80 | # via beautifulsoup4 81 | sqlparse==0.4.3 82 | # via django 83 | toml==0.10.2 84 | # via pytest 85 | tomli==2.0.1 86 | # via 87 | # coverage 88 | # pyproject-api 89 | # tox 90 | tox==4.0.16 91 | # via -r requirements/test.in 92 | virtualenv==20.17.1 93 | # via tox 94 | -------------------------------------------------------------------------------- /requirements/django_3_0.in: -------------------------------------------------------------------------------- 1 | -r common.in 2 | -r test.in 3 | 4 | Django>=3.0,<3.1 5 | -------------------------------------------------------------------------------- /requirements/django_3_0.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # pip-compile requirements/django_3_0.in 6 | # 7 | asgiref==3.6.0 8 | # via django 9 | attrs==22.2.0 10 | # via pytest 11 | beautifulsoup4==4.11.1 12 | # via -r requirements/test.in 13 | cachetools==5.2.0 14 | # via tox 15 | chardet==5.1.0 16 | # via tox 17 | colorama==0.4.6 18 | # via tox 19 | coverage[toml]==7.0.1 20 | # via pytest-cov 21 | distlib==0.3.6 22 | # via virtualenv 23 | django==3.0.14 24 | # via -r requirements/django_3_0.in 25 | dnspython==2.2.1 26 | # via pymongo 27 | factory-boy==3.2.1 28 | # via -r requirements/test.in 29 | faker==15.3.4 30 | # via 31 | # -r requirements/test.in 32 | # factory-boy 33 | filelock==3.8.2 34 | # via 35 | # tox 36 | # virtualenv 37 | iniconfig==1.1.1 38 | # via pytest 39 | mongoengine==0.24.2 40 | # via -r requirements/common.in 41 | packaging==22.0 42 | # via 43 | # pyproject-api 44 | # pytest 45 | # tox 46 | platformdirs==2.6.0 47 | # via 48 | # tox 49 | # virtualenv 50 | pluggy==1.0.0 51 | # via 52 | # pytest 53 | # tox 54 | py==1.11.0 55 | # via pytest 56 | pymongo==4.3.3 57 | # via mongoengine 58 | pyproject-api==1.2.1 59 | # via tox 60 | pytest==6.2.5 61 | # via 62 | # -r requirements/test.in 63 | # pytest-cov 64 | # pytest-django 65 | # pytest-ordering 66 | # pytest-pythonpath 67 | pytest-cov==4.0.0 68 | # via -r requirements/test.in 69 | pytest-django==4.5.2 70 | # via -r requirements/test.in 71 | pytest-ordering==0.6 72 | # via -r requirements/test.in 73 | pytest-pythonpath==0.7.4 74 | # via -r requirements/test.in 75 | python-dateutil==2.8.2 76 | # via faker 77 | pytz==2022.7 78 | # via django 79 | six==1.16.0 80 | # via python-dateutil 81 | soupsieve==2.3.2.post1 82 | # via beautifulsoup4 83 | sqlparse==0.4.3 84 | # via django 85 | toml==0.10.2 86 | # via pytest 87 | tomli==2.0.1 88 | # via 89 | # coverage 90 | # pyproject-api 91 | # tox 92 | tox==4.0.16 93 | # via -r requirements/test.in 94 | virtualenv==20.17.1 95 | # via tox 96 | -------------------------------------------------------------------------------- /requirements/django_3_1.in: -------------------------------------------------------------------------------- 1 | -r common.in 2 | -r test.in 3 | 4 | Django>=3.1,<3.2 5 | -------------------------------------------------------------------------------- /requirements/django_3_1.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # pip-compile requirements/django_3_1.in 6 | # 7 | asgiref==3.6.0 8 | # via django 9 | attrs==22.2.0 10 | # via pytest 11 | beautifulsoup4==4.11.1 12 | # via -r requirements/test.in 13 | cachetools==5.2.0 14 | # via tox 15 | chardet==5.1.0 16 | # via tox 17 | colorama==0.4.6 18 | # via tox 19 | coverage[toml]==7.0.1 20 | # via pytest-cov 21 | distlib==0.3.6 22 | # via virtualenv 23 | django==3.1.14 24 | # via -r requirements/django_3_1.in 25 | dnspython==2.2.1 26 | # via pymongo 27 | factory-boy==3.2.1 28 | # via -r requirements/test.in 29 | faker==15.3.4 30 | # via 31 | # -r requirements/test.in 32 | # factory-boy 33 | filelock==3.8.2 34 | # via 35 | # tox 36 | # virtualenv 37 | iniconfig==1.1.1 38 | # via pytest 39 | mongoengine==0.24.2 40 | # via -r requirements/common.in 41 | packaging==22.0 42 | # via 43 | # pyproject-api 44 | # pytest 45 | # tox 46 | platformdirs==2.6.0 47 | # via 48 | # tox 49 | # virtualenv 50 | pluggy==1.0.0 51 | # via 52 | # pytest 53 | # tox 54 | py==1.11.0 55 | # via pytest 56 | pymongo==4.3.3 57 | # via mongoengine 58 | pyproject-api==1.2.1 59 | # via tox 60 | pytest==6.2.5 61 | # via 62 | # -r requirements/test.in 63 | # pytest-cov 64 | # pytest-django 65 | # pytest-ordering 66 | # pytest-pythonpath 67 | pytest-cov==4.0.0 68 | # via -r requirements/test.in 69 | pytest-django==4.5.2 70 | # via -r requirements/test.in 71 | pytest-ordering==0.6 72 | # via -r requirements/test.in 73 | pytest-pythonpath==0.7.4 74 | # via -r requirements/test.in 75 | python-dateutil==2.8.2 76 | # via faker 77 | pytz==2022.7 78 | # via django 79 | six==1.16.0 80 | # via python-dateutil 81 | soupsieve==2.3.2.post1 82 | # via beautifulsoup4 83 | sqlparse==0.4.3 84 | # via django 85 | toml==0.10.2 86 | # via pytest 87 | tomli==2.0.1 88 | # via 89 | # coverage 90 | # pyproject-api 91 | # tox 92 | tox==4.0.16 93 | # via -r requirements/test.in 94 | virtualenv==20.17.1 95 | # via tox 96 | -------------------------------------------------------------------------------- /requirements/django_3_2.in: -------------------------------------------------------------------------------- 1 | -r common.in 2 | -r test.in 3 | 4 | Django>=3.2,<4.0 5 | -------------------------------------------------------------------------------- /requirements/django_3_2.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # pip-compile requirements/django_3_2.in 6 | # 7 | asgiref==3.6.0 8 | # via django 9 | attrs==22.2.0 10 | # via pytest 11 | beautifulsoup4==4.11.1 12 | # via -r requirements/test.in 13 | cachetools==5.2.0 14 | # via tox 15 | chardet==5.1.0 16 | # via tox 17 | colorama==0.4.6 18 | # via tox 19 | coverage[toml]==7.0.1 20 | # via pytest-cov 21 | distlib==0.3.6 22 | # via virtualenv 23 | django==3.2.16 24 | # via -r requirements/django_3_2.in 25 | dnspython==2.2.1 26 | # via pymongo 27 | factory-boy==3.2.1 28 | # via -r requirements/test.in 29 | faker==15.3.4 30 | # via 31 | # -r requirements/test.in 32 | # factory-boy 33 | filelock==3.8.2 34 | # via 35 | # tox 36 | # virtualenv 37 | iniconfig==1.1.1 38 | # via pytest 39 | mongoengine==0.24.2 40 | # via -r requirements/common.in 41 | packaging==22.0 42 | # via 43 | # pyproject-api 44 | # pytest 45 | # tox 46 | platformdirs==2.6.0 47 | # via 48 | # tox 49 | # virtualenv 50 | pluggy==1.0.0 51 | # via 52 | # pytest 53 | # tox 54 | py==1.11.0 55 | # via pytest 56 | pymongo==4.3.3 57 | # via mongoengine 58 | pyproject-api==1.2.1 59 | # via tox 60 | pytest==6.2.5 61 | # via 62 | # -r requirements/test.in 63 | # pytest-cov 64 | # pytest-django 65 | # pytest-ordering 66 | # pytest-pythonpath 67 | pytest-cov==4.0.0 68 | # via -r requirements/test.in 69 | pytest-django==4.5.2 70 | # via -r requirements/test.in 71 | pytest-ordering==0.6 72 | # via -r requirements/test.in 73 | pytest-pythonpath==0.7.4 74 | # via -r requirements/test.in 75 | python-dateutil==2.8.2 76 | # via faker 77 | pytz==2022.7 78 | # via django 79 | six==1.16.0 80 | # via python-dateutil 81 | soupsieve==2.3.2.post1 82 | # via beautifulsoup4 83 | sqlparse==0.4.3 84 | # via django 85 | toml==0.10.2 86 | # via pytest 87 | tomli==2.0.1 88 | # via 89 | # coverage 90 | # pyproject-api 91 | # tox 92 | tox==4.0.16 93 | # via -r requirements/test.in 94 | virtualenv==20.17.1 95 | # via tox 96 | -------------------------------------------------------------------------------- /requirements/django_4_0.in: -------------------------------------------------------------------------------- 1 | -r common.in 2 | -r test.in 3 | 4 | Django>=4.0,<4.1 5 | -------------------------------------------------------------------------------- /requirements/django_4_0.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # pip-compile requirements/django_4_0.in 6 | # 7 | asgiref==3.6.0 8 | # via django 9 | attrs==22.2.0 10 | # via pytest 11 | beautifulsoup4==4.11.1 12 | # via -r requirements/test.in 13 | cachetools==5.2.0 14 | # via tox 15 | chardet==5.1.0 16 | # via tox 17 | colorama==0.4.6 18 | # via tox 19 | coverage[toml]==7.0.1 20 | # via pytest-cov 21 | distlib==0.3.6 22 | # via virtualenv 23 | django==4.0.8 24 | # via -r requirements/django_4_0.in 25 | dnspython==2.2.1 26 | # via pymongo 27 | factory-boy==3.2.1 28 | # via -r requirements/test.in 29 | faker==15.3.4 30 | # via 31 | # -r requirements/test.in 32 | # factory-boy 33 | filelock==3.8.2 34 | # via 35 | # tox 36 | # virtualenv 37 | iniconfig==1.1.1 38 | # via pytest 39 | mongoengine==0.24.2 40 | # via -r requirements/common.in 41 | packaging==22.0 42 | # via 43 | # pyproject-api 44 | # pytest 45 | # tox 46 | platformdirs==2.6.0 47 | # via 48 | # tox 49 | # virtualenv 50 | pluggy==1.0.0 51 | # via 52 | # pytest 53 | # tox 54 | py==1.11.0 55 | # via pytest 56 | pymongo==4.3.3 57 | # via mongoengine 58 | pyproject-api==1.2.1 59 | # via tox 60 | pytest==6.2.5 61 | # via 62 | # -r requirements/test.in 63 | # pytest-cov 64 | # pytest-django 65 | # pytest-ordering 66 | # pytest-pythonpath 67 | pytest-cov==4.0.0 68 | # via -r requirements/test.in 69 | pytest-django==4.5.2 70 | # via -r requirements/test.in 71 | pytest-ordering==0.6 72 | # via -r requirements/test.in 73 | pytest-pythonpath==0.7.4 74 | # via -r requirements/test.in 75 | python-dateutil==2.8.2 76 | # via faker 77 | six==1.16.0 78 | # via python-dateutil 79 | soupsieve==2.3.2.post1 80 | # via beautifulsoup4 81 | sqlparse==0.4.3 82 | # via django 83 | toml==0.10.2 84 | # via pytest 85 | tomli==2.0.1 86 | # via 87 | # coverage 88 | # pyproject-api 89 | # tox 90 | tox==4.0.16 91 | # via -r requirements/test.in 92 | virtualenv==20.17.1 93 | # via tox 94 | -------------------------------------------------------------------------------- /requirements/django_4_1.in: -------------------------------------------------------------------------------- 1 | -r common.in 2 | -r test.in 3 | 4 | Django>=4.1,<4.2 5 | -------------------------------------------------------------------------------- /requirements/django_4_1.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # pip-compile requirements/django_4_1.in 6 | # 7 | asgiref==3.6.0 8 | # via django 9 | attrs==22.2.0 10 | # via pytest 11 | beautifulsoup4==4.11.1 12 | # via -r requirements/test.in 13 | cachetools==5.2.0 14 | # via tox 15 | chardet==5.1.0 16 | # via tox 17 | colorama==0.4.6 18 | # via tox 19 | coverage[toml]==7.0.1 20 | # via pytest-cov 21 | distlib==0.3.6 22 | # via virtualenv 23 | django==4.1.4 24 | # via -r requirements/django_4_1.in 25 | dnspython==2.2.1 26 | # via pymongo 27 | factory-boy==3.2.1 28 | # via -r requirements/test.in 29 | faker==15.3.4 30 | # via 31 | # -r requirements/test.in 32 | # factory-boy 33 | filelock==3.8.2 34 | # via 35 | # tox 36 | # virtualenv 37 | iniconfig==1.1.1 38 | # via pytest 39 | mongoengine==0.24.2 40 | # via -r requirements/common.in 41 | packaging==22.0 42 | # via 43 | # pyproject-api 44 | # pytest 45 | # tox 46 | platformdirs==2.6.0 47 | # via 48 | # tox 49 | # virtualenv 50 | pluggy==1.0.0 51 | # via 52 | # pytest 53 | # tox 54 | py==1.11.0 55 | # via pytest 56 | pymongo==4.3.3 57 | # via mongoengine 58 | pyproject-api==1.2.1 59 | # via tox 60 | pytest==6.2.5 61 | # via 62 | # -r requirements/test.in 63 | # pytest-cov 64 | # pytest-django 65 | # pytest-ordering 66 | # pytest-pythonpath 67 | pytest-cov==4.0.0 68 | # via -r requirements/test.in 69 | pytest-django==4.5.2 70 | # via -r requirements/test.in 71 | pytest-ordering==0.6 72 | # via -r requirements/test.in 73 | pytest-pythonpath==0.7.4 74 | # via -r requirements/test.in 75 | python-dateutil==2.8.2 76 | # via faker 77 | six==1.16.0 78 | # via python-dateutil 79 | soupsieve==2.3.2.post1 80 | # via beautifulsoup4 81 | sqlparse==0.4.3 82 | # via django 83 | toml==0.10.2 84 | # via pytest 85 | tomli==2.0.1 86 | # via 87 | # coverage 88 | # pyproject-api 89 | # tox 90 | tox==4.0.16 91 | # via -r requirements/test.in 92 | virtualenv==20.17.1 93 | # via tox 94 | -------------------------------------------------------------------------------- /requirements/docs.in: -------------------------------------------------------------------------------- 1 | Jinja2 2 | MarkupSafe 3 | Sphinx 4 | docutils 5 | -------------------------------------------------------------------------------- /requirements/docs.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # pip-compile requirements/docs.in 6 | # 7 | alabaster==0.7.12 8 | # via sphinx 9 | babel==2.11.0 10 | # via sphinx 11 | certifi==2022.12.7 12 | # via requests 13 | charset-normalizer==2.1.1 14 | # via requests 15 | docutils==0.19 16 | # via 17 | # -r requirements/docs.in 18 | # sphinx 19 | idna==3.4 20 | # via requests 21 | imagesize==1.4.1 22 | # via sphinx 23 | jinja2==3.1.2 24 | # via 25 | # -r requirements/docs.in 26 | # sphinx 27 | markupsafe==2.1.1 28 | # via 29 | # -r requirements/docs.in 30 | # jinja2 31 | packaging==22.0 32 | # via sphinx 33 | pygments==2.13.0 34 | # via sphinx 35 | pytz==2022.7 36 | # via babel 37 | requests==2.28.1 38 | # via sphinx 39 | snowballstemmer==2.2.0 40 | # via sphinx 41 | sphinx==5.3.0 42 | # via -r requirements/docs.in 43 | sphinxcontrib-applehelp==1.0.2 44 | # via sphinx 45 | sphinxcontrib-devhelp==1.0.2 46 | # via sphinx 47 | sphinxcontrib-htmlhelp==2.0.0 48 | # via sphinx 49 | sphinxcontrib-jsmath==1.0.1 50 | # via sphinx 51 | sphinxcontrib-qthelp==1.0.3 52 | # via sphinx 53 | sphinxcontrib-serializinghtml==1.1.5 54 | # via sphinx 55 | urllib3==1.26.13 56 | # via requests 57 | -------------------------------------------------------------------------------- /requirements/documentation.in: -------------------------------------------------------------------------------- 1 | -r django_3_2.in 2 | -r docs.in 3 | -------------------------------------------------------------------------------- /requirements/documentation.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # pip-compile requirements/documentation.in 6 | # 7 | alabaster==0.7.12 8 | # via sphinx 9 | asgiref==3.6.0 10 | # via django 11 | attrs==22.2.0 12 | # via pytest 13 | babel==2.11.0 14 | # via sphinx 15 | beautifulsoup4==4.11.1 16 | # via -r requirements/test.in 17 | cachetools==5.2.0 18 | # via tox 19 | certifi==2022.12.7 20 | # via requests 21 | chardet==5.1.0 22 | # via tox 23 | charset-normalizer==2.1.1 24 | # via requests 25 | colorama==0.4.6 26 | # via tox 27 | coverage[toml]==7.0.1 28 | # via pytest-cov 29 | distlib==0.3.6 30 | # via virtualenv 31 | django==3.2.16 32 | # via -r requirements/django_3_2.in 33 | dnspython==2.2.1 34 | # via pymongo 35 | docutils==0.19 36 | # via 37 | # -r requirements/docs.in 38 | # sphinx 39 | factory-boy==3.2.1 40 | # via -r requirements/test.in 41 | faker==15.3.4 42 | # via 43 | # -r requirements/test.in 44 | # factory-boy 45 | filelock==3.8.2 46 | # via 47 | # tox 48 | # virtualenv 49 | idna==3.4 50 | # via requests 51 | imagesize==1.4.1 52 | # via sphinx 53 | iniconfig==1.1.1 54 | # via pytest 55 | jinja2==3.1.2 56 | # via 57 | # -r requirements/docs.in 58 | # sphinx 59 | markupsafe==2.1.1 60 | # via 61 | # -r requirements/docs.in 62 | # jinja2 63 | mongoengine==0.24.2 64 | # via -r requirements/common.in 65 | packaging==22.0 66 | # via 67 | # pyproject-api 68 | # pytest 69 | # sphinx 70 | # tox 71 | platformdirs==2.6.0 72 | # via 73 | # tox 74 | # virtualenv 75 | pluggy==1.0.0 76 | # via 77 | # pytest 78 | # tox 79 | py==1.11.0 80 | # via pytest 81 | pygments==2.13.0 82 | # via sphinx 83 | pymongo==4.3.3 84 | # via mongoengine 85 | pyproject-api==1.2.1 86 | # via tox 87 | pytest==6.2.5 88 | # via 89 | # -r requirements/test.in 90 | # pytest-cov 91 | # pytest-django 92 | # pytest-ordering 93 | # pytest-pythonpath 94 | pytest-cov==4.0.0 95 | # via -r requirements/test.in 96 | pytest-django==4.5.2 97 | # via -r requirements/test.in 98 | pytest-ordering==0.6 99 | # via -r requirements/test.in 100 | pytest-pythonpath==0.7.4 101 | # via -r requirements/test.in 102 | python-dateutil==2.8.2 103 | # via faker 104 | pytz==2022.7 105 | # via 106 | # babel 107 | # django 108 | requests==2.28.1 109 | # via sphinx 110 | six==1.16.0 111 | # via python-dateutil 112 | snowballstemmer==2.2.0 113 | # via sphinx 114 | soupsieve==2.3.2.post1 115 | # via beautifulsoup4 116 | sphinx==5.3.0 117 | # via -r requirements/docs.in 118 | sphinxcontrib-applehelp==1.0.2 119 | # via sphinx 120 | sphinxcontrib-devhelp==1.0.2 121 | # via sphinx 122 | sphinxcontrib-htmlhelp==2.0.0 123 | # via sphinx 124 | sphinxcontrib-jsmath==1.0.1 125 | # via sphinx 126 | sphinxcontrib-qthelp==1.0.3 127 | # via sphinx 128 | sphinxcontrib-serializinghtml==1.1.5 129 | # via sphinx 130 | sqlparse==0.4.3 131 | # via django 132 | toml==0.10.2 133 | # via pytest 134 | tomli==2.0.1 135 | # via 136 | # coverage 137 | # pyproject-api 138 | # tox 139 | tox==4.0.16 140 | # via -r requirements/test.in 141 | urllib3==1.26.13 142 | # via requests 143 | virtualenv==20.17.1 144 | # via tox 145 | -------------------------------------------------------------------------------- /requirements/test.in: -------------------------------------------------------------------------------- 1 | beautifulsoup4 2 | factory_boy 3 | Faker 4 | pytest-cov 5 | pytest-django 6 | pytest-ordering 7 | pytest-pythonpath 8 | pytest 9 | tox 10 | -------------------------------------------------------------------------------- /requirements/test.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # pip-compile requirements/test.in 6 | # 7 | attrs==22.2.0 8 | # via pytest 9 | beautifulsoup4==4.11.1 10 | # via -r requirements/test.in 11 | cachetools==5.2.0 12 | # via tox 13 | chardet==5.1.0 14 | # via tox 15 | colorama==0.4.6 16 | # via tox 17 | coverage[toml]==7.0.1 18 | # via pytest-cov 19 | distlib==0.3.6 20 | # via virtualenv 21 | factory-boy==3.2.1 22 | # via -r requirements/test.in 23 | faker==15.3.4 24 | # via 25 | # -r requirements/test.in 26 | # factory-boy 27 | filelock==3.8.2 28 | # via 29 | # tox 30 | # virtualenv 31 | iniconfig==1.1.1 32 | # via pytest 33 | packaging==22.0 34 | # via 35 | # pyproject-api 36 | # pytest 37 | # tox 38 | platformdirs==2.6.0 39 | # via 40 | # tox 41 | # virtualenv 42 | pluggy==1.0.0 43 | # via 44 | # pytest 45 | # tox 46 | py==1.11.0 47 | # via pytest 48 | pyproject-api==1.2.1 49 | # via tox 50 | pytest==6.2.5 51 | # via 52 | # -r requirements/test.in 53 | # pytest-cov 54 | # pytest-django 55 | # pytest-ordering 56 | # pytest-pythonpath 57 | pytest-cov==4.0.0 58 | # via -r requirements/test.in 59 | pytest-django==4.5.2 60 | # via -r requirements/test.in 61 | pytest-ordering==0.6 62 | # via -r requirements/test.in 63 | pytest-pythonpath==0.7.4 64 | # via -r requirements/test.in 65 | python-dateutil==2.8.2 66 | # via faker 67 | six==1.16.0 68 | # via python-dateutil 69 | soupsieve==2.3.2.post1 70 | # via beautifulsoup4 71 | toml==0.10.2 72 | # via pytest 73 | tomli==2.0.1 74 | # via 75 | # coverage 76 | # pyproject-api 77 | # tox 78 | tox==4.0.16 79 | # via -r requirements/test.in 80 | virtualenv==20.17.1 81 | # via tox 82 | -------------------------------------------------------------------------------- /requirements/testing.in: -------------------------------------------------------------------------------- 1 | -r test.in 2 | -------------------------------------------------------------------------------- /requirements/testing.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # pip-compile requirements/testing.in 6 | # 7 | attrs==22.2.0 8 | # via pytest 9 | beautifulsoup4==4.11.1 10 | # via -r requirements/test.in 11 | cachetools==5.2.0 12 | # via tox 13 | chardet==5.1.0 14 | # via tox 15 | colorama==0.4.6 16 | # via tox 17 | coverage[toml]==7.0.1 18 | # via pytest-cov 19 | distlib==0.3.6 20 | # via virtualenv 21 | factory-boy==3.2.1 22 | # via -r requirements/test.in 23 | faker==15.3.4 24 | # via 25 | # -r requirements/test.in 26 | # factory-boy 27 | filelock==3.8.2 28 | # via 29 | # tox 30 | # virtualenv 31 | iniconfig==1.1.1 32 | # via pytest 33 | packaging==22.0 34 | # via 35 | # pyproject-api 36 | # pytest 37 | # tox 38 | platformdirs==2.6.0 39 | # via 40 | # tox 41 | # virtualenv 42 | pluggy==1.0.0 43 | # via 44 | # pytest 45 | # tox 46 | py==1.11.0 47 | # via pytest 48 | pyproject-api==1.2.1 49 | # via tox 50 | pytest==6.2.5 51 | # via 52 | # -r requirements/test.in 53 | # pytest-cov 54 | # pytest-django 55 | # pytest-ordering 56 | # pytest-pythonpath 57 | pytest-cov==4.0.0 58 | # via -r requirements/test.in 59 | pytest-django==4.5.2 60 | # via -r requirements/test.in 61 | pytest-ordering==0.6 62 | # via -r requirements/test.in 63 | pytest-pythonpath==0.7.4 64 | # via -r requirements/test.in 65 | python-dateutil==2.8.2 66 | # via faker 67 | six==1.16.0 68 | # via python-dateutil 69 | soupsieve==2.3.2.post1 70 | # via beautifulsoup4 71 | toml==0.10.2 72 | # via pytest 73 | tomli==2.0.1 74 | # via 75 | # coverage 76 | # pyproject-api 77 | # tox 78 | tox==4.0.16 79 | # via -r requirements/test.in 80 | virtualenv==20.17.1 81 | # via tox 82 | -------------------------------------------------------------------------------- /scripts/black.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | black . 3 | -------------------------------------------------------------------------------- /scripts/build_docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #./scripts/uninstall.sh 3 | #./scripts/install.sh 4 | 5 | sphinx-build -n -a -b html docs builddocs 6 | cd builddocs && zip -r ../builddocs.zip . -x ".*" && cd .. 7 | -------------------------------------------------------------------------------- /scripts/clean_up.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | find . -name "*.pyc" -exec rm -rf {} \; 3 | find . -name "__pycache__" -exec rm -rf {} \; 4 | find . -name "*.orig" -exec rm -rf {} \; 5 | find . -name "*.py,cover" -exec rm -rf {} \; 6 | find . -name "*.log" -exec rm -rf {} \; 7 | find . -name "*.log.*" -exec rm -rf {} \; 8 | rm -rf build/ 9 | rm -rf dist/ 10 | rm -rf .cache/ 11 | rm -rf htmlcov/ 12 | rm -rf builddocs/ 13 | rm -rf builddocs.zip 14 | rm -rf .pytest_cache/ 15 | rm -rf .tox/ 16 | -------------------------------------------------------------------------------- /scripts/compile_requirements.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "code_style.in" 3 | pip-compile requirements/code_style.in "$@" 4 | 5 | echo "common.in" 6 | pip-compile requirements/common.in "$@" 7 | 8 | echo "debug.in" 9 | pip-compile requirements/debug.in "$@" 10 | 11 | echo "dev.in" 12 | pip-compile requirements/dev.in "$@" 13 | 14 | echo "django_2_2.in" 15 | pip-compile requirements/django_2_2.in "$@" 16 | 17 | echo "django_3_0.in" 18 | pip-compile requirements/django_3_0.in "$@" 19 | 20 | echo "django_3_1.in" 21 | pip-compile requirements/django_3_1.in "$@" 22 | 23 | echo "django_3_2.in" 24 | pip-compile requirements/django_3_2.in "$@" 25 | 26 | echo "django_4_0.in" 27 | pip-compile requirements/django_4_0.in "$@" 28 | 29 | echo "django_4_1.in" 30 | pip-compile requirements/django_4_1.in "$@" 31 | 32 | echo "docs.in" 33 | pip-compile requirements/docs.in "$@" 34 | 35 | echo "documentation.in" 36 | pip-compile requirements/documentation.in "$@" 37 | 38 | echo "test.in" 39 | pip-compile requirements/test.in "$@" 40 | 41 | echo "testing.in" 42 | pip-compile requirements/testing.in "$@" 43 | -------------------------------------------------------------------------------- /scripts/create_dirs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | mkdir -p var/db/ 3 | -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | pip install -e . 3 | ./scripts/create_dirs.sh 4 | -------------------------------------------------------------------------------- /scripts/isort.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | isort . --overwrite-in-place 3 | -------------------------------------------------------------------------------- /scripts/make_release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #./scripts/uninstall.sh 3 | #./scripts/clean_up.sh 4 | #python setup.py register 5 | python setup.py sdist bdist_wheel 6 | twine upload dist/* --verbose 7 | -------------------------------------------------------------------------------- /scripts/migrate.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | cd tests/ 3 | ./manage.py migrate --traceback -v 3 --settings=settings.dev "$@" 4 | -------------------------------------------------------------------------------- /scripts/rebuild_docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #./scripts/uninstall.sh 3 | #./scripts/install.sh 4 | ./scripts/clean_up.sh 5 | sphinx-apidoc . --full -o docs -H 'django-filter-mongoengine' -A 'Artur Barseghyan ' -f -d 20 6 | cp docs/conf.py.distrib docs/conf.py 7 | cp docs/index.rst.distrib docs/index.rst 8 | -------------------------------------------------------------------------------- /scripts/runserver.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | cd tests/ 3 | server="0.0.0.0" 4 | port="8000" 5 | if [[ $1 == "--port" ]] 6 | then 7 | port="$2" 8 | shift 9 | shift 10 | args="$@" 11 | else 12 | port="8000" 13 | args="$@" 14 | fi 15 | 16 | #./manage.py runserver "$server:$port" --traceback -v 3 "$args" 17 | 18 | if [[ $args ]] 19 | then 20 | ./manage.py runserver "$server:$port" --traceback -v 3 --settings=settings.dev "$args" 21 | else 22 | ./manage.py runserver "$server:$port" --traceback -v 3 --settings=settings.dev "$@" 23 | fi 24 | -------------------------------------------------------------------------------- /scripts/uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | pip uninstall django-mongoengine-filter -y 3 | rm build -rf 4 | rm dist -rf 5 | rm -rf django_mongoengine_filter.egg-info 6 | rm -rf django-mongoengine-filter.egg-info 7 | rm builddocs.zip 8 | rm builddocs/ -rf 9 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | with open("README.rst", "r") as _file: 4 | readme = _file.read() 5 | 6 | install_requires = [ 7 | "six>=1.9", 8 | ] 9 | 10 | extras_require = [] 11 | 12 | tests_require = [ 13 | "factory_boy", 14 | "fake-factory", 15 | "pytest", 16 | "pytest-django", 17 | "pytest-cov", 18 | "tox", 19 | ] 20 | 21 | setup( 22 | name="django-mongoengine-filter", 23 | version="0.4.2", 24 | description=( 25 | "django-mongoengine-filter is a reusable Django application inspired " 26 | "from django-filter for allowing mongoengine users to filter querysets " 27 | "dynamically." 28 | ), 29 | long_description=readme, 30 | keywords="mongoengine, django-filter", 31 | author="Artur Barseghyhan", 32 | author_email="artur.barseghyan@gmail.com", 33 | url="https://github.com/barseghyanartur/django-mongoengine-filter", 34 | project_urls={ 35 | "Bug Tracker": "https://github.com/barseghyanartur/" 36 | "django-mongoengine-filter/issues", 37 | "Documentation": "https://django-mongoengine-filter.readthedocs.io/", 38 | "Source Code": "https://github.com/barseghyanartur/" 39 | "django-mongoengine-filter", 40 | "Changelog": "https://django-mongoengine-filter.readthedocs.io/" 41 | "en/latest/changelog.html", 42 | }, 43 | packages=find_packages(exclude=["tests"]), 44 | package_data={"django_mongoengine_filter": ["locale/*/LC_MESSAGES/*"]}, 45 | license="GPL-2.0-only OR LGPL-2.1-or-later", 46 | classifiers=[ 47 | "Development Status :: 4 - Beta", 48 | "Environment :: Web Environment", 49 | "Intended Audience :: Developers", 50 | "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", 51 | "License :: OSI Approved :: GNU Lesser General Public License v2 or " 52 | "later (LGPLv2+)", 53 | "Operating System :: OS Independent", 54 | "Programming Language :: Python", 55 | "Programming Language :: Python :: 3.7", 56 | "Programming Language :: Python :: 3.8", 57 | "Programming Language :: Python :: 3.9", 58 | "Programming Language :: Python :: 3.10", 59 | "Programming Language :: Python :: 3.11", 60 | "Framework :: Django", 61 | ], 62 | python_requires=">=3.7", 63 | install_requires=(install_requires + extras_require), 64 | tests_require=tests_require, 65 | include_package_data=True, 66 | zip_safe=False, 67 | ) 68 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barseghyanartur/django-mongoengine-filter/cf9a83e47377b8b4276d19f3d71379d5ea1af77b/tests/__init__.py -------------------------------------------------------------------------------- /tests/dfm_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barseghyanartur/django-mongoengine-filter/cf9a83e47377b8b4276d19f3d71379d5ea1af77b/tests/dfm_app/__init__.py -------------------------------------------------------------------------------- /tests/dfm_app/constants.py: -------------------------------------------------------------------------------- 1 | __all__ = ( 2 | "PROFILE_TYPE_FREE", 3 | "PROFILE_TYPE_MEMBER", 4 | "PROFILE_TYPES", 5 | "PROFILE_TYPES_CHOICES", 6 | "GENDER_MALE", 7 | "GENDER_FEMALE", 8 | "GENDERS", 9 | ) 10 | 11 | PROFILE_TYPE_FREE = "free" 12 | PROFILE_TYPE_MEMBER = "member" 13 | PROFILE_TYPES = (PROFILE_TYPE_FREE, PROFILE_TYPE_MEMBER) 14 | PROFILE_TYPES_CHOICES = ( 15 | (PROFILE_TYPE_FREE, "Free"), 16 | (PROFILE_TYPE_MEMBER, "Member"), 17 | ) 18 | 19 | GENDER_MALE = "male" 20 | GENDER_FEMALE = "female" 21 | GENDERS = (GENDER_MALE, GENDER_FEMALE) 22 | -------------------------------------------------------------------------------- /tests/dfm_app/documents.py: -------------------------------------------------------------------------------- 1 | from mongoengine import document, fields 2 | 3 | from .constants import GENDER_MALE, GENDERS, PROFILE_TYPE_FREE, PROFILE_TYPES 4 | 5 | __all__ = ("Person",) 6 | 7 | 8 | class Person(document.Document): 9 | name = fields.StringField( 10 | required=True, max_length=255, default="Robot", verbose_name="Name" 11 | ) 12 | age = fields.IntField(required=True, verbose_name="Age") 13 | num_fingers = fields.IntField( 14 | required=False, verbose_name="Number of fingers" 15 | ) 16 | profile_type = fields.StringField( 17 | required=False, 18 | blank=False, 19 | null=False, 20 | choices=PROFILE_TYPES, 21 | default=PROFILE_TYPE_FREE, 22 | ) 23 | gender = fields.StringField( 24 | required=False, 25 | blank=False, 26 | null=False, 27 | choices=GENDERS, 28 | default=GENDER_MALE, 29 | ) 30 | agnostic = fields.BooleanField(default=True) 31 | 32 | def __str__(self): 33 | return self.name 34 | -------------------------------------------------------------------------------- /tests/dfm_app/filters.py: -------------------------------------------------------------------------------- 1 | import django_mongoengine_filter 2 | 3 | from .documents import Person 4 | 5 | __all__ = ("PersonFilter",) 6 | 7 | 8 | class PersonFilter(django_mongoengine_filter.FilterSet): 9 | profile_type = django_mongoengine_filter.StringFilter() 10 | ten_fingers = django_mongoengine_filter.MethodFilter( 11 | action="ten_fingers_filter" 12 | ) 13 | agnostic = django_mongoengine_filter.BooleanFilter() 14 | # gender = django_mongoengine_filter.StringFilter() 15 | # contract_type = django_mongoengine_filter.StringFilter() 16 | # type = django_mongoengine_filter.StringFilter() 17 | # work_think_level = django_mongoengine_filter.StringFilter() 18 | 19 | class Meta: 20 | model = Person 21 | fields = ["profile_type", "ten_fingers", "agnostic"] 22 | 23 | def ten_fingers_filter(self, queryset, name, value): 24 | if not self.is_valid(): 25 | raise Exception() 26 | if value == "yes": 27 | return queryset.filter(num_fingers=10) 28 | return queryset 29 | -------------------------------------------------------------------------------- /tests/dfm_app/templates/dfm_app/person_list.html: -------------------------------------------------------------------------------- 1 | {#% for obj in filter.qs %#} 2 |
      3 | {% for obj in object_list %} 4 |
    • {{ obj.name }} - {{ obj.age }}
    • 5 | {% endfor %} 6 |
    7 | -------------------------------------------------------------------------------- /tests/dfm_app/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | from django_mongoengine_filter.views import FilterView 4 | 5 | from .documents import Person 6 | from .filters import PersonFilter 7 | 8 | __all__ = ( 9 | "person_list", 10 | "PersonListView", 11 | ) 12 | 13 | 14 | def person_list(request): 15 | """Sample function-based view.""" 16 | filter_obj = PersonFilter(request.GET, queryset=Person.objects) 17 | return render( 18 | request, 19 | "dfm_app/person_list.html", 20 | {"object_list": filter_obj.qs, "count": filter_obj.count()}, 21 | ) 22 | 23 | 24 | class PersonListView(FilterView): 25 | """Sample class-based view.""" 26 | 27 | filterset_class = PersonFilter 28 | template_name = "dfm_app/person_list.html" 29 | -------------------------------------------------------------------------------- /tests/factories/__init__.py: -------------------------------------------------------------------------------- 1 | from .person import * # noqa 2 | -------------------------------------------------------------------------------- /tests/factories/person.py: -------------------------------------------------------------------------------- 1 | import factory 2 | import factory.fuzzy 3 | from dfm_app.constants import GENDERS, PROFILE_TYPES 4 | from dfm_app.documents import Person 5 | 6 | __all__ = ("PersonFactory",) 7 | 8 | 9 | def build_factory(cls, specified_fields=None): 10 | return factory.build( 11 | dict, FACTORY_CLASS=cls, **specified_fields if specified_fields else {} 12 | ) 13 | 14 | 15 | class PersonFactory(factory.mongoengine.MongoEngineFactory): 16 | name = factory.Faker("word") 17 | age = factory.Faker("pyint") 18 | num_fingers = factory.Faker("pyint") 19 | profile_type = factory.fuzzy.FuzzyChoice(choices=PROFILE_TYPES) 20 | gender = factory.fuzzy.FuzzyChoice(choices=GENDERS) 21 | 22 | class Meta: 23 | model = Person 24 | -------------------------------------------------------------------------------- /tests/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | sys.path.insert(0, "..") 7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") 8 | try: 9 | from django.core.management import execute_from_command_line 10 | except ImportError: 11 | # The above import may fail for some other reason. Ensure that the 12 | # issue is really that Django is missing to avoid masking other 13 | # exceptions on Python 2. 14 | try: 15 | import django 16 | except ImportError: 17 | raise ImportError( 18 | "Couldn't import Django. Are you sure it's installed and " 19 | "available on your PYTHONPATH environment variable? Did you " 20 | "forget to activate a virtual environment?" 21 | ) 22 | raise 23 | execute_from_command_line(sys.argv) 24 | -------------------------------------------------------------------------------- /tests/settings/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import * # noqa 2 | -------------------------------------------------------------------------------- /tests/settings/base.py: -------------------------------------------------------------------------------- 1 | DATABASES = { 2 | "default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"} 3 | } 4 | 5 | SECRET_KEY = "top-secret" 6 | 7 | INSTALLED_APPS = [ 8 | "django.contrib.admin", 9 | "django.contrib.auth", 10 | "django.contrib.contenttypes", 11 | "django.contrib.sessions", 12 | "django.contrib.messages", 13 | "django.contrib.staticfiles", 14 | # Third party 15 | "dfm_app", 16 | ] 17 | 18 | MIDDLEWARE = [ 19 | "django.middleware.security.SecurityMiddleware", 20 | "django.contrib.sessions.middleware.SessionMiddleware", 21 | "django.middleware.common.CommonMiddleware", 22 | "django.middleware.csrf.CsrfViewMiddleware", 23 | "django.contrib.auth.middleware.AuthenticationMiddleware", 24 | "django.contrib.messages.middleware.MessageMiddleware", 25 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 26 | ] 27 | 28 | ROOT_URLCONF = "urls" 29 | 30 | 31 | TEMPLATES = [ 32 | { 33 | "BACKEND": "django.template.backends.django.DjangoTemplates", 34 | "DIRS": [], 35 | "APP_DIRS": True, 36 | "OPTIONS": { 37 | "context_processors": [ 38 | "django.template.context_processors.debug", 39 | "django.template.context_processors.request", 40 | "django.contrib.auth.context_processors.auth", 41 | "django.contrib.messages.context_processors.messages", 42 | ] 43 | }, 44 | } 45 | ] 46 | 47 | ALLOWED_HOSTS = ["*", "127.0.0.1", "localhost"] 48 | -------------------------------------------------------------------------------- /tests/settings/dev.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import mongoengine 4 | 5 | from .base import * # noqa 6 | from .helpers import PROJECT_DIR 7 | 8 | mongoengine.connect() 9 | 10 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 11 | 12 | DEBUG = True 13 | 14 | DATABASES = { 15 | "default": { 16 | # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. 17 | "ENGINE": "django.db.backends.sqlite3", 18 | # Or path to database file if using sqlite3. 19 | "NAME": PROJECT_DIR( 20 | os.path.join("..", "..", "var", "db", "example.db") 21 | ), 22 | # The following settings are not used with sqlite3: 23 | "USER": "", 24 | "PASSWORD": "", 25 | # Empty for localhost through domain sockets or '127.0.0.1' for 26 | # localhost through TCP. 27 | "HOST": "", 28 | # Set to empty string for default. 29 | "PORT": "", 30 | } 31 | } 32 | 33 | MEDIA_ROOT = PROJECT_DIR(os.path.join("..", "..", "var", "media")) 34 | 35 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 36 | # trailing slash. 37 | # Examples: "http://example.com/media/", "http://media.example.com/" 38 | MEDIA_URL = "/media/" 39 | 40 | # Absolute path to the directory static files should be collected to. 41 | # Don't put anything in this directory yourself; store your static files 42 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 43 | # Example: "/var/www/example.com/static/" 44 | STATIC_ROOT = PROJECT_DIR(os.path.join("..", "..", "var", "static")) 45 | 46 | # URL prefix for static files. 47 | # Example: "http://example.com/static/", "http://static.example.com/" 48 | STATIC_URL = "/static/" 49 | -------------------------------------------------------------------------------- /tests/settings/helpers.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | __all__ = ( 4 | "gettext", 5 | "project_dir", 6 | "PROJECT_DIR", 7 | ) 8 | 9 | 10 | def project_dir(base): 11 | """Absolute path to a file from current directory.""" 12 | return os.path.abspath( 13 | os.path.join(os.path.dirname(__file__), base).replace("\\", "/") 14 | ) 15 | 16 | 17 | def gettext(val): 18 | """Dummy gettext.""" 19 | return val 20 | 21 | 22 | PROJECT_DIR = project_dir 23 | -------------------------------------------------------------------------------- /tests/settings/test.py: -------------------------------------------------------------------------------- 1 | from .base import * # noqa 2 | -------------------------------------------------------------------------------- /tests/test_filters.py: -------------------------------------------------------------------------------- 1 | from bs4 import BeautifulSoup 2 | from django.test import Client, TestCase 3 | from django.urls import reverse 4 | from faker import Faker 5 | from mongoengine import connect 6 | 7 | from .dfm_app.constants import ( 8 | GENDER_FEMALE, 9 | GENDER_MALE, 10 | PROFILE_TYPE_FREE, 11 | PROFILE_TYPE_MEMBER, 12 | ) 13 | from .factories import PersonFactory 14 | 15 | __all__ = ("FiltersTest",) 16 | 17 | db = connect("test") 18 | db.drop_database("test") 19 | 20 | 21 | class FiltersTest(TestCase): 22 | @classmethod 23 | def setUpClass(cls): 24 | cls.faker = Faker() 25 | cls.client = Client() 26 | cls.person_free_male = PersonFactory( 27 | name="Free male", 28 | profile_type=PROFILE_TYPE_FREE, 29 | gender=GENDER_MALE, 30 | num_fingers=11, 31 | ) 32 | cls.person_free_female = PersonFactory( 33 | name="Free female", 34 | profile_type=PROFILE_TYPE_FREE, 35 | gender=GENDER_FEMALE, 36 | num_fingers=10, 37 | ) 38 | cls.person_member_female = PersonFactory( 39 | name="Member female", 40 | profile_type=PROFILE_TYPE_MEMBER, 41 | gender=GENDER_FEMALE, 42 | num_fingers=10, 43 | ) 44 | cls.url = reverse("person_list") 45 | cls.url_cbv = reverse("person_list_cbv") 46 | super(FiltersTest, cls).setUpClass() 47 | 48 | def _test_base(self, url): 49 | # All 50 | response_all = self.client.get(url) 51 | soup_all = BeautifulSoup( 52 | getattr(response_all, "content", ""), features="html.parser" 53 | ) 54 | self.assertEqual(len(soup_all.find_all("li")), 3) 55 | 56 | # Free male 57 | response_free_male = self.client.get( 58 | "{url}?profile_type={profile_type}&gender={gender}".format( 59 | url=url, profile_type=PROFILE_TYPE_FREE, gender=GENDER_MALE 60 | ) 61 | ) 62 | soup_free_male = BeautifulSoup( 63 | getattr(response_free_male, "content", ""), features="html.parser" 64 | ) 65 | self.assertEqual(len(soup_free_male.find_all("li")), 2) 66 | 67 | # Free female 68 | response_free_female = self.client.get( 69 | "{url}?profile_type={profile_type}&gender={gender}".format( 70 | url=url, profile_type=PROFILE_TYPE_FREE, gender=GENDER_FEMALE 71 | ) 72 | ) 73 | soup_free_female = BeautifulSoup( 74 | getattr(response_free_female, "content", ""), features="html.parser" 75 | ) 76 | self.assertEqual(len(soup_free_female.find_all("li")), 2) 77 | 78 | # Member female 79 | response_member_female = self.client.get( 80 | "{url}?profile_type={profile_type}&gender={gender}".format( 81 | url=url, profile_type=PROFILE_TYPE_MEMBER, gender=GENDER_FEMALE 82 | ) 83 | ) 84 | soup_member_female = BeautifulSoup( 85 | getattr(response_member_female, "content", ""), 86 | features="html.parser", 87 | ) 88 | self.assertEqual(len(soup_member_female.find_all("li")), 1) 89 | 90 | # Custom method 91 | response_ten_fingers = self.client.get( 92 | "{url}?ten_fingers=yes".format(url=url) 93 | ) 94 | soup_ten_fingers = BeautifulSoup( 95 | getattr(response_ten_fingers, "content", ""), features="html.parser" 96 | ) 97 | self.assertEqual(len(soup_ten_fingers.find_all("li")), 2) 98 | 99 | # Agnostic 100 | response_agnostic = self.client.get( 101 | "{url}?agnostic=True".format(url=url) 102 | ) 103 | soup_all = BeautifulSoup( 104 | getattr(response_agnostic, "content", ""), features="html.parser" 105 | ) 106 | self.assertEqual(len(soup_all.find_all("li")), 3) 107 | 108 | # Not Agnostic 109 | response_agnostic = self.client.get( 110 | "{url}?agnostic=False".format(url=url) 111 | ) 112 | soup_all = BeautifulSoup( 113 | getattr(response_agnostic, "content", ""), features="html.parser" 114 | ) 115 | self.assertEqual(len(soup_all.find_all("li")), 0) 116 | 117 | def test_base(self): 118 | return self._test_base(self.url) 119 | 120 | def test_base_cbv(self): 121 | return self._test_base(self.url_cbv) 122 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | from dfm_app.views import PersonListView, person_list 2 | from django.urls import re_path 3 | 4 | urlpatterns = [ 5 | re_path(r"^persons/$", person_list, name="person_list"), 6 | re_path( 7 | r"^persons-cbv/$", PersonListView.as_view(), name="person_list_cbv" 8 | ), 9 | ] 10 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{37,38,39,310}-django{22,30,31}-mg{433} 4 | py{37,38,39,310,311}-django{32}-mg{433} 5 | py{38,39,310,311}-django{40,41}-mg{433} 6 | 7 | [testenv] 8 | envlogdir = var/logs/ 9 | passenv = * 10 | allowlist_externals=* 11 | deps = 12 | django22: -r{toxinidir}/requirements/django_2_2.txt 13 | django30: -r{toxinidir}/requirements/django_3_0.txt 14 | django31: -r{toxinidir}/requirements/django_3_1.txt 15 | django32: -r{toxinidir}/requirements/django_3_2.txt 16 | django40: -r{toxinidir}/requirements/django_4_0.txt 17 | django41: -r{toxinidir}/requirements/django_4_1.txt 18 | mg313: PyMongo==3.13 19 | mg433: PyMongo==4.3.3 20 | 21 | commands = 22 | pip install -e . 23 | {envpython} -m pytest -vrx 24 | 25 | [gh-actions] 26 | python = 27 | 3.7: py37 28 | 3.8: py38 29 | 3.9: py39 30 | 3.10: py310 31 | 3.11: py311 32 | --------------------------------------------------------------------------------