├── .github └── workflows │ └── main.yml ├── .gitignore ├── .idea ├── .gitignore ├── aws.xml ├── django-datatable-view.iml ├── inspectionProfiles │ ├── Project_Default.xml │ └── profiles_settings.xml ├── misc.xml ├── modules.xml ├── vcs.xml └── watcherTasks.xml ├── .pre-commit-config.yaml ├── LICENSE.txt ├── README.md ├── datatableview ├── __init__.py ├── cache.py ├── columns.py ├── datatables.py ├── exceptions.py ├── forms.py ├── helpers.py ├── models.py ├── static │ └── js │ │ ├── datatableview.js │ │ └── datatableview.min.js ├── templates │ └── datatableview │ │ ├── bootstrap_structure.html │ │ ├── default_structure.html │ │ └── legacy_structure.html ├── tests │ ├── __init__.py │ ├── test_columns.py │ ├── test_datatables.py │ ├── test_helpers.py │ ├── test_utils.py │ ├── test_views.py │ └── testcase.py ├── utils.py └── views │ ├── __init__.py │ ├── base.py │ ├── legacy.py │ └── xeditable.py ├── demo_app ├── demo_app │ ├── settings.py │ ├── settings_test.py │ └── urls.py ├── example_app │ ├── __init__.py │ ├── fixtures │ │ └── initial_data.json │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_entry_is_published.py │ │ └── __init__.py │ ├── models.py │ ├── static │ │ ├── datepicker │ │ │ ├── css │ │ │ │ └── datepicker.css │ │ │ └── js │ │ │ │ └── bootstrap-datepicker.js │ │ └── syntaxhighlighter │ │ │ ├── shAutoloader.js │ │ │ ├── shBrushJScript.js │ │ │ ├── shBrushPython.js │ │ │ ├── shBrushXML.js │ │ │ ├── shCore.css │ │ │ ├── shCore.js │ │ │ └── shThemeDefault.css │ ├── templates │ │ ├── 500.html │ │ ├── base.html │ │ ├── blank.html │ │ ├── custom_table_template.html │ │ ├── demos │ │ │ ├── bootstrap_template.html │ │ │ ├── col_reorder.html │ │ │ ├── columns_reference.html │ │ │ ├── configure_datatable_object.html │ │ │ ├── configure_values_datatable_object.html │ │ │ ├── css_styling.html │ │ │ ├── custom_model_fields.html │ │ │ ├── helpers_reference.html │ │ │ ├── multi_filter.html │ │ │ ├── multiple_tables.html │ │ │ ├── select_row.html │ │ │ └── x_editable_columns.html │ │ ├── example_base.html │ │ ├── index.html │ │ ├── javascript_initialization.html │ │ ├── meta │ │ │ ├── columns.html │ │ │ ├── footer.html │ │ │ ├── hidden_columns.html │ │ │ ├── model.html │ │ │ ├── ordering.html │ │ │ ├── page_length.html │ │ │ ├── search_fields.html │ │ │ ├── structure_template.html │ │ │ └── unsortable_columns.html │ │ ├── migration_guide.html │ │ └── valid_column_formats.html │ ├── templatetags │ │ ├── __init__.py │ │ └── example_app_tags.py │ ├── urls.py │ └── views.py ├── manage.py └── test_app │ ├── __init__.py │ ├── fixtures │ └── test_data.json │ └── models.py ├── docs ├── Makefile ├── conf.py ├── datatableview │ ├── columns.rst │ ├── datatables.rst │ ├── forms.rst │ ├── helpers.rst │ ├── index.rst │ └── views.rst ├── index.rst ├── make.bat ├── migration-guide.rst └── topics │ ├── caching.rst │ ├── custom-columns.rst │ ├── index.rst │ ├── interaction-model.rst │ ├── searching.rst │ └── sorting.rst └── pyproject.toml /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Django Datatable View Tests 2 | 3 | on: 4 | push: 5 | schedule: 6 | - cron: '0 1 * * 5' 7 | 8 | concurrency: 9 | group: ${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | outdated: 14 | name: Outdated packages 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-python@v5 19 | with: 20 | python-version: "3.13" 21 | cache: 'pip' 22 | 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install . 27 | pip install .[test] 28 | 29 | - name: outdated 30 | run: pip list --outdated --not-required --user | grep . && echo "There are outdated packages" && exit 1 || echo "All packages up to date" 31 | 32 | black: 33 | name: Black 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v4 37 | - uses: actions/setup-python@v5 38 | with: 39 | python-version: "3.13" 40 | cache: 'pip' 41 | 42 | - name: Install dependencies 43 | run: | 44 | python -m pip install --upgrade pip 45 | pip install . 46 | pip install .[test] 47 | 48 | - name: Black 49 | run: black --check . 50 | 51 | ruff: 52 | name: Ruff 53 | runs-on: ubuntu-latest 54 | steps: 55 | - uses: actions/checkout@v4 56 | - uses: actions/setup-python@v5 57 | with: 58 | python-version: "3.13" 59 | cache: 'pip' 60 | 61 | - name: Install dependencies 62 | run: | 63 | python -m pip install --upgrade pip 64 | pip install . 65 | pip install .[test] 66 | 67 | - name: Ruff 68 | run: ruff check 69 | 70 | pre-commit: 71 | name: Pre-Commit 72 | runs-on: ubuntu-latest 73 | steps: 74 | - uses: actions/checkout@v4 75 | - uses: actions/setup-python@v5 76 | with: 77 | python-version: "3.13" 78 | cache: 'pip' 79 | 80 | - name: Install dependencies 81 | run: | 82 | python -m pip install --upgrade pip 83 | pip install . 84 | pip install .[test] 85 | pre-commit install 86 | 87 | - name: Pre-Commit 88 | run: pre-commit run --all-files --show-diff-on-failure 89 | 90 | security: 91 | name: Bandit Security 92 | runs-on: ubuntu-latest 93 | steps: 94 | - uses: actions/checkout@v4 95 | - uses: actions/setup-python@v5 96 | with: 97 | python-version: "3.13" 98 | cache: 'pip' 99 | 100 | - name: Install dependencies 101 | run: | 102 | pip install . 103 | pip install .[test] 104 | 105 | - name: Bandit 106 | run: bandit -c pyproject.toml -r -f json -o report.json . 107 | 108 | - name: Show report 109 | if: ${{ success() || failure() }} 110 | run: cat report.json 111 | 112 | - name: "Upload Coverage Results" 113 | if: ${{ success() || failure() }} 114 | uses: actions/upload-artifact@v4 115 | with: 116 | name: Bandit Security Report 117 | path: report.json 118 | 119 | tests: 120 | name: Python ${{ matrix.python-version }} / ${{ matrix.db }} / Django ${{ matrix.django-version}} 121 | runs-on: ubuntu-latest 122 | # continue-on-error: ${{ matrix.django-version == '~=5.0' }} 123 | strategy: 124 | max-parallel: 4 125 | matrix: 126 | db: [ sqlite, mariadb ] 127 | django-version: [ "~=5.0" ] 128 | python-version: ["3.12", "3.13" ] 129 | 130 | services: 131 | mariadb: 132 | image: mariadb:latest 133 | env: 134 | MARIADB_ROOT_PASSWORD: password 135 | ports: 136 | - 3306:3306 137 | options: >- 138 | --health-cmd="mariadb-admin ping" 139 | --health-interval=10s 140 | --health-timeout=5s 141 | --health-retries=3 142 | 143 | steps: 144 | - name: Verify MySQL connection from host 145 | if: matrix.db == 'mariadb' 146 | run: | 147 | mysql --host 127.0.0.1 --port 3306 -uroot -ppassword -e "SHOW DATABASES" 2>&1 > /dev/null 148 | mysql --host 127.0.0.1 --port 3306 -uroot -ppassword -V 149 | 150 | - uses: actions/checkout@v4 151 | - name: Set up Python ${{ matrix.python-version }} 152 | uses: actions/setup-python@v5 153 | with: 154 | python-version: ${{ matrix.python-version }} 155 | cache: 'pip' 156 | 157 | - name: Install dependencies 158 | run: | 159 | pip install . 160 | pip install .[test] 161 | pip uninstall -y Django 162 | pip install Django${{ matrix.django-version }} 163 | 164 | - name: Run ${{ matrix.db }} Django ${{ matrix.django-version }} Tests 165 | env: 166 | PYTHONWARNINGS: once::DeprecationWarning 167 | DB_TYPE: ${{ matrix.db }} 168 | run: export PYTHONPATH=`pwd` && coverage run 169 | - name: "Upload Coverage Results for PY:${{ matrix.python-version }} DB:${{ matrix.db}} DJ:${{ matrix.django-version }}" 170 | uses: actions/upload-artifact@v4 171 | with: 172 | include-hidden-files: true 173 | if-no-files-found: warn 174 | name: coverage-${{ matrix.python-version }}-${{ matrix.db}}-${{ matrix.django-version }} 175 | path: .coverage 176 | retention-days: 1 177 | 178 | - name: Django Check 179 | run: python demo_app/manage.py check 180 | 181 | coverage: 182 | name: Upload Coverage to Codecov 183 | needs: [ tests ] 184 | runs-on: ubuntu-latest 185 | steps: 186 | - uses: actions/checkout@v4 187 | - uses: actions/setup-python@v5 188 | with: 189 | python-version: "3.13" 190 | 191 | - name: Install dependencies 192 | run: | 193 | pip install . 194 | pip install .[test] 195 | 196 | - uses: actions/download-artifact@v4 197 | with: 198 | path: . 199 | 200 | - name: Combine Report Coverage 201 | run: | 202 | coverage combine coverage-*/.coverage 203 | coverage report 204 | coverage xml 205 | 206 | - name: Upload coverage to Codecov 207 | uses: codecov/codecov-action@v3 208 | with: 209 | directory: . 210 | token: ${{ secrets.CODECOV_TOKEN }} 211 | fail_ci_if_error: true 212 | 213 | release: 214 | name: Release 215 | if: ${{ github.event_name != 'schedule' }} 216 | runs-on: ubuntu-latest 217 | needs: ['outdated', 'black', 'pre-commit', 'security', 'tests', 'coverage'] 218 | permissions: write-all 219 | outputs: 220 | bumped: ${{ steps.release.outputs.bumped }} 221 | bump_version: ${{ steps.release.outputs.bump_version }} 222 | bump_sha: ${{ steps.release.outputs.bump_sha }} 223 | steps: 224 | - uses: actions/checkout@v4 225 | with: 226 | fetch-depth: 0 227 | - uses: actions/setup-python@v5 228 | with: 229 | python-version: "3.13" 230 | - name: Install dependencies 231 | run: | 232 | pip install git+https://${{ secrets.ORGANIZATIONAL_REPO_TOKEN }}@github.com/pivotal-energy-solutions/tensor-infrastructure@master#egg=infrastructure 233 | - name: Release 234 | id: release 235 | env: 236 | PYTHONWARNINGS: once::DeprecationWarning 237 | GITHUB_TOKEN: ${{ secrets.ORGANIZATIONAL_REPO_TOKEN }} 238 | run: | 239 | bumper -P 240 | echo "bumped=$(jq '.bumped' out.json)" >> $GITHUB_OUTPUT 241 | echo "bump_version=$(jq '.bump_version' out.json)" >> $GITHUB_OUTPUT 242 | echo "bump_sha=$(jq '.bump_sha' out.json)" >> $GITHUB_OUTPUT 243 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # Environments 86 | .env 87 | .venv 88 | env/ 89 | venv/ 90 | ENV/ 91 | env.bak/ 92 | venv.bak/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | 107 | .DS_Store 108 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /.idea/aws.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/django-datatable-view.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 26 | 27 | 33 | 34 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 10 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/watcherTasks.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_language_version: 2 | python: python3.13 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.0.1 6 | hooks: 7 | - id: check-added-large-files 8 | args: [ '--maxkb=500' ] 9 | - id: check-byte-order-marker 10 | exclude: .*\.csv|.*\.xsd|.*\.xlsx|.*\.xml 11 | - id: check-case-conflict 12 | - id: check-merge-conflict 13 | - id: check-symlinks 14 | - id: detect-private-key 15 | - id: end-of-file-fixer 16 | exclude: .idea/.*|.*\.blg|.*\.json|.*\.dat 17 | - id: trailing-whitespace 18 | exclude: .idea/.*|.*\.blg|.*\.json|.*\.dat 19 | - id: mixed-line-ending 20 | exclude: .idea/.* 21 | - id: check-json 22 | - repo: https://github.com/astral-sh/ruff-pre-commit 23 | # Ruff version. 24 | rev: v0.7.3 25 | hooks: 26 | # Run the linter. 27 | - id: ruff 28 | args: [ --fix ] 29 | # Run the formatter. 30 | - id: ruff-format 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Datatable View 2 | 3 | This package is used in conjunction with the jQuery plugin [DataTables](http://datatables.net/), and supports state-saving detection with [fnSetFilteringDelay](http://datatables.net/plug-ins/api). The package consists of a class-based view, and a small collection of utilities for rendering table data from models. 4 | 5 | [![PyPI Downloads][pypi-dl-image]][pypi-dl-link] 6 | [![PyPI Version][pypi-v-image]][pypi-v-link] 7 | [![Build Status][travis-image]][travis-link] 8 | [![Documentation Status][rtfd-image]][rtfd-link] 9 | 10 | [pypi-dl-link]: https://pypi.python.org/pypi/django-datatable-view 11 | [pypi-dl-image]: https://img.shields.io/pypi/dm/django-datatable-view.png 12 | [pypi-v-link]: https://pypi.python.org/pypi/django-datatable-view 13 | [pypi-v-image]: https://img.shields.io/pypi/v/django-datatable-view.png 14 | [travis-link]: https://travis-ci.org/pivotal-energy-solutions/django-datatable-view 15 | [travis-image]: https://travis-ci.org/pivotal-energy-solutions/django-datatable-view.svg?branch=traviscl 16 | [rtfd-link]: http://django-datatable-view.readthedocs.org/en/latest/?badge=latest 17 | [rtfd-image]: https://readthedocs.org/projects/django-datatable-view/badge/?version=latest 18 | 19 | Dependencies: 20 | 21 | * Python 3.8 or later 22 | * [Django](http://www.djangoproject.com/) >= 2.2 23 | * [dateutil](http://labix.org/python-dateutil) library for flexible, fault-tolerant date parsing. 24 | * [jQuery](https://jquery.com/) >= 2 25 | * [dataTables.js](https://datatables.net/) >= 1.10 26 | 27 | # Getting Started 28 | 29 | Install the package: 30 | 31 | ```bash 32 | pip install django-datatable-view 33 | ``` 34 | 35 | Add to ``INSTALLED_APPS`` (so default templates and js can be discovered), and use the ``DatatableView`` like a Django ``ListView``: 36 | 37 | ```python 38 | # settings.py 39 | INSTALLED_APPS = [ 40 | 'datatableview', 41 | # ... 42 | ] 43 | 44 | 45 | # views.py 46 | from datatableview.views import DatatableView 47 | class ZeroConfigurationDatatableView(DatatableView): 48 | model = MyModel 49 | ``` 50 | 51 | Use the ``{{ datatable }}`` provided in the template context to render the table and initialize from server ajax: 52 | 53 | ```html 54 | 55 | 56 | 57 | 60 | 61 | 62 | 63 | 64 | 65 | 70 | 71 | 72 | {{ datatable }} 73 | ``` 74 | 75 | # Features at a glance 76 | 77 | * ``DatatableView``, a drop-in replacement for ``ListView`` that allows options to be specified for the datatable that will be rendered on the page. 78 | * ``MultipleDatatableView`` for configurating multiple Datatable specifications on a single view 79 | * ``ModelForm``-like declarative table design. 80 | * Support for ``ValuesQuerySet`` execution mode instead of object instances 81 | * Queryset caching between requests 82 | * Customizable table headers 83 | * Compound columns (columns representing more than one model field) 84 | * Columns backed by methods or callbacks instead of model fields 85 | * Easy related fields 86 | * Automatic search and sort support 87 | * Total control over cell contents (HTML, processing of raw values) 88 | * Search data fields that aren't present on the table 89 | * Support for DT_RowData 90 | * Customization hook for full JSON response object 91 | * Drop-in x-editable support, per-column 92 | * Customizable table templates 93 | * Easy Bootstrap integration 94 | * Allows all normal dataTables.js and x-editable Javascript options 95 | * Small library of common column markup processors 96 | * Full test suite 97 | 98 | # Documentation and Live Demos 99 | Read the module documentation at http://django-datatable-view.readthedocs.org. 100 | 101 | A public live demo server is in the works. For version 0.8, we will continue to keep the live demo site alive at http://django-datatable-view.appspot.com/ Please note that 0.8 does not reflect the current state or direction of the project. 102 | 103 | You can run the live demos locally from the included example project, using a few common setup steps. 104 | 105 | ```bash 106 | $ git clone https://github.com/pivotal-energy-solutions/django-datatable-view.git 107 | $ cd django-datatable-view 108 | $ mkvirtualenv datatableview 109 | (datatableview)$ pip install -r requirements.txt 110 | (datatableview)$ datatableview/tests/example_project/manage.py migrate 111 | (datatableview)$ datatableview/tests/example_project/manage.py loaddata initial_data 112 | (datatableview)$ datatableview/tests/example_project/manage.py runserver 113 | ``` 114 | 115 | The example project is configured to use a local sqlite3 database, and relies on the ``django-datatable-view`` app itself, which is made available in the python path by simply running the project from the distributed directory root. 116 | 117 | 118 | ## Authors 119 | 120 | * Autumn Valenta 121 | * Steven Klass 122 | 123 | 124 | ## Copyright and license 125 | 126 | Copyright 2011-2023 Pivotal Energy Solutions. All rights reserved. 127 | 128 | Licensed under the Apache License, Version 2.0 (the "License"); 129 | you may not use this work except in compliance with the License. 130 | You may obtain a copy of the License in the LICENSE file, or at: 131 | 132 | http://www.apache.org/licenses/LICENSE-2.0 133 | 134 | Unless required by applicable law or agreed to in writing, software 135 | distributed under the License is distributed on an "AS IS" BASIS, 136 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 137 | See the License for the specific language governing permissions and 138 | limitations under the License. 139 | -------------------------------------------------------------------------------- /datatableview/__init__.py: -------------------------------------------------------------------------------- 1 | from .datatables import Datatable, ValuesDatatable, LegacyDatatable 2 | from .columns import ( 3 | Column, 4 | TextColumn, 5 | DateColumn, 6 | DateTimeColumn, 7 | BooleanColumn, 8 | IntegerColumn, 9 | FloatColumn, 10 | DisplayColumn, 11 | CompoundColumn, 12 | CheckBoxSelectColumn, 13 | ) 14 | from .exceptions import SkipRecord 15 | 16 | __name__ = "datatableview" 17 | __author__ = "Autumn Valenta" 18 | __version_info__ = (2, 1, 34) 19 | __version__ = "2.1.34" 20 | __date__ = "2013/11/14 2:00:00 PM" 21 | __credits__ = ["Autumn Valenta", "Steven Klass"] 22 | __license__ = "See the file LICENSE.txt for licensing information." 23 | 24 | __all__ = [ 25 | "Datatable", 26 | "ValuesDatatable", 27 | "LegacyDatatable", 28 | "Column", 29 | "TextColumn", 30 | "DateColumn", 31 | "DateTimeColumn", 32 | "BooleanColumn", 33 | "IntegerColumn", 34 | "FloatColumn", 35 | "DisplayColumn", 36 | "CompoundColumn", 37 | "CheckBoxSelectColumn", 38 | "SkipRecord", 39 | ] 40 | -------------------------------------------------------------------------------- /datatableview/cache.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import hashlib 3 | import logging 4 | 5 | from django.core.cache import caches 6 | from django.conf import settings 7 | 8 | log = logging.getLogger(__name__) 9 | 10 | 11 | class cache_types(object): 12 | NONE = None 13 | DEFAULT = "default" 14 | SIMPLE = "simple" # Stores queryset objects directly in cache 15 | PK_LIST = "pk_list" # Stores queryset pks in cache for later expansion back to queryset 16 | 17 | 18 | try: 19 | CACHE_BACKEND = settings.DATATABLEVIEW_CACHE_BACKEND 20 | except AttributeError: 21 | CACHE_BACKEND = "default" 22 | 23 | try: 24 | CACHE_PREFIX = settings.DATATABLEVIEW_CACHE_PREFIX 25 | except AttributeError: 26 | CACHE_PREFIX = "datatableview_" 27 | 28 | try: 29 | DEFAULT_CACHE_TYPE = settings.DATATABLEVIEW_DEFAULT_CACHE_TYPE 30 | except AttributeError: 31 | DEFAULT_CACHE_TYPE = cache_types.SIMPLE 32 | 33 | try: 34 | CACHE_KEY_HASH = settings.DATATABLEVIEW_CACHE_KEY_HASH 35 | except AttributeError: 36 | CACHE_KEY_HASH = True 37 | 38 | try: 39 | CACHE_KEY_HASH_LENGTH = settings.DATATABLEVIEW_CACHE_KEY_HASH_LENGTH 40 | except AttributeError: 41 | CACHE_KEY_HASH_LENGTH = None 42 | 43 | cache = caches[CACHE_BACKEND] 44 | 45 | hash_slice = None 46 | if CACHE_KEY_HASH: 47 | hash_slice = slice(None, CACHE_KEY_HASH_LENGTH) 48 | 49 | 50 | def _hash_key_component(s): 51 | return hashlib.sha1(s.encode("utf-8")).hexdigest()[hash_slice] 52 | 53 | 54 | def get_cache_key(datatable_class, view=None, user=None, **kwargs): 55 | """ 56 | Returns a cache key unique to the current table, and (if available) the request user. 57 | 58 | The ``view`` argument should be the class reference itself, since it is easily obtainable 59 | in contexts where the instance is not available. 60 | """ 61 | 62 | datatable_name = datatable_class.__name__ 63 | if datatable_name.endswith("_Synthesized"): 64 | datatable_name = datatable_name[:-12] 65 | datatable_id = "%s.%s" % (datatable_class.__module__, datatable_name) 66 | if CACHE_KEY_HASH: 67 | datatable_id = _hash_key_component(datatable_id) 68 | 69 | cache_key = "datatable_%s" % (datatable_id,) 70 | 71 | if view: 72 | if not inspect.isclass(view): 73 | # Reduce view to its class 74 | view = view.__class__ 75 | 76 | view_id = "%s.%s" % (view.__module__, view.__name__) 77 | if CACHE_KEY_HASH: 78 | view_id = _hash_key_component(view_id) 79 | cache_key += "__view_%s" % (view_id,) 80 | 81 | if user and user.is_authenticated: 82 | cache_key += "__user_%s" % (user.pk,) 83 | 84 | # All other kwargs are used directly to create a hashed suffix 85 | # Order the kwargs by key name, then convert them to their repr() values. 86 | items = sorted(kwargs.items(), key=lambda item: item[0]) 87 | values = [] 88 | for k, v in items: 89 | values.append("%r:%r" % (k, v)) 90 | 91 | if values: 92 | kwargs_id = "__".join(values) 93 | kwargs_id = _hash_key_component(kwargs_id) 94 | cache_key += "__kwargs_%s" % (kwargs_id,) 95 | 96 | log.debug("Cache key derived for %r: %r (from kwargs %r)", datatable_class, cache_key, values) 97 | 98 | return cache_key 99 | 100 | 101 | def get_cached_data(datatable, **kwargs): 102 | """Returns the cached object list under the appropriate key, or None if not set.""" 103 | cache_key = "%s%s" % (CACHE_PREFIX, datatable.get_cache_key(**kwargs)) 104 | data = cache.get(cache_key) 105 | log.debug("Reading data from cache at %r: %r", cache_key, data) 106 | return data 107 | 108 | 109 | def cache_data(datatable, data, **kwargs): 110 | """Stores the object list in the cache under the appropriate key.""" 111 | cache_key = "%s%s" % (CACHE_PREFIX, datatable.get_cache_key(**kwargs)) 112 | log.debug("Setting data to cache at %r: %r", cache_key, data) 113 | cache.set(cache_key, data) 114 | -------------------------------------------------------------------------------- /datatableview/exceptions.py: -------------------------------------------------------------------------------- 1 | class ColumnError(Exception): 2 | """Some kind of problem with a datatable column.""" 3 | 4 | 5 | class SkipRecord(Exception): 6 | """User-raised problem with a record during serialization.""" 7 | -------------------------------------------------------------------------------- /datatableview/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.forms import ValidationError 3 | from django.forms.models import fields_for_model 4 | 5 | 6 | class XEditableUpdateForm(forms.Form): 7 | """ 8 | Represents only a single field of a given model instance. 9 | """ 10 | 11 | # Note that a primary key can be anything at all, not just an integer 12 | pk = forms.CharField(max_length=512) 13 | 14 | # The field name we're editing on the target. 15 | # This isn't normally a great way to track the field name in the request, but we're going need 16 | # to validate the field against the model, so we use the form logic process to force the form 17 | # into failure mode if the field name is bad. 18 | # Displaying field itself should not be required on the frontend, but x-editable.js sends it 19 | # along as part of the editing widget. 20 | name = forms.CharField(max_length=100) 21 | 22 | def __init__(self, model, data, *args, **kwargs): 23 | super(XEditableUpdateForm, self).__init__(data, *args, **kwargs) 24 | 25 | self.model = model 26 | self.set_value_field(model, data.get("name")) 27 | 28 | def set_value_field(self, model, field_name): 29 | """ 30 | Adds a ``value`` field to this form that uses the appropriate formfield for the named target 31 | field. This will help to ensure that the value is correctly validated. 32 | """ 33 | fields = fields_for_model(model, fields=[field_name]) 34 | self.fields["value"] = fields[field_name] 35 | 36 | def clean_name(self): 37 | """Validates that the ``name`` field corresponds to a field on the model.""" 38 | field_name = self.cleaned_data["name"] 39 | # get_all_field_names is deprecated in Django 1.8, this also fixes proxied models 40 | if hasattr(self.model._meta, "get_fields"): 41 | field_names = [field.name for field in self.model._meta.get_fields()] 42 | else: 43 | field_names = self.model._meta.get_all_field_names() 44 | if field_name not in field_names: 45 | raise ValidationError("%r is not a valid field." % field_name) 46 | return field_name 47 | -------------------------------------------------------------------------------- /datatableview/models.py: -------------------------------------------------------------------------------- 1 | # Required for Django to recognize this app 2 | -------------------------------------------------------------------------------- /datatableview/static/js/datatableview.js: -------------------------------------------------------------------------------- 1 | /* For datatable view */ 2 | 3 | var datatableview = (function(){ 4 | var defaultDataTableOptions = { 5 | "serverSide": true, 6 | "paging": true 7 | } 8 | var optionsNameMap = { 9 | 'name': 'name', 10 | 'config-sortable': 'orderable', 11 | 'config-sorting': 'order', 12 | 'config-visible': 'visible', 13 | 'config-searchable': 'searchable' 14 | }; 15 | 16 | var checkGlobalConfirmHook = true; 17 | var autoInitialize = false; 18 | 19 | function initialize($$, opts) { 20 | $$.each(function(){ 21 | var datatable = $(this); 22 | var options = datatableview.getOptions(datatable, opts); 23 | datatable.DataTable(options); 24 | }); 25 | return $$; 26 | } 27 | 28 | function getOptions(datatable, opts) { 29 | /* Reads the options found on the datatable DOM into an object ready to be sent to the 30 | actual DataTable() constructor. Is also responsible for calling the finalizeOptions() 31 | hook to process what is found. 32 | */ 33 | var columnOptions = []; 34 | var sortingOptions = []; 35 | 36 | datatable.find('thead th').each(function(){ 37 | var header = $(this); 38 | var options = {}; 39 | for (var i = 0; i < header[0].attributes.length; i++) { 40 | var attr = header[0].attributes[i]; 41 | if (attr.specified && /^data-/.test(attr.name)) { 42 | var name = attr.name.replace(/^data-/, ''); 43 | var value = attr.value; 44 | 45 | // Typecasting out of string 46 | name = optionsNameMap[name]; 47 | if (/^(true|false)/.test(value.toLowerCase())) { 48 | value = (value === 'true'); 49 | } 50 | 51 | if (name == 'order') { 52 | // This doesn't go in the columnOptions 53 | var sort_info = value.split(','); 54 | sort_info[1] = parseInt(sort_info[1]); 55 | sortingOptions.push(sort_info); 56 | continue; 57 | } 58 | 59 | options[name] = value; 60 | } 61 | } 62 | columnOptions.push(options); 63 | }); 64 | 65 | // Arrange the sorting column requests and strip the priority information 66 | sortingOptions.sort(function(a, b){ return a[0] - b[0] }); 67 | for (var i = 0; i < sortingOptions.length; i++) { 68 | sortingOptions[i] = sortingOptions[i].slice(1); 69 | } 70 | 71 | options = $.extend({}, datatableview.defaults, opts, { 72 | "order": sortingOptions, 73 | "columns": columnOptions, 74 | "pageLength": datatable.attr('data-page-length'), 75 | "infoCallback": function(oSettings, iStart, iEnd, iMax, iTotal, sPre){ 76 | $("#" + datatable.attr('data-result-counter-id')).html(parseInt(iTotal).toLocaleString()); 77 | var infoString; 78 | if (iTotal == 0) { 79 | infoString = oSettings.oLanguage.sInfoEmpty.replace('_START_',iStart).replace('_END_',iEnd).replace('_TOTAL_',iTotal); 80 | } 81 | else { 82 | infoString = oSettings.oLanguage.sInfo.replace('_START_',iStart).replace('_END_',iEnd).replace('_TOTAL_',iTotal); 83 | if (iMax != iTotal) { 84 | infoString += oSettings.oLanguage.sInfoFiltered.replace('_MAX_',iMax); 85 | } 86 | } 87 | 88 | return infoString; 89 | } 90 | }); 91 | options.ajax = $.extend(options.ajax, { 92 | "url": datatable.attr('data-source-url'), 93 | "type": datatable.attr('data-ajax-method') || 'GET', 94 | "beforeSend": function(request){ 95 | request.setRequestHeader("X-CSRFToken", getCookie('csrftoken')); 96 | } 97 | }); 98 | 99 | options = datatableview.finalizeOptions(datatable, options); 100 | return options; 101 | } 102 | 103 | function finalizeOptions(datatable, options) { 104 | /* Hook for processing all options before sent to actual DataTable() constructor. */ 105 | 106 | // Legacy behavior, will be removed in favor of user providing their own finalizeOptions() 107 | if (datatableview.checkGlobalConfirmHook) { 108 | if (window.confirm_datatable_options !== undefined) { 109 | options = window.confirm_datatable_options(options, datatable); 110 | } 111 | } 112 | return options; 113 | } 114 | 115 | function makeXEditable(options) { 116 | var options = $.extend({}, options); 117 | if (!options.ajaxOptions) { 118 | options.ajaxOptions = {} 119 | } 120 | if (!options.ajaxOptions.headers) { 121 | options.ajaxOptions.headers = {} 122 | } 123 | options.ajaxOptions.headers['X-CSRFToken'] = getCookie('csrftoken'); 124 | options.error = function (data) { 125 | var response = data.responseJSON; 126 | if (response.status == 'error') { 127 | var errors = $.map(response.form_errors, function(errors, field){ 128 | return errors.join('\n'); 129 | }); 130 | return errors.join('\n'); 131 | } 132 | }; 133 | return function(nRow, mData, iDisplayIndex) { 134 | $('td a[data-xeditable]', nRow).editable(options); 135 | return nRow; 136 | } 137 | } 138 | 139 | function getCookie(name) { 140 | var cookieValue = null; 141 | if (document.cookie && document.cookie != '') { 142 | var cookies = document.cookie.split(';'); 143 | for (var i = 0; i < cookies.length; i++) { 144 | var cookie = jQuery.trim(cookies[i]); 145 | // Does this cookie string begin with the name we want? 146 | if (cookie.substring(0, name.length + 1) == (name + '=')) { 147 | cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); 148 | break; 149 | } 150 | } 151 | } 152 | return cookieValue; 153 | } 154 | 155 | var api = { 156 | // values 157 | autoInitialize: autoInitialize, 158 | auto_initialize: undefined, // Legacy name 159 | checkGlobalConfirmHook: checkGlobalConfirmHook, 160 | defaults: defaultDataTableOptions, 161 | 162 | // functions 163 | initialize: initialize, 164 | getOptions: getOptions, 165 | finalizeOptions: finalizeOptions, 166 | makeXEditable: makeXEditable, 167 | make_xeditable: makeXEditable // Legacy name 168 | } 169 | return api; 170 | })(); 171 | 172 | $(function(){ 173 | var shouldInit = null; 174 | if (datatableview.auto_initialize === undefined) { 175 | shouldInit = datatableview.autoInitialize; 176 | } else { 177 | shouldInit = datatableview.auto_initialize 178 | } 179 | 180 | if (shouldInit) { 181 | datatableview.initialize($('.datatable')); 182 | } 183 | }); 184 | -------------------------------------------------------------------------------- /datatableview/static/js/datatableview.min.js: -------------------------------------------------------------------------------- 1 | var datatableview=function(){var e={name:"name","config-sortable":"orderable","config-sorting":"order","config-visible":"visible","config-searchable":"searchable"};function t(e){return(e=$.extend({},e)).ajaxOptions||(e.ajaxOptions={}),e.ajaxOptions.headers||(e.ajaxOptions.headers={}),e.ajaxOptions.headers["X-CSRFToken"]=a("csrftoken"),e.error=function(e){var t=e.responseJSON;if("error"==t.status)return $.map(t.form_errors,(function(e,t){return e.join("\n")})).join("\n")},function(t,a,n){return $("td a[data-xeditable]",t).editable(e),t}}function a(e){var t=null;if(document.cookie&&""!=document.cookie)for(var a=document.cookie.split(";"),n=0;n 2 | 3 | 4 | 21 | 22 | 26 | 27 | 28 | {% for column in columns %} 29 | 30 | {% endfor %} 31 | 32 | 33 | {% if config.footer %} 34 | 35 | 36 | {% for column in datatable %} 37 | 38 | {% endfor %} 39 | 40 | 41 | {% endif %} 42 |
{{ column.label }}
{{ column.label }}
43 | -------------------------------------------------------------------------------- /datatableview/templates/datatableview/default_structure.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | {% for column in datatable %} 8 | {{ column }} 9 | {% endfor %} 10 | 11 | 12 | {% if config.footer %} 13 | 14 | 15 | {% for column in datatable %} 16 | 17 | {% endfor %} 18 | 19 | 20 | {% endif %} 21 |
{{ column.label }}
22 | -------------------------------------------------------------------------------- /datatableview/templates/datatableview/legacy_structure.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | {% for name, attributes in column_info %} 8 | 9 | {% endfor %} 10 | 11 | 12 |
{{ name }}
13 | -------------------------------------------------------------------------------- /datatableview/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pivotal-energy-solutions/django-datatable-view/f142b3444e6377bb879f1c46f342e858ae9000eb/datatableview/tests/__init__.py -------------------------------------------------------------------------------- /datatableview/tests/test_columns.py: -------------------------------------------------------------------------------- 1 | from django.apps import apps 2 | from django.core.management import call_command 3 | 4 | from datatableview.columns import Column, COLUMN_CLASSES 5 | from .testcase import DatatableViewTestCase 6 | 7 | ExampleModel = apps.get_model("test_app", "ExampleModel") 8 | 9 | 10 | class ColumnTests(DatatableViewTestCase): 11 | def test_custom_column_registers_itself(self): 12 | previous_length = len(COLUMN_CLASSES) 13 | 14 | class CustomColumn(Column): 15 | model_field_class = "fake" 16 | 17 | self.assertEqual(len(COLUMN_CLASSES), previous_length + 1) 18 | self.assertEqual(COLUMN_CLASSES[0][0], CustomColumn) 19 | self.assertEqual( 20 | COLUMN_CLASSES[0][1], 21 | [CustomColumn.model_field_class] + CustomColumn.handles_field_classes, 22 | ) 23 | 24 | del COLUMN_CLASSES[:1] 25 | 26 | def test_value_is_pair(self): 27 | obj = ExampleModel.objects.create(name="test name 1") 28 | 29 | column = Column() 30 | value = column.value(obj) 31 | self.assertEqual(type(value), tuple) 32 | 33 | # def test_process_value_checks_all_sources(self): 34 | def test_process_value_is_empty_for_fake_source(self): 35 | processed = [] 36 | 37 | def processor(value, **kwargs): 38 | processed.append(value) 39 | 40 | obj = ExampleModel.objects.create(name="test name 1") 41 | 42 | # Verify bad source names don't find values 43 | processed[:] = [] 44 | column = Column(sources=["fake1"], processor=processor) 45 | column.value(obj) 46 | self.assertEqual(processed, []) 47 | 48 | column = Column(sources=["fake1", "fake2"], processor=processor) 49 | column.value(obj) 50 | self.assertEqual(processed, []) 51 | -------------------------------------------------------------------------------- /datatableview/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from django.apps import apps 2 | 3 | from datatableview.columns import Column 4 | from .testcase import DatatableViewTestCase 5 | from datatableview.utils import get_first_orm_bit, resolve_orm_path 6 | 7 | ExampleModel = apps.get_model("test_app", "ExampleModel") 8 | RelatedModel = apps.get_model("test_app", "RelatedModel") 9 | RelatedM2MModel = apps.get_model("test_app", "RelatedM2MModel") 10 | ReverseRelatedModel = apps.get_model("test_app", "ReverseRelatedModel") 11 | 12 | 13 | class UtilsTests(DatatableViewTestCase): 14 | def test_get_first_orm_bit(self): 15 | """ """ 16 | self.assertEqual(get_first_orm_bit(Column(sources=["field"])), "field") 17 | self.assertEqual(get_first_orm_bit(Column(sources=["field__otherfield"])), "field") 18 | 19 | def test_resolve_orm_path_local(self): 20 | """Verifies that references to a local field on a model are returned.""" 21 | field = resolve_orm_path(ExampleModel, "name") 22 | self.assertEqual(field, ExampleModel._meta.get_field("name")) 23 | 24 | def test_resolve_orm_path_fk(self): 25 | """Verify that ExampleModel->RelatedModel.name == RelatedModel.name""" 26 | remote_field = resolve_orm_path(ExampleModel, "related__name") 27 | self.assertEqual(remote_field, RelatedModel._meta.get_field("name")) 28 | 29 | def test_resolve_orm_path_reverse_fk(self): 30 | """Verify that ExampleModel->>>ReverseRelatedModel.name == ReverseRelatedModel.name""" 31 | remote_field = resolve_orm_path(ExampleModel, "reverserelatedmodel__name") 32 | self.assertEqual(remote_field, ReverseRelatedModel._meta.get_field("name")) 33 | 34 | def test_resolve_orm_path_m2m(self): 35 | """Verify that ExampleModel->>>RelatedM2MModel.name == RelatedM2MModel.name""" 36 | remote_field = resolve_orm_path(ExampleModel, "relateds__name") 37 | self.assertEqual(remote_field, RelatedM2MModel._meta.get_field("name")) 38 | -------------------------------------------------------------------------------- /datatableview/tests/test_views.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.urls import reverse 4 | 5 | from example_app.views import ZeroConfigurationDatatableView 6 | from example_app.models import Entry 7 | from example_app.views import ( 8 | BootstrapTemplateDatatableView, 9 | PrettyNamesDatatableView, 10 | SpecificColumnsDatatableView, 11 | CustomizedTemplateDatatableView, 12 | MultipleTablesDatatableView, 13 | SatelliteDatatableView, 14 | ColumnBackedByMethodDatatableView, 15 | CompoundColumnsDatatableView, 16 | HelpersReferenceDatatableView, 17 | DefaultCallbackNamesDatatableView, 18 | ManyToManyFieldsDatatableView, 19 | ) 20 | 21 | from .testcase import DatatableViewTestCase 22 | 23 | 24 | class FakeRequest(object): 25 | def __init__(self, url, method="GET"): 26 | self.path = url 27 | self.method = method 28 | setattr(self, method, {}) 29 | 30 | 31 | class ViewsTests(DatatableViewTestCase): 32 | urls = "datatableview.tests.example_project.example_project.example_app.urls" 33 | 34 | fixtures = ["initial_data.json"] 35 | 36 | def get_json_response(self, url): 37 | response = self.client.get(url, HTTP_X_REQUESTED_WITH="XMLHttpRequest") 38 | content = response.content 39 | content = content.decode() 40 | return json.loads(content) 41 | 42 | def test_zero_configuration_datatable_view(self): 43 | """Verifies that no column definitions means all local fields are used.""" 44 | view = ZeroConfigurationDatatableView 45 | url = reverse("zero-configuration") 46 | view.request = FakeRequest(url) 47 | response = self.client.get(url) 48 | self.assertEqual(len(list(response.context["datatable"])), len(Entry._meta.local_fields)) 49 | 50 | def test_specific_columns_datatable_view(self): 51 | """Verifies that "columns" list matches context object length.""" 52 | view = SpecificColumnsDatatableView() 53 | url = reverse("specific-columns") 54 | view.request = FakeRequest(url) 55 | response = self.client.get(url) 56 | self.assertEqual( 57 | len(list(response.context["datatable"])), len(view.get_datatable().columns) 58 | ) 59 | 60 | def test_pretty_names_datatable_view(self): 61 | """Verifies that a pretty name definition is used instead of the verbose name.""" 62 | view = PrettyNamesDatatableView() 63 | url = reverse("pretty-names") 64 | view.request = FakeRequest(url) 65 | response = self.client.get(url) 66 | self.assertEqual( 67 | len(list(response.context["datatable"])), len(view.get_datatable().columns) 68 | ) 69 | self.assertEqual( 70 | response.context["datatable"].columns["pub_date"].label, "Publication date" 71 | ) 72 | 73 | # def test_x_editable_columns_datatable_view(self): 74 | # view = views.XEditableColumnsDatatableView 75 | # url = reverse('x-editable-columns') 76 | # response = self.client.get(url) 77 | 78 | def test_customized_template_datatable_view(self): 79 | """ 80 | Verify that the custom structure template is getting rendered instead of the default one. 81 | """ 82 | view = CustomizedTemplateDatatableView() 83 | url = reverse("customized-template") 84 | view.request = FakeRequest(url) 85 | response = self.client.get(url) 86 | self.assertContains( 87 | response, """= 2 and isinstance(column[1], list): 80 | column = list(column) 81 | column[1] = tuple(column[1]) 82 | columns[i] = tuple(column) 83 | 84 | return self._datatable_options 85 | 86 | def get_datatable_kwargs(self, **kwargs): 87 | kwargs = super(LegacyDatatableMixin, self).get_datatable_kwargs(**kwargs) 88 | kwargs["callback_target"] = self 89 | kwargs.update(self._get_datatable_options()) 90 | return kwargs 91 | 92 | def preload_record_data(self, obj): 93 | return {} 94 | 95 | def get_extra_record_data(self, obj): 96 | return {} 97 | 98 | 99 | class LegacyDatatableView(LegacyDatatableMixin, ListView): 100 | """ 101 | Implements :py:class:`LegacyDatatableMixin` and the standard Django ``ListView``. 102 | """ 103 | -------------------------------------------------------------------------------- /datatableview/views/xeditable.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | 4 | from ..forms import XEditableUpdateForm 5 | from .base import DatatableView 6 | 7 | from django import get_version 8 | from django.core.exceptions import ValidationError 9 | from django.http import HttpResponse, HttpResponseBadRequest 10 | from django.utils.decorators import method_decorator 11 | from django.views.decorators.csrf import ensure_csrf_cookie 12 | from django.db.models import ForeignKey 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | CAN_UPDATE_FIELDS = get_version().split(".") >= ["1", "5"] 17 | 18 | 19 | class XEditableMixin(object): 20 | xeditable_form_class = XEditableUpdateForm 21 | 22 | xeditable_fieldname_param = "xeditable_field" # GET parameter name used for choices ajax 23 | 24 | @method_decorator(ensure_csrf_cookie) 25 | def dispatch(self, request, *args, **kwargs): 26 | """Introduces the ``ensure_csrf_cookie`` decorator and handles xeditable choices ajax.""" 27 | if request.GET.get(self.xeditable_fieldname_param): 28 | return self.get_ajax_xeditable_choices(request, *args, **kwargs) 29 | return super(XEditableMixin, self).dispatch(request, *args, **kwargs) 30 | 31 | def get_ajax_xeditable_choices(self, request, *args, **kwargs): 32 | """AJAX GET handler for xeditable queries asking for field choice lists.""" 33 | field_name = request.GET.get(self.xeditable_fieldname_param) 34 | if not field_name: 35 | return HttpResponseBadRequest("Field name must be given") 36 | 37 | queryset = self.get_queryset() 38 | if not self.model: 39 | self.model = queryset.model 40 | 41 | # Sanitize the requested field name by limiting valid names to the datatable_options columns 42 | from datatableview.views import legacy 43 | 44 | if isinstance(self, legacy.LegacyDatatableMixin): 45 | columns = self._get_datatable_options()["columns"] 46 | for name in columns: 47 | if isinstance(name, (list, tuple)): 48 | name = name[1] 49 | if name == field_name: 50 | break 51 | else: 52 | return HttpResponseBadRequest("Invalid field name") 53 | else: 54 | datatable = self.get_datatable() 55 | if not hasattr(datatable, "config"): 56 | datatable.configure() 57 | if field_name not in datatable.config["columns"]: 58 | return HttpResponseBadRequest("Invalid field name") 59 | 60 | field = self.model._meta.get_field(field_name) 61 | choices = self.get_field_choices(field, field_name) 62 | return HttpResponse(json.dumps(choices)) 63 | 64 | def post(self, request, *args, **kwargs): 65 | """ 66 | Builds a dynamic form that targets only the field in question, and saves the modification. 67 | """ 68 | self.object_list = None 69 | form = self.get_xeditable_form(self.get_xeditable_form_class()) 70 | if form.is_valid(): 71 | obj = self.get_update_object(form) 72 | if obj is None: 73 | data = json.dumps({"status": "error", "message": "Object does not exist."}) 74 | return HttpResponse(data, content_type="application/json", status=404) 75 | return self.update_object(form, obj) 76 | else: 77 | data = json.dumps( 78 | { 79 | "status": "error", 80 | "message": "Invalid request", 81 | "form_errors": form.errors, 82 | } 83 | ) 84 | return HttpResponse(data, content_type="application/json", status=400) 85 | 86 | def get_xeditable_form_class(self): 87 | """Returns ``self.xeditable_form_class``.""" 88 | return self.xeditable_form_class 89 | 90 | def get_xeditable_form_kwargs(self): 91 | """Returns a dict of keyword arguments to be sent to the xeditable form class.""" 92 | kwargs = { 93 | "model": self.get_queryset().model, 94 | } 95 | if self.request.method in ("POST", "PUT"): 96 | kwargs.update( 97 | { 98 | "data": self.request.POST, 99 | } 100 | ) 101 | return kwargs 102 | 103 | def get_xeditable_form(self, form_class): 104 | """Builds xeditable form computed from :py:meth:`.get_xeditable_form_class`.""" 105 | return form_class(**self.get_xeditable_form_kwargs()) 106 | 107 | def get_update_object(self, form): 108 | """ 109 | Retrieves the target object based on the update form's ``pk`` and the table's queryset. 110 | """ 111 | pk = form.cleaned_data["pk"] 112 | queryset = self.get_queryset() 113 | try: 114 | obj = queryset.get(pk=pk) 115 | except queryset.model.DoesNotExist: 116 | obj = None 117 | 118 | return obj 119 | 120 | def update_object(self, form, obj): 121 | """Saves the new value to the target object.""" 122 | field_name = form.cleaned_data["name"] 123 | value = form.cleaned_data["value"] 124 | 125 | for validator in obj._meta.get_field(field_name).validators: 126 | try: 127 | validator(value) 128 | except ValidationError as e: 129 | data = json.dumps( 130 | { 131 | "status": "error", 132 | "message": "Invalid request", 133 | "form_errors": {field_name: [e.message]}, 134 | } 135 | ) 136 | return HttpResponse(data, content_type="application/json", status=400) 137 | 138 | setattr(obj, field_name, value) 139 | save_kwargs = {} 140 | if CAN_UPDATE_FIELDS: 141 | save_kwargs["update_fields"] = [field_name] 142 | obj.save(**save_kwargs) 143 | 144 | data = json.dumps( 145 | { 146 | "status": "success", 147 | } 148 | ) 149 | return HttpResponse(data, content_type="application/json") 150 | 151 | def get_field_choices(self, field, field_name): 152 | """ 153 | Returns the valid choices for ``field``. The ``field_name`` argument is given for 154 | convenience. 155 | """ 156 | if self.request.GET.get("select2"): 157 | names = ["id", "text"] 158 | else: 159 | names = ["value", "text"] 160 | choices_getter = getattr(self, "get_field_%s_choices", None) 161 | if choices_getter is None: 162 | if isinstance(field, ForeignKey): 163 | choices_getter = self._get_foreignkey_choices 164 | else: 165 | choices_getter = self._get_default_choices 166 | return [dict(zip(names, choice)) for choice in choices_getter(field, field_name)] 167 | 168 | def _get_foreignkey_choices(self, field, field_name): 169 | formfield_kwargs = {} 170 | if not field.blank: 171 | # Explicitly remove empty choice, since formfield isn't working with instance data and 172 | # will consequently try to assume initial=None, forcing the blank option to appear. 173 | formfield_kwargs["empty_label"] = None 174 | formfield = field.formfield(**formfield_kwargs) 175 | 176 | # In case of using Foreignkey limit_choices_to, django prepares model form and handles 177 | # form validation correctly, so does django-datatableview with x-editable plugin. However 178 | # this piece of code helps filtering limited choices to be only visible choices,else 179 | # all the choices are visible. 180 | if formfield.limit_choices_to: 181 | formfield.queryset = formfield.queryset.filter(**formfield.limit_choices_to) 182 | 183 | # return formfield.choices 184 | # formfield choices deconstructed to get ModelChoiceIteratorValue correctly (>= Django 3.1) 185 | # https://docs.djangoproject.com/en/3.2/ref/forms/fields/#django.forms.ModelChoiceIteratorValue.value 186 | return [(i[0].__str__(), i[1]) for i in formfield.choices] 187 | 188 | def _get_default_choices(self, field, field_name): 189 | return field.choices 190 | 191 | 192 | class XEditableDatatableView(XEditableMixin, DatatableView): 193 | pass 194 | -------------------------------------------------------------------------------- /demo_app/demo_app/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for demo_app project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.2.17. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.2/ref/settings/ 11 | """ 12 | 13 | import logging 14 | import os 15 | import sys 16 | 17 | import environ 18 | 19 | env = environ.Env( 20 | DEBUG=(bool, False), 21 | DEBUG_LEVEL=(int, logging.WARNING), 22 | SECRET_KEY=(str, "SECRET_KEY"), 23 | MARIADB_DATABASE=(str, "db"), 24 | MARIADB_USER=(str, "root"), 25 | MARIADB_PASSWORD=(str, "password"), 26 | MARIADB_HOST=(str, "127.0.0.1"), 27 | MARIADB_PORT=(str, "3306"), 28 | ) 29 | 30 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 31 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 32 | 33 | 34 | # Quick-start development settings - unsuitable for production 35 | # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ 36 | 37 | # SECURITY WARNING: keep the secret key used in production secret! 38 | SECRET_KEY = env("SECRET_KEY") 39 | 40 | # SECURITY WARNING: don't run with debug turned on in production! 41 | DEBUG = env("DEBUG") 42 | 43 | ALLOWED_HOSTS = [] 44 | 45 | 46 | # Application definition 47 | 48 | INSTALLED_APPS = [ 49 | "django.contrib.admin", 50 | "django.contrib.auth", 51 | "django.contrib.contenttypes", 52 | "django.contrib.sessions", 53 | "django.contrib.messages", 54 | "django.contrib.staticfiles", 55 | "example_app", 56 | "test_app", 57 | "datatableview", 58 | ] 59 | 60 | MIDDLEWARE = [ 61 | "django.middleware.security.SecurityMiddleware", 62 | "django.contrib.sessions.middleware.SessionMiddleware", 63 | "django.middleware.common.CommonMiddleware", 64 | "django.middleware.csrf.CsrfViewMiddleware", 65 | "django.contrib.auth.middleware.AuthenticationMiddleware", 66 | "django.contrib.messages.middleware.MessageMiddleware", 67 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 68 | ] 69 | 70 | ROOT_URLCONF = "demo_app.urls" 71 | 72 | TEMPLATES = [ 73 | { 74 | "BACKEND": "django.template.backends.django.DjangoTemplates", 75 | "DIRS": [], 76 | "APP_DIRS": True, 77 | "OPTIONS": { 78 | "context_processors": [ 79 | "django.template.context_processors.debug", 80 | "django.template.context_processors.request", 81 | "django.contrib.auth.context_processors.auth", 82 | "django.contrib.messages.context_processors.messages", 83 | ], 84 | }, 85 | }, 86 | ] 87 | 88 | WSGI_APPLICATION = "demo_app.wsgi.application" 89 | 90 | 91 | # Database 92 | # https://docs.djangoproject.com/en/2.2/ref/settings/#databases 93 | 94 | DATABASES = { 95 | "default": { 96 | "ENGINE": "django.db.backends.mysql", 97 | "NAME": env("MARIADB_DATABASE"), 98 | "USER": env("MARIADB_USER"), 99 | "PASSWORD": env("MARIADB_PASSWORD"), 100 | "HOST": env("MARIADB_HOST"), 101 | "PORT": env("DOCKER_MYSQL_PORT", default=env("MARIADB_PORT", default="3306")), 102 | "OPTIONS": {"charset": "utf8mb4"}, 103 | "TEST": { 104 | "MIGRATE": False, 105 | "CHARSET": "utf8mb4", 106 | "COLLATION": "utf8mb4_unicode_520_ci", 107 | }, 108 | } 109 | } 110 | 111 | 112 | # Password validation 113 | # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators 114 | 115 | AUTH_PASSWORD_VALIDATORS = [ 116 | {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"}, 117 | {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, 118 | {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, 119 | {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, 120 | ] 121 | 122 | 123 | # Internationalization 124 | # https://docs.djangoproject.com/en/2.2/topics/i18n/ 125 | 126 | LANGUAGE_CODE = "en-us" 127 | 128 | TIME_ZONE = "UTC" 129 | 130 | USE_I18N = True 131 | 132 | USE_TZ = True 133 | 134 | 135 | # Static files (CSS, JavaScript, Images) 136 | # https://docs.djangoproject.com/en/2.2/howto/static-files/ 137 | 138 | STATIC_URL = "/static/" 139 | 140 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 141 | 142 | LOGGING = { 143 | "version": 1, 144 | "disable_existing_loggers": False, 145 | "formatters": { 146 | "standard": { 147 | "format": "%(asctime)s %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] - %(message)s", 148 | "datefmt": "%H:%M:%S", 149 | }, 150 | }, 151 | "handlers": { 152 | "null": { 153 | "level": "DEBUG", 154 | "class": "logging.NullHandler", 155 | }, 156 | "console": { 157 | "class": "logging.StreamHandler", 158 | "level": "DEBUG", 159 | "formatter": "standard", 160 | "stream": sys.stdout, 161 | }, 162 | }, 163 | "loggers": { 164 | "django": {"handlers": ["console"], "level": "INFO", "propagate": True}, 165 | "django.request": {"handlers": ["console"], "level": "INFO", "propagate": False}, 166 | "django.security": {"handlers": ["console"], "level": "INFO", "propagate": False}, 167 | "django.server": {"handlers": ["console"], "level": "INFO", "propagate": False}, 168 | "django.db.backends": {"handlers": ["console"], "level": "INFO", "propagate": False}, 169 | "django.template": {"handlers": ["console"], "level": "INFO", "propagate": False}, 170 | # 'django_celery_beat': {'handlers': ['console'], 'level': 'WARNING', 'propagate': False}, 171 | # 'celery': {'handlers': ['console'], 'level': 'WARNING', 'propagate': False}, 172 | # 'amqp': {'handlers': ['console'], 'level': 'WARNING', 'propagate': False}, 173 | # 'kombu': {'handlers': ['console'], 'level': 'WARNING', 'propagate': False}, 174 | "requests": {"handlers": ["console"], "level": "WARNING"}, 175 | "multiprocessing": {"handlers": ["console"], "level": "WARNING"}, 176 | "py.warnings": {"handlers": ["console"], "level": "WARNING"}, 177 | "registration": { 178 | "handlers": ["console"], 179 | "level": env("DEBUG_LEVEL", "WARNING"), 180 | "propagate": False, 181 | }, 182 | "demo_app": { 183 | "handlers": ["console"], 184 | "level": env("DEBUG_LEVEL", "ERROR"), 185 | "propagate": False, 186 | }, 187 | "": {"handlers": ["console"], "level": env("DEBUG_LEVEL", "ERROR"), "propagate": True}, 188 | }, 189 | } 190 | -------------------------------------------------------------------------------- /demo_app/demo_app/settings_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import warnings 3 | 4 | from .settings import * # noqa: F403,F401 5 | 6 | # Handle system warning as log messages 7 | warnings.simplefilter("once") 8 | 9 | for handler in LOGGING.get("handlers", []): # noqa: F405 10 | LOGGING["handlers"][handler]["level"] = "CRITICAL" # noqa: F405 11 | for logger in LOGGING.get("loggers", []): # noqa: F405 12 | LOGGING["loggers"][logger]["level"] = "CRITICAL" # noqa: F405 13 | 14 | mysql_db = DATABASES["default"] # noqa: F405 15 | DEFAULT_DB = { 16 | "ENGINE": "django.db.backends.sqlite3", 17 | "NAME": ":memory:", 18 | "TEST": {"MIGRATE": False}, 19 | } 20 | if os.environ.get("DB_TYPE") == "mysql": 21 | print("Using MySQL Backend!") 22 | DEFAULT_DB = mysql_db 23 | 24 | DATABASES = { 25 | "default": DEFAULT_DB, 26 | } 27 | -------------------------------------------------------------------------------- /demo_app/demo_app/urls.py: -------------------------------------------------------------------------------- 1 | """demo_app URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | 17 | from django.contrib import admin 18 | from django.urls import path, include 19 | 20 | 21 | urlpatterns = [path("admin/", admin.site.urls), path("", include("example_app.urls"))] 22 | -------------------------------------------------------------------------------- /demo_app/example_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pivotal-energy-solutions/django-datatable-view/f142b3444e6377bb879f1c46f342e858ae9000eb/demo_app/example_app/__init__.py -------------------------------------------------------------------------------- /demo_app/example_app/fixtures/initial_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "model": "example_app.Blog", "pk": 1, "fields": { 3 | "name": "First Blog", 4 | "tagline": "First and finest" 5 | }}, 6 | { "model": "example_app.Blog", "pk": 2, "fields": { 7 | "name": "Second Blog", 8 | "tagline": "Last but not least" 9 | }}, 10 | 11 | { "model": "example_app.Author", "pk": 1, "fields": { 12 | "name": "Autumn Valenta", 13 | "email": "tvalenta@pivotalenergysolutions.com" 14 | }}, 15 | 16 | { "model": "example_app.Author", "pk": 2, "fields": { 17 | "name": "The Cow", 18 | "email": "moo@aol.com" 19 | }}, 20 | 21 | { "model": "example_app.Entry", "pk": 1, "fields": { 22 | "blog": 1, 23 | "headline": "Hello World", 24 | "body_text": "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", 25 | "pub_date": "2013-01-01", 26 | "mod_date": "2013-01-02", 27 | "n_comments": 2, 28 | "n_pingbacks": 0, 29 | "rating": 0, 30 | "authors": [1], 31 | "status": 1, 32 | "is_published": true 33 | }}, 34 | { "model": "example_app.Entry", "pk": 2, "fields": { 35 | "blog": 1, 36 | "headline": "Headline", 37 | "body_text": "", 38 | "pub_date": "2013-02-01", 39 | "mod_date": "2013-02-02", 40 | "n_comments": 0, 41 | "n_pingbacks": 5, 42 | "rating": 0, 43 | "authors": [2], 44 | "status": 1, 45 | "is_published": true 46 | }}, 47 | { "model": "example_app.Entry", "pk": 3, "fields": { 48 | "blog": 1, 49 | "headline": "Third Headline", 50 | "body_text": "", 51 | "pub_date": "2013-01-01", 52 | "mod_date": "2013-01-02", 53 | "n_comments": 0, 54 | "n_pingbacks": 0, 55 | "rating": 0, 56 | "authors": [1, 2], 57 | "status": 1, 58 | "is_published": true 59 | }}, 60 | { "model": "example_app.Entry", "pk": 4, "fields": { 61 | "blog": 1, 62 | "headline": "Fourth Headline", 63 | "body_text": "", 64 | "pub_date": "2013-01-01", 65 | "mod_date": "2013-01-02", 66 | "n_comments": 273, 67 | "n_pingbacks": 1017, 68 | "rating": 0, 69 | "authors": [1, 2], 70 | "status": 0, 71 | "is_published": false 72 | }}, 73 | { "model": "example_app.Entry", "pk": 5, "fields": { 74 | "blog": 1, 75 | "headline": "Fifth Headline", 76 | "body_text": "", 77 | "pub_date": "2013-01-01", 78 | "mod_date": "2013-01-02", 79 | "n_comments": 0, 80 | "n_pingbacks": 0, 81 | "rating": 0, 82 | "authors": [1, 2], 83 | "status": 0, 84 | "is_published": false 85 | }}, 86 | { "model": "example_app.Entry", "pk": 6, "fields": { 87 | "blog": 2, 88 | "headline": "Sixth Headline", 89 | "body_text": "", 90 | "pub_date": "2013-01-01", 91 | "mod_date": "2013-01-02", 92 | "n_comments": 0, 93 | "n_pingbacks": 0, 94 | "rating": 0, 95 | "authors": [1, 2], 96 | "status": 1, 97 | "is_published": true 98 | }} 99 | ] 100 | -------------------------------------------------------------------------------- /demo_app/example_app/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.2 on 2016-10-05 00:54 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | initial = True 9 | 10 | dependencies = [] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="Author", 15 | fields=[ 16 | ( 17 | "id", 18 | models.AutoField( 19 | auto_created=True, primary_key=True, serialize=False, verbose_name="ID" 20 | ), 21 | ), 22 | ("name", models.CharField(max_length=50)), 23 | ("email", models.EmailField(max_length=254)), 24 | ], 25 | ), 26 | migrations.CreateModel( 27 | name="Blog", 28 | fields=[ 29 | ( 30 | "id", 31 | models.AutoField( 32 | auto_created=True, primary_key=True, serialize=False, verbose_name="ID" 33 | ), 34 | ), 35 | ("name", models.CharField(max_length=100)), 36 | ("tagline", models.TextField()), 37 | ], 38 | ), 39 | migrations.CreateModel( 40 | name="Entry", 41 | fields=[ 42 | ( 43 | "id", 44 | models.AutoField( 45 | auto_created=True, primary_key=True, serialize=False, verbose_name="ID" 46 | ), 47 | ), 48 | ("headline", models.CharField(max_length=255)), 49 | ("body_text", models.TextField()), 50 | ("pub_date", models.DateField()), 51 | ("mod_date", models.DateField()), 52 | ("n_comments", models.IntegerField()), 53 | ("n_pingbacks", models.IntegerField()), 54 | ("rating", models.IntegerField()), 55 | ("status", models.IntegerField(choices=[(0, b"Draft"), (1, b"Published")])), 56 | ("authors", models.ManyToManyField(to="example_app.Author")), 57 | ( 58 | "blog", 59 | models.ForeignKey( 60 | on_delete=django.db.models.deletion.CASCADE, to="example_app.Blog" 61 | ), 62 | ), 63 | ], 64 | ), 65 | ] 66 | -------------------------------------------------------------------------------- /demo_app/example_app/migrations/0002_entry_is_published.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.2 on 2016-10-05 02:02 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("example_app", "0001_initial"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="entry", 14 | name="is_published", 15 | field=models.BooleanField(default=False), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /demo_app/example_app/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pivotal-energy-solutions/django-datatable-view/f142b3444e6377bb879f1c46f342e858ae9000eb/demo_app/example_app/migrations/__init__.py -------------------------------------------------------------------------------- /demo_app/example_app/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Blog(models.Model): 5 | name = models.CharField(max_length=100) 6 | tagline = models.TextField() 7 | 8 | def __str__(self): 9 | return self.name 10 | 11 | def get_absolute_url(self): 12 | return "#blog-{pk}".format(pk=self.pk) 13 | 14 | 15 | class Author(models.Model): 16 | name = models.CharField(max_length=50) 17 | email = models.EmailField() 18 | 19 | def __str__(self): 20 | return self.name 21 | 22 | def get_absolute_url(self): 23 | return "#author-{pk}".format(pk=self.pk) 24 | 25 | 26 | class Entry(models.Model): 27 | blog = models.ForeignKey("Blog", on_delete=models.CASCADE) 28 | headline = models.CharField(max_length=255) 29 | body_text = models.TextField() 30 | pub_date = models.DateField() 31 | mod_date = models.DateField() 32 | authors = models.ManyToManyField(Author) 33 | n_comments = models.IntegerField() 34 | n_pingbacks = models.IntegerField() 35 | rating = models.IntegerField() 36 | status = models.IntegerField( 37 | choices=( 38 | (0, "Draft"), 39 | (1, "Published"), 40 | ) 41 | ) 42 | is_published = models.BooleanField(default=False) 43 | 44 | def __str__(self): 45 | return self.headline 46 | 47 | def get_absolute_url(self): 48 | return "#entry-{pk}".format(pk=self.pk) 49 | 50 | def get_pub_date(self): 51 | return self.pub_date 52 | 53 | def get_interaction_total(self): 54 | return self.n_comments + self.n_pingbacks 55 | -------------------------------------------------------------------------------- /demo_app/example_app/static/syntaxhighlighter/shAutoloader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SyntaxHighlighter 3 | * http://alexgorbatchev.com/SyntaxHighlighter 4 | * 5 | * SyntaxHighlighter is donationware. If you are using it, please donate. 6 | * http://alexgorbatchev.com/SyntaxHighlighter/donate.html 7 | * 8 | * @version 9 | * 3.0.83 (July 02 2010) 10 | * 11 | * @copyright 12 | * Copyright (C) 2004-2010 Alex Gorbatchev. 13 | * 14 | * @license 15 | * Dual licensed under the MIT and GPL licenses. 16 | */ 17 | eval(function(p,a,c,k,e,d){e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('(2(){1 h=5;h.I=2(){2 n(c,a){4(1 d=0;d[:\\w-\\.]+)","xg").exec(code),result=[];if(match.attributes!=null){var attributes,regex=new XRegExp("(? [\\w:\\-\\.]+)"+"\\s*=\\s*"+"(? \".*?\"|'.*?'|\\w+)","xg");while((attributes=regex.exec(code))!=null){result.push(new constructor(attributes.name,match.index+attributes.index,"color1"));result.push(new constructor(attributes.value,match.index+attributes.index+attributes[0].indexOf(attributes.value),"string"))}}if(tag!=null)result.push(new constructor(tag.name,match.index+tag[0].indexOf(tag.name),"keyword"));return result}this.regexList=[{regex:new XRegExp("(\\<|<)\\!\\[[\\w\\s]*?\\[(.|\\s)*?\\]\\](\\>|>)","gm"),css:"color2"},{regex:SyntaxHighlighter.regexLib.xmlComments,css:"comments"},{regex:new XRegExp("(<|<)[\\s\\/\\?]*(\\w+)(?.*?)[\\s\\/\\?]*(>|>)","sg"),func:process}]}Brush.prototype=new SyntaxHighlighter.Highlighter;Brush.aliases=["xml","xhtml","xslt","html"];SyntaxHighlighter.brushes.Xml=Brush;typeof exports!="undefined"?exports.Brush=Brush:null})(); 18 | -------------------------------------------------------------------------------- /demo_app/example_app/static/syntaxhighlighter/shCore.css: -------------------------------------------------------------------------------- 1 | /** 2 | * SyntaxHighlighter 3 | * http://alexgorbatchev.com/SyntaxHighlighter 4 | * 5 | * SyntaxHighlighter is donationware. If you are using it, please donate. 6 | * http://alexgorbatchev.com/SyntaxHighlighter/donate.html 7 | * 8 | * @version 9 | * 3.0.83 (July 02 2010) 10 | * 11 | * @copyright 12 | * Copyright (C) 2004-2010 Alex Gorbatchev. 13 | * 14 | * @license 15 | * Dual licensed under the MIT and GPL licenses. 16 | */ 17 | .syntaxhighlighter a,.syntaxhighlighter div,.syntaxhighlighter code,.syntaxhighlighter table,.syntaxhighlighter table td,.syntaxhighlighter table tr,.syntaxhighlighter table tbody,.syntaxhighlighter table thead,.syntaxhighlighter table caption,.syntaxhighlighter textarea{-moz-border-radius:0 0 0 0 !important;-webkit-border-radius:0 0 0 0 !important;background:none !important;border:0 !important;bottom:auto !important;float:none !important;height:auto !important;left:auto !important;line-height:1.1em !important;margin:0 !important;outline:0 !important;overflow:visible !important;padding:0 !important;position:static !important;right:auto !important;text-align:left !important;top:auto !important;vertical-align:baseline !important;width:auto !important;box-sizing:content-box !important;font-family:"Consolas","Bitstream Vera Sans Mono","Courier New",Courier,monospace !important;font-weight:normal !important;font-style:normal !important;font-size:1em !important;min-height:inherit !important;min-height:auto !important}.syntaxhighlighter{width:100% !important;margin:1em 0 1em 0 !important;position:relative !important;overflow:auto !important;font-size:1em !important}.syntaxhighlighter.source{overflow:hidden !important}.syntaxhighlighter .bold{font-weight:bold !important}.syntaxhighlighter .italic{font-style:italic !important}.syntaxhighlighter .line{white-space:pre !important}.syntaxhighlighter table{width:100% !important}.syntaxhighlighter table caption{text-align:left !important;padding:.5em 0 .5em 1em !important}.syntaxhighlighter table td.code{width:100% !important}.syntaxhighlighter table td.code .container{position:relative !important}.syntaxhighlighter table td.code .container textarea{box-sizing:border-box !important;position:absolute !important;left:0 !important;top:0 !important;width:100% !important;height:100% !important;border:none !important;background:white !important;padding-left:1em !important;overflow:hidden !important;white-space:pre !important}.syntaxhighlighter table td.gutter .line{text-align:right !important;padding:0 .5em 0 1em !important}.syntaxhighlighter table td.code .line{padding:0 1em !important}.syntaxhighlighter.nogutter td.code .container textarea,.syntaxhighlighter.nogutter td.code .line{padding-left:0 !important}.syntaxhighlighter.show{display:block !important}.syntaxhighlighter.collapsed table{display:none !important}.syntaxhighlighter.collapsed .toolbar{padding:.1em .8em 0 .8em !important;font-size:1em !important;position:static !important;width:auto !important;height:auto !important}.syntaxhighlighter.collapsed .toolbar span{display:inline !important;margin-right:1em !important}.syntaxhighlighter.collapsed .toolbar span a{padding:0 !important;display:none !important}.syntaxhighlighter.collapsed .toolbar span a.expandSource{display:inline !important}.syntaxhighlighter .toolbar{position:absolute !important;right:1px !important;top:1px !important;width:11px !important;height:11px !important;font-size:10px !important;z-index:10 !important}.syntaxhighlighter .toolbar span.title{display:inline !important}.syntaxhighlighter .toolbar a{display:block !important;text-align:center !important;text-decoration:none !important;padding-top:1px !important}.syntaxhighlighter .toolbar a.expandSource{display:none !important}.syntaxhighlighter.ie{font-size:.9em !important;padding:1px 0 1px 0 !important}.syntaxhighlighter.ie .toolbar{line-height:8px !important}.syntaxhighlighter.ie .toolbar a{padding-top:0 !important}.syntaxhighlighter.printing .line.alt1 .content,.syntaxhighlighter.printing .line.alt2 .content,.syntaxhighlighter.printing .line.highlighted .number,.syntaxhighlighter.printing .line.highlighted.alt1 .content,.syntaxhighlighter.printing .line.highlighted.alt2 .content{background:none !important}.syntaxhighlighter.printing .line .number{color:#bbb !important}.syntaxhighlighter.printing .line .content{color:black !important}.syntaxhighlighter.printing .toolbar{display:none !important}.syntaxhighlighter.printing a{text-decoration:none !important}.syntaxhighlighter.printing .plain,.syntaxhighlighter.printing .plain a{color:black !important}.syntaxhighlighter.printing .comments,.syntaxhighlighter.printing .comments a{color:#008200 !important}.syntaxhighlighter.printing .string,.syntaxhighlighter.printing .string a{color:blue !important}.syntaxhighlighter.printing .keyword{color:#069 !important;font-weight:bold !important}.syntaxhighlighter.printing .preprocessor{color:gray !important}.syntaxhighlighter.printing .variable{color:#a70 !important}.syntaxhighlighter.printing .value{color:#090 !important}.syntaxhighlighter.printing .functions{color:#ff1493 !important}.syntaxhighlighter.printing .constants{color:#06c !important}.syntaxhighlighter.printing .script{font-weight:bold !important}.syntaxhighlighter.printing .color1,.syntaxhighlighter.printing .color1 a{color:gray !important}.syntaxhighlighter.printing .color2,.syntaxhighlighter.printing .color2 a{color:#ff1493 !important}.syntaxhighlighter.printing .color3,.syntaxhighlighter.printing .color3 a{color:red !important}.syntaxhighlighter.printing .break,.syntaxhighlighter.printing .break a{color:black !important} 18 | -------------------------------------------------------------------------------- /demo_app/example_app/static/syntaxhighlighter/shThemeDefault.css: -------------------------------------------------------------------------------- 1 | /** 2 | * SyntaxHighlighter 3 | * http://alexgorbatchev.com/SyntaxHighlighter 4 | * 5 | * SyntaxHighlighter is donationware. If you are using it, please donate. 6 | * http://alexgorbatchev.com/SyntaxHighlighter/donate.html 7 | * 8 | * @version 9 | * 3.0.83 (July 02 2010) 10 | * 11 | * @copyright 12 | * Copyright (C) 2004-2010 Alex Gorbatchev. 13 | * 14 | * @license 15 | * Dual licensed under the MIT and GPL licenses. 16 | */ 17 | .syntaxhighlighter{background-color:white !important}.syntaxhighlighter .line.alt1{background-color:white !important}.syntaxhighlighter .line.alt2{background-color:white !important}.syntaxhighlighter .line.highlighted.alt1,.syntaxhighlighter .line.highlighted.alt2{background-color:#e0e0e0 !important}.syntaxhighlighter .line.highlighted.number{color:black !important}.syntaxhighlighter table caption{color:black !important}.syntaxhighlighter .gutter{color:#afafaf !important}.syntaxhighlighter .gutter .line{border-right:3px solid #6ce26c !important}.syntaxhighlighter .gutter .line.highlighted{background-color:#6ce26c !important;color:white !important}.syntaxhighlighter.printing .line .content{border:none !important}.syntaxhighlighter.collapsed{overflow:visible !important}.syntaxhighlighter.collapsed .toolbar{color:blue !important;background:white !important;border:1px solid #6ce26c !important}.syntaxhighlighter.collapsed .toolbar a{color:blue !important}.syntaxhighlighter.collapsed .toolbar a:hover{color:red !important}.syntaxhighlighter .toolbar{color:white !important;background:#6ce26c !important;border:none !important}.syntaxhighlighter .toolbar a{color:white !important}.syntaxhighlighter .toolbar a:hover{color:black !important}.syntaxhighlighter .plain,.syntaxhighlighter .plain a{color:black !important}.syntaxhighlighter .comments,.syntaxhighlighter .comments a{color:#008200 !important}.syntaxhighlighter .string,.syntaxhighlighter .string a{color:blue !important}.syntaxhighlighter .keyword{color:#069 !important}.syntaxhighlighter .preprocessor{color:gray !important}.syntaxhighlighter .variable{color:#a70 !important}.syntaxhighlighter .value{color:#090 !important}.syntaxhighlighter .functions{color:#ff1493 !important}.syntaxhighlighter .constants{color:#06c !important}.syntaxhighlighter .script{font-weight:bold !important;color:#069 !important;background-color:none !important}.syntaxhighlighter .color1,.syntaxhighlighter .color1 a{color:gray !important}.syntaxhighlighter .color2,.syntaxhighlighter .color2 a{color:#ff1493 !important}.syntaxhighlighter .color3,.syntaxhighlighter .color3 a{color:red !important}.syntaxhighlighter .keyword{font-weight:bold !important} 18 | -------------------------------------------------------------------------------- /demo_app/example_app/templates/500.html: -------------------------------------------------------------------------------- 1 |

2 | Uh oh. Server error on our end. Please please please visit our GitHub issues page and open a new issue with some basic information about what you tried to do, what kind of data you entered, etc. 3 |

4 | 5 |

6 | Thanks and sorry — django-datatable-view developers 7 |

8 | -------------------------------------------------------------------------------- /demo_app/example_app/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load example_app_tags %} 2 | 3 | 4 | 5 | {% block title %}{% endblock title %} 6 | {% block static %} 7 | {# jQuery #} 8 | 9 | 10 | {# datatables.js #} 11 | 12 | 13 | 14 | {# Bootstrap #} 15 | 16 | 17 | 18 | {# django-datatable-view #} 19 | 20 | 23 | 24 | {# code highlighting #} 25 | 26 | 27 | 28 | 29 | 30 | 31 | {# test_project helpers #} 32 | 47 | 54 | {% endblock static %} 55 | 56 | 57 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /demo_app/example_app/templates/blank.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pivotal-energy-solutions/django-datatable-view/f142b3444e6377bb879f1c46f342e858ae9000eb/demo_app/example_app/templates/blank.html -------------------------------------------------------------------------------- /demo_app/example_app/templates/custom_table_template.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | {% for column in columns %} 5 | 6 | {% endfor %} 7 | 8 | 9 |
{{ column.label }}
10 | -------------------------------------------------------------------------------- /demo_app/example_app/templates/demos/bootstrap_template.html: -------------------------------------------------------------------------------- 1 | {% extends "example_base.html" %} 2 | 3 | {% block static %} 4 | {{ block.super }} 5 | 6 | 7 | 8 | 9 | 10 | {% endblock static %} 11 | -------------------------------------------------------------------------------- /demo_app/example_app/templates/demos/col_reorder.html: -------------------------------------------------------------------------------- 1 | {% extends "example_base.html" %} 2 | 3 | {% block static %} 4 | {{ block.super }} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 20 | {% endblock static %} 21 | 22 | 23 | {% block implementation %} 24 |
25 |     datatableview.auto_initialize = false;
26 |     $(function(){
27 |         datatableview.initialize($('.datatable'), {
28 |             'sDom': 'Rlfrtip'
29 |         });
30 |     });
31 | 
32 | {% endblock implementation %} 33 | -------------------------------------------------------------------------------- /demo_app/example_app/templates/demos/columns_reference.html: -------------------------------------------------------------------------------- 1 | {% extends "example_base.html" %} 2 | 3 | {% block content %} 4 |

Description

5 | {% block description %} 6 | {{ block.super }} 7 | {% endblock description %} 8 | 9 | 10 |

11 |

Example

12 |
13 | from datatableview import columns
14 | from datatableview.datatables import Datatable
15 | from myproject.fields import CustomField
16 | 
17 | class CustomColumn(columns.Column):
18 |     model_field_class = CustomField
19 |     lookup_types = ['icontains', 'mycustomquery']
20 | 
21 | 
22 | # You can let the field detect the new column by itself:
23 | class MyDatatable(Datatable):
24 |     class Meta:
25 |         columns = ['my_customfield']
26 | 
27 | # Or you can always use it explicitly:
28 | class MyDatatable(Datatable):
29 |     customcolumn = CustomColumn("My Field", sources=['my_customfield')
30 |     class Meta:
31 |         columns = ['customcolumn']
32 | 
33 | 
34 | {% endblock content %} 35 | -------------------------------------------------------------------------------- /demo_app/example_app/templates/demos/configure_datatable_object.html: -------------------------------------------------------------------------------- 1 | {% extends "example_base.html" %} 2 | 3 | {% block static %} 4 | {{ block.super }} 5 | 6 | 11 | {% endblock static %} 12 | 13 | {% block description %} 14 | {{ block.super }} 15 | 16 |

17 | The full list of allowed Meta attributes follows, all of which are technically 18 | optional: 19 |

20 |
21 |
22 | 33 |
34 |
35 | 36 |

{% include "meta/model.html" %} 37 |

{% include "meta/columns.html" %} 38 |

{% include "meta/ordering.html" %} 39 |

{% include "meta/page_length.html" %} 40 |

{% include "meta/search_fields.html" %} 41 |

{% include "meta/unsortable_columns.html" %} 42 |

{% include "meta/hidden_columns.html" %} 43 |

{% include "meta/footer.html" %} 44 |

{% include "meta/structure_template.html" %} 45 | {% endblock description %} 46 | -------------------------------------------------------------------------------- /demo_app/example_app/templates/demos/configure_values_datatable_object.html: -------------------------------------------------------------------------------- 1 | {% extends "example_base.html" %} 2 | 3 | {% block description %} 4 | {{ block.super }} 5 | 6 |
 7 | {
 8 |     'pk': 6,  # ValuesDatatable requires automatic selection of 'pk'
 9 |     'id': 6,  # This happens to be the 'pk' field, so it found its way into the object
10 | 
11 |     # The full set of ORM names taken from the various ``column.sources`` lists.
12 |     # Simple columns (where the names matched the orm query path) require no modification,
13 |     # but notice the "blog__id" and "blog__name" entries that came from a single column
14 |     # declaration.
15 |     'headline': u'',
16 |     'n_comments': 0,
17 |     'n_pingbacks': 0,
18 |     'pub_date': datetime.date(2013, 1, 1),
19 |     'blog__id': 2,
20 |     'blog__name': u'Second Blog',
21 | 
22 |     # Aliases have been given to the object for column names that didn't match the actual
23 |     # ``sources`` names.  Note how 'blog' is a list, because it had multiple sources.
24 |     'publication_date': datetime.date(2013, 1, 1),
25 |     'blog': [2, u'Second Blog'],
26 | 
27 | }
28 | 
29 | 30 | {% endblock description %} 31 | -------------------------------------------------------------------------------- /demo_app/example_app/templates/demos/css_styling.html: -------------------------------------------------------------------------------- 1 | {% extends "example_base.html" %} 2 | 3 | {% block static %} 4 | {{ block.super }} 5 | 6 | 7 | 8 | 13 | {% endblock static %} 14 | 15 | {% block description %} 16 | {{ block.super }} 17 |
18 | <style type="text/css">
19 | .datatable th[data-name="publication-date"] {
20 |     width: 8%;
21 | }
22 | </style>
23 |     
24 | {% endblock description %} 25 | -------------------------------------------------------------------------------- /demo_app/example_app/templates/demos/custom_model_fields.html: -------------------------------------------------------------------------------- 1 | {% extends "example_base.html" %} 2 | 3 | {% block content %} 4 |

Simple fields

5 |

6 | If a custom model field subclasses an existing Django field, that field will automatically 7 | be handled by your datatable's search function, as long as there are no custom query types 8 | that you want to support. However, if you have a field that does not subclass a built-in 9 | field, or does but supports additional query types, you must register that model field so 10 | that it can be correctly consulted for data prep and querying. 11 |

12 |

13 | For fields don't inherit from a Django field, don't offer special query types, and can 14 | generally be treated like a built-in field such as CharField or 15 | IntegerField, you can use a shorthand registration utility: 16 |

17 | 18 |
19 |     # Somewhere in your project, such as models.py
20 |     # This should happen globally so that it happens just once,
21 |     # not every time a request occurs.
22 | 
23 |     from datatableview.columns import register_simple_modelfield
24 |     register_simple_modelfield(MyCustomModelField)
25 |     
26 | 27 |

Supporting custom ORM query types

28 |

29 | Custom fields with support for non-standard query types need to declare a special 30 | Column class to detail the relevant information. If the field supports a 31 | query lookup called '__customquery', you could declare the column like this: 32 |

33 |
34 |     # This should happen somewhere in your project, such as columns.py,
35 |     # but you should make sure that the file is imported somewhere,
36 |     # such as in views.py.
37 | 
38 |     from datatableview.columns import Column
39 |     class MyCustomColumn(Column):
40 |         model_field_class = MyCustomFieldClass
41 |         lookup_types = ['customquery']
42 |     
43 |

44 | Columns declared like this are automatically registered with metaclass magic, placed at the 45 | top priority position of the datatableview.columns.COLUMN_CLASSES list, until 46 | another column is declared and takes the top-priority position. This allows you to override 47 | the built-in support for existing columns if you wish. 48 |

49 |

50 | Note that you should include ALL of the lookup types you want to support on a custom 51 | column, not just the new lookup types. Lookups should be chosen based on what you think 52 | would be intuitive. In text columns, icontains is chosen because partial 53 | matches are expected, and iexact isn't very useful because paired with 54 | icontains it doesn't find any additional results and yet by itself it's too 55 | inflexible. 56 |

57 | {% endblock content %} 58 | -------------------------------------------------------------------------------- /demo_app/example_app/templates/demos/helpers_reference.html: -------------------------------------------------------------------------------- 1 | {% extends "example_base.html" %} 2 | 3 | {% block static %} 4 | {{ block.super }} 5 | 6 | 7 | 8 | 9 | 10 | 20 | {% endblock static %} 21 | 22 | {% block description %} 23 | {{ block.super }} 24 | 25 | See the 26 | 27 | module documentation for helpers for more information. 28 | 29 | {% endblock description %} 30 | -------------------------------------------------------------------------------- /demo_app/example_app/templates/demos/multi_filter.html: -------------------------------------------------------------------------------- 1 | {% extends "example_base.html" %} 2 | 3 | {% block static %} 4 | {{ block.super }} 5 | 6 | 7 | 8 | 27 | {% endblock static %} 28 | 29 | 30 | {% block implementation %} 31 |
32 |     datatableview.auto_initialize = false;
33 |     $(function(){
34 |         var datatable = datatableview.initialize($('.datatable'), {});
35 |         $('.datatable tfoot th').each(function(){
36 |             var title = $('.datatable thead th').eq($(this).index()).text();
37 |             $(this).html('');
38 |         });
39 |         var table = datatable.api();
40 |         table.columns().every(function(){
41 |             var column = this;
42 |             $('input', column.footer()).on('keyup change', function(){
43 |                 if (column.search() !== this.value) {
44 |                     column.search(this.value).draw();
45 |                 }
46 |             })
47 |         })
48 |     });
49 | 
50 | {% endblock implementation %} 51 | -------------------------------------------------------------------------------- /demo_app/example_app/templates/demos/multiple_tables.html: -------------------------------------------------------------------------------- 1 | {% extends "example_base.html" %} 2 | 3 | {% block demo %} 4 |

Demo #1: Default table from class-level options

5 | {{ demo1_datatable }} 6 | 7 |

8 |

Demo #2: Copy of #1's options, but modified the columns list

9 | {{ demo2_datatable }} 10 | 11 |

12 |

Demo #3: Completely separate model Blog and corresponding options

13 | {{ demo3_datatable }} 14 | {% endblock demo %} 15 | -------------------------------------------------------------------------------- /demo_app/example_app/templates/demos/select_row.html: -------------------------------------------------------------------------------- 1 | {% extends "example_base.html" %} 2 | 3 | {% block static %} 4 | {{ block.super }} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 | 45 | {% endblock static %} 46 | 47 | 48 | {% block implementation %} 49 |
50 |     datatableview.finalizeOptions = (function(){
51 |             var super_finalizeOptions = datatableview.finalizeOptions;
52 |             return function _confirm_datatable_options(datatable, options){
53 |                 /**
54 |                  * Search our chechbox column by `data-idall` and add select plugin options to our checkboxes
55 |                  * Also we need to set class 'select-checkbox' for our  element.
56 |                  */
57 |                 if (options['columns'] &&
58 |                     options['columns'].length) {
59 |                     for (var i=0; i < options['columns'].length; i++) {
60 | 
61 |                         if (options['columns'][i]['name'] &&
62 |                             options['columns'][i]['name'].indexOf('data-idall') !== -1) {
63 |                             options.select = {
64 |                                 style: 'multi',
65 |                                 selector: 'td:first-child'
66 |                             };
67 | 
68 |                             options['columns'][i]['sClass'] = 'select-checkbox'
69 |                         }
70 |                     }
71 |                 }
72 |                 options = super_finalizeOptions(datatable, options);
73 |                 return options
74 |             }
75 |         })();
76 | 
77 | 
78 |     /*
79 |         For example to get selected rows use:
80 |     */
81 |         var datatable = document.querySelector('#DataTables_Table_0')[0].DataTable();
82 |         var selecteditems = [];
83 |         datatable.on('select', function (e, dt, type, indexes) {
84 |                 selecteditems = dt.rows({selected: true}).ids().toArray();
85 |             }).on( 'deselect', function ( e, dt, type, indexes ) {
86 |                 selecteditems = dt.rows({selected: true}).ids().toArray();
87 |             });
88 | 
89 | 
90 | {% endblock implementation %} 91 | -------------------------------------------------------------------------------- /demo_app/example_app/templates/demos/x_editable_columns.html: -------------------------------------------------------------------------------- 1 | {% extends "example_base.html" %} 2 | 3 | {% block static %} 4 | {{ block.super }} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 24 | {% endblock static %} 25 | -------------------------------------------------------------------------------- /demo_app/example_app/templates/example_base.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |

Live demo

5 | {% block demo %} 6 | {{ datatable }} 7 | {% endblock demo %} 8 | 9 | {% if implementation %} 10 |

11 |

Implementation

12 | {% block implementation %} 13 |
14 |         {{ implementation|safe }}
15 |         
16 | {% endblock implementation %} 17 | {% endif %} 18 | 19 |

20 |

Description

21 | {% block description %} 22 | {{ description|safe|linebreaks }} 23 | {% endblock description %} 24 | 25 |

26 | {% endblock content %} 27 | -------------------------------------------------------------------------------- /demo_app/example_app/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load example_app_tags %} 3 | 4 | {% block title %} 5 | Example index 6 | {% endblock title %} 7 | 8 | {% block header %} 9 | Example index 10 | {% endblock header %} 11 | 12 | {% block content %} 13 |

django-datatable-view example project

14 |

15 | Running 16 | django-datatable-view {{ datatableview_version }}, 17 | django {{ django_version }} 18 | datatables.js {{ datatables_version }} 19 | 20 |

21 | 22 |

23 | Click on an example type in the sidebar to interact with a live table and read details on 24 | how it can be implemented. 25 |

26 | 27 |

Database status

28 | {% if db_works %} 29 |

30 | Your database tables appear migrated! Check out the example pages at your leisure. 31 |

32 | {% else %} 33 |

34 | Make sure you've done a migrate for the test project! This view failed to 35 | read from one of the example model tables! 36 |

37 |

38 | This project's settings use a simple sqlite3 file to keep things tidy for you, so we 39 | recommend using this project in its own environment. Ideally, you could run the 40 | manage.py script from inside the root of the django-datatable-view project 41 | directory: 42 |

43 |
$ cd django-datatable-view
 44 | $ datatableview{{ os_sep }}test{{ os_sep }}test_project{{ os_sep }}manage.py syncdb
 45 | $ datatableview{{ os_sep }}test{{ os_sep }}test_project{{ os_sep }}manage.py loaddata initial_data
 46 | $ datatableview{{ os_sep }}test{{ os_sep }}test_project{{ os_sep }}manage.py runserver
 47 | 
48 | {% endif %} 49 | 50 |

Main topics

51 |

52 | The demos and example code found in this example project all rely on the automatic 53 | initialization provided by a simple jQuery document.ready event. A couple of 54 | the pages use additional static resources (Bootstrap theme, x-editable); these pages include 55 | an HTML comment in the source that highlights their use in the live demo. 56 |

57 | 58 |

59 | Aside from the primary value of this set of examples as a reference site, noteworthy topics 60 | that branch outside of basic syntax are: 61 |

62 | 68 | 69 |

Models used in the examples

70 |

71 | The main model used in most of the examples is Entry, because of its diverse 72 | fields. Blog makes a simple relationship demonstration possible, and 73 | Author helps shed light on how to handle 74 | many to many relationships. 75 |

76 |
 77 | class Blog(models.Model):
 78 |     name = models.CharField(max_length=100)
 79 |     tagline = models.TextField()
 80 | 
 81 | class Author(models.Model):
 82 |     name = models.CharField(max_length=50)
 83 |     email = models.EmailField()
 84 | 
 85 | class Entry(models.Model):
 86 |     blog = models.ForeignKey(Blog)
 87 |     headline = models.CharField(max_length=255)
 88 |     body_text = models.TextField()
 89 |     pub_date = models.DateField()
 90 |     mod_date = models.DateField()
 91 |     authors = models.ManyToManyField(Author)
 92 |     n_comments = models.IntegerField()
 93 |     n_pingbacks = models.IntegerField()
 94 |     rating = models.IntegerField()
 95 |     status = models.IntegerField(choices=(
 96 |         (0, "Draft"),
 97 |         (1, "Published"),
 98 |     ))
 99 | 
100 | 101 | {% endblock content %} 102 | -------------------------------------------------------------------------------- /demo_app/example_app/templates/javascript_initialization.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block static %} 4 | {{ block.super }} 5 | 6 | 7 | {% endblock static %} 8 | 9 | {% block content %} 10 |

Initialization

11 |

12 | The easiest way to get going is to include the datatableview.js (or 13 | datatableview.min.js) file bundled with the app: 14 |

15 |
16 | <script type="text/javascript" charset="utf8" src="{% templatetag openvariable %} STATIC_URL {% templatetag closevariable %}js/datatableview.js"></script>
17 |     
18 | 19 |

20 | This file introduces a global ``datatableview`` object that wraps the provided 21 | initialization function and internal and public utilities. 22 |

23 | 24 |

25 | To easily initialize datatables in Javascript, you can select your elements and send them 26 | to datatableview.initialize(): 27 |

28 | 29 |
30 | $(function(){
31 |     datatableview.initialize($('.mytable'));
32 | 
33 |     // Or, if there are common options that should be given to all select elements,
34 |     // you can specify them now.  data-* API attributes on the table columns will potentially
35 |     // override the individual options.
36 |     var common_options = {};
37 |     datatableview.initialize($('.mytable'), common_options);
38 | });
39 |     
40 | 41 |

Automatic no-configuration Initialization

42 |

43 | Before version 0.9, automatic initialzation was on by default. Although it now defaults to 44 | off, you can enable it by changing the setting on the global datatableview 45 | javascript variable: 46 |

47 | 48 |
49 | datatableview.auto_initialize = true;
50 |     
51 | 52 |

53 | You can do this on a per-page basis, or make this part of your site's global javascript 54 | configuration. Make sure you set the flag before the jQuery(document).ready() 55 | handler is finished. 56 |

57 | 58 |

59 | Automatic initialization doesn't allow for easy per-table javascript configuration. To work 60 | around this quirk, you can use the now-deprecated global javascript 61 | function hook confirm_datatable_options(options, datatable), which is run 62 | against every table that initialized (the value of 63 | datatableview.auto_initialization doesn't matter!) in order to provide an 64 | opportunity to modify the options about to be sent to the underlying call to the real 65 | $(table).dataTable(options). 66 |

67 | 68 |

69 | Make sure you return the options object in the global hook! 70 |

71 | 72 |

73 | An example of how to declare this global hook follows: 74 |

75 | 76 |
77 | function confirm_datatable_options(options, datatable) {
78 |     // "datatable" variable is the jQuery object, not the oTable, since it
79 |     // hasn't been initialized yet.
80 |     options.fnRowCallback = function(...){ ... };
81 |     return options;
82 | }
83 |     
84 | 85 |

86 | Note that this function hook is not required for the automatic initialization to 87 | work. 88 |

89 | {% endblock content %} 90 | -------------------------------------------------------------------------------- /demo_app/example_app/templates/meta/columns.html: -------------------------------------------------------------------------------- 1 |

2 | 3 | columns 4 | 5 |

6 | 7 |

8 | A list of model fields that you wish to import into the table as columns, separate from any 9 | columns that are declared on the Datatable itself. 10 |

11 | 12 |

13 | In version 0.8 and earlier, an item in this list could name a related column or a model method 14 | and a column would be generated to handle the value. In 0.9 and later, this is no longer 15 | allowed. The Datatable configuration object in 0.9 needs an explicit name for 16 | every column, and specifying a related field such as "blog__name" starts mixing the 17 | concepts of column names and model fields. The columns list is for column names, 18 | not arbitrary ORM paths. To specify a related field, please see 19 | Related and virtual field columns. 20 |

21 | 22 |

23 | For legacy reasons, the Meta.columns setting can still be specified in the 24 | full-blown format that existed before version 0.9, but when 1.0 is released, this behavior will 25 | be removed. 26 |

27 | -------------------------------------------------------------------------------- /demo_app/example_app/templates/meta/footer.html: -------------------------------------------------------------------------------- 1 |

2 | 3 | footer 4 | 5 |

6 | 7 |

8 | Setting footer to True will allow the default datatable rendering 9 | templates to show a simple <tfoot> in the table that shares the same labels 10 | that the header uses. 11 |

12 |

13 | On its own, this feature is available for added clarity when scrolling down a long table, but it 14 | can be used to set up more advanced features, such as adding column-specific search boxes for 15 | per-column searching (described by the official 16 | dataTables.js documentation 17 | here). 18 |

19 | -------------------------------------------------------------------------------- /demo_app/example_app/templates/meta/hidden_columns.html: -------------------------------------------------------------------------------- 1 |

2 | 3 | hidden_columns 4 | 5 |

6 |
Columns from demo: n pingbacks (not shown)
7 | 8 |

9 | A list of column names that should be hidden from the user on the frontend. 10 |

11 |

12 | The use case for this option is that a column might need to be generated and communicated during 13 | AJAX requests, but stripped out for that specific page. This might allow for reusable data 14 | sources where the page is modifying what is being seen. Because new data sources are easy to 15 | make, however, we recommend avoiding too much funny business. 16 |

17 |

18 | The "hidden" data is still going across the wire anyway, and is inspectable by prying eyes. 19 | Don't rely on this for sensitive data. 20 |

21 |

22 | Another use for this feature in dataTables.js is for plugins. Exporters like the CSV one have 23 | access to the "hidden" fields and can export them to a file even if they are not visible on the 24 | web page. 25 |

26 | -------------------------------------------------------------------------------- /demo_app/example_app/templates/meta/model.html: -------------------------------------------------------------------------------- 1 |

2 | 3 | model 4 | 5 |

6 | 7 |

8 | The model that the table will represent. This helps the table automatically inspect fields 9 | named by the columns list and find their verbose_name settings. If 10 | this is not declared, it can be provided automatically by the model attribute on 11 | the view or even the incoming object_list (which is often a queryset). 12 |

13 | -------------------------------------------------------------------------------- /demo_app/example_app/templates/meta/ordering.html: -------------------------------------------------------------------------------- 1 |

2 | 3 | ordering 4 | 5 |

6 |
Columns from demo: ID
7 | 8 |

9 | A list of field and/or column names describing how the table should be sorted by default. This 10 | overrides the model's own Meta.ordering setting, but will itself be overriden by 11 | the client if sorting operations are specified during AJAX requests. 12 |

13 |

14 | Like model ordering, field/column names can use a prefix '-' to indicate reverse 15 | ordering for that field. 16 |

17 | -------------------------------------------------------------------------------- /demo_app/example_app/templates/meta/page_length.html: -------------------------------------------------------------------------------- 1 |

2 | 3 | page_length 4 | 5 |

6 | 7 |

8 | The page length sets the client option for how many records are shown in a single page of the 9 | datatable, which in turn controls how many objects are requested at a time from the server over 10 | AJAX. 11 |

12 |

13 | Make sure your table is javascript-configured to allow the page size you choose, since 14 | dataTables.js will allow you to return to your default setting if the user changes 15 | the page size away to something else. The demo on this page added a smaller option than is 16 | normally available by default in order to use a page size of 5 in the demo. 17 |

18 | -------------------------------------------------------------------------------- /demo_app/example_app/templates/meta/search_fields.html: -------------------------------------------------------------------------------- 1 |

2 | 3 | search_fields 4 | 5 |

6 |
7 | Columns from demo: 8 | 9 | blog__name (not shown, but search "first" or "second" to find entries being filtered by this 10 | setting) 11 | 12 |
13 | 14 |

15 | Additional database fields to query for global searches can be specified with this setting. 16 |

17 |

18 | Normally you would simply back each of your columns with the correct list of database fields, 19 | but if valid searchable fields don't actually appear in the columns, you can have these extra 20 | fields appended to the list of searchable items without lying about the fields in your column 21 | specification. 22 |

23 | -------------------------------------------------------------------------------- /demo_app/example_app/templates/meta/structure_template.html: -------------------------------------------------------------------------------- 1 |

2 | 3 | structure_template 4 | 5 |

6 | 7 |

8 | Defines a template path that the datatable should render on its initial non-AJAX page load. 9 | This is the template that appears on the main template when the 10 | {% templatetag openvariable %} datatable {% templatetag closevariable %} variable 11 | is rendered. 12 |

13 | -------------------------------------------------------------------------------- /demo_app/example_app/templates/meta/unsortable_columns.html: -------------------------------------------------------------------------------- 1 |

2 | 3 | unsortable_columns 4 | 5 |

6 |
Columns from demo: n comments
7 | 8 |

9 | A list of column names that should not be sortable by the client. This is enforced on the 10 | server side configuration validation as well, since a deliberate AJAX request to sort a column 11 | you wanted the client to disable could have very bad performance implications if it was allowed 12 | to have a go at the data. 13 |

14 | -------------------------------------------------------------------------------- /demo_app/example_app/templates/valid_column_formats.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |

Deprecated column definition guide

5 | 6 |

7 | Before version 0.9 and the Datatable configuration class it introduced, all 8 | options had to be specified in one big dictionary and assigned as an attribute on the view. 9 | For simple tables, view attributes weren't that so bad, but it didn't take much to get 10 | out of control. You should avoid using it when building new tables. 11 |

12 | 13 |

14 | Using this configuration style, you name a model field and the table would find the model 15 | field's verbose name for display purposes. To use a custom verbose name, or if the column 16 | needed to show more than one model field worth of data (a method, property, two concatenated 17 | field values, etc) then the column had to become a 2- or 3-tuple of settings. 18 |

19 | 20 |

21 | The following formats are all valid (but deprecated) ways to write a column definition on 22 | a ``LegacyConfigurationDatatableView`` or ``LegacyDatatableView``, via the 23 | datatable_options class attribute dict: 24 |

25 |
26 | datatable_options = {
27 |     'columns': [
28 |         # "concrete" field backed by the database
29 |         'name',  # field's verbose_name will be used
30 |         ("Name", 'name'),  # custom verbose name provided
31 |         ("Name", 'name', 'callback_attr_name'),  # callback looked up on view
32 |         ("Name", 'name', callback_handle),  # calback used directly
33 | 
34 |         # non-field, but backed by methods, properties, etc
35 |         "Virtual Field",
36 | 
37 |         # "fake" virtual field whose data is generated by the view
38 |         ("Virtual Field", None, 'callback_attr_name'),
39 |         ("Virtual Field", None, callback_handle),
40 |     ],
41 | }
42 |     
43 | 44 |

45 | For concrete fields that also provide callbacks, the actual database value will be consulted 46 | during searches and sorts, but the table will use the return value of the callback as the 47 | display data. 48 |

49 | 50 |

51 | Virtual fields are useful ways to mount methods onto a table. Consider a read-only property 52 | that generates its return value based on some underlying database field: 53 |

54 |
55 | datatable_options = {
56 |     'columns': [
57 |         ("Average profit", 'get_average_profit'),
58 |     ],
59 | }
60 |     
61 | 62 |

63 | Be careful with virtual columns that might cause database queries per-row. That doesn't 64 | scale very well! 65 |

66 | {% endblock content %} 67 | -------------------------------------------------------------------------------- /demo_app/example_app/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pivotal-energy-solutions/django-datatable-view/f142b3444e6377bb879f1c46f342e858ae9000eb/demo_app/example_app/templatetags/__init__.py -------------------------------------------------------------------------------- /demo_app/example_app/templatetags/example_app_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django import get_version 3 | from django.urls import reverse 4 | 5 | register = template.Library() 6 | 7 | if get_version().split(".") < ["1", "5"]: 8 | 9 | @register.simple_tag(name="url") 10 | def django_1_4_url_simple(url_name): 11 | return reverse(url_name) 12 | -------------------------------------------------------------------------------- /demo_app/example_app/urls.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from django.urls import re_path 4 | 5 | from . import views 6 | 7 | urls = [] 8 | for attr in dir(views): 9 | View = getattr(views, attr) 10 | try: 11 | is_demo = issubclass(View, views.DemoMixin) and View is not views.DemoMixin 12 | except TypeError: 13 | continue 14 | if is_demo: 15 | name = re.sub(r"([a-z]|[A-Z]+)(?=[A-Z])", r"\1-", attr).lower() 16 | name = name.replace("-datatable-view", "") 17 | urls.append(re_path(r"^{name}/$".format(name=name), View.as_view(), name=name)) 18 | 19 | urlpatterns = [ 20 | re_path(r"^$", views.IndexView.as_view(), name="index"), 21 | re_path(r"^reset/$", views.ResetView.as_view()), 22 | re_path(r"^migration-guide/$", views.MigrationGuideView.as_view(), name="migration-guide"), 23 | re_path(r"^column-formats/$", views.ValidColumnFormatsView.as_view(), name="column-formats"), 24 | re_path( 25 | r"^javascript-initialization/$", 26 | views.JavascriptInitializationView.as_view(), 27 | name="js-init", 28 | ), 29 | re_path(r"^satellite/$", views.SatelliteDatatableView.as_view(), name="satellite"), 30 | ] + urls 31 | -------------------------------------------------------------------------------- /demo_app/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | 4 | import os 5 | import sys 6 | 7 | 8 | def main(): 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo_app.settings") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /demo_app/test_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pivotal-energy-solutions/django-datatable-view/f142b3444e6377bb879f1c46f342e858ae9000eb/demo_app/test_app/__init__.py -------------------------------------------------------------------------------- /demo_app/test_app/fixtures/test_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "model": "test_app.RelatedModel", "pk": 1, "fields": { 3 | "name": "Related 1" 4 | }}, 5 | 6 | { "model": "test_app.RelatedM2MModel", "pk": 1, "fields": { 7 | "name": "RelatedM2M 1" 8 | }}, 9 | 10 | { "model": "test_app.ExampleModel", "pk": 1, "fields": { 11 | "name": "Example 1", 12 | "date_created": "2013-01-01T17:41:28+00:00" 13 | }}, 14 | { "model": "test_app.ExampleModel", "pk": 2, "fields": { 15 | "name": "Example 2", 16 | "date_created": "2013-01-01T17:41:28+00:00", 17 | "related": 1 18 | }} 19 | ] 20 | -------------------------------------------------------------------------------- /demo_app/test_app/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class ExampleModel(models.Model): 5 | name = models.CharField(max_length=64) 6 | value = models.BooleanField(default=False) 7 | date_created = models.DateTimeField(auto_now_add=True) 8 | related = models.ForeignKey("RelatedModel", blank=True, null=True, on_delete=models.CASCADE) 9 | relateds = models.ManyToManyField("RelatedM2MModel", blank=True) 10 | 11 | def __str__(self): 12 | return "ExampleModel %d" % (self.pk,) 13 | 14 | def __repr__(self): 15 | return "" % ( 16 | self.pk, 17 | self.name, 18 | ) 19 | 20 | def get_absolute_url(self): 21 | return "#{pk}".format(pk=self.pk) 22 | 23 | def get_negative_pk(self): 24 | return -1 * self.pk 25 | 26 | 27 | class RelatedModel(models.Model): 28 | name = models.CharField(max_length=64) 29 | 30 | def __str__(self): 31 | return "RelatedModel %d" % (self.pk,) 32 | 33 | def get_absolute_url(self): 34 | return "#{pk}".format(pk=self.pk) 35 | 36 | 37 | class RelatedM2MModel(models.Model): 38 | name = models.CharField(max_length=15) 39 | 40 | 41 | class ReverseRelatedModel(models.Model): 42 | name = models.CharField(max_length=15) 43 | example = models.ForeignKey("ExampleModel", on_delete=models.CASCADE) 44 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-datatable-view.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-datatable-view.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/django-datatable-view" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-datatable-view" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /docs/datatableview/columns.rst: -------------------------------------------------------------------------------- 1 | ``columns`` 2 | =========== 3 | 4 | .. py:module:: datatableview.columns 5 | 6 | 7 | Column 8 | ------ 9 | 10 | .. autoclass:: Column 11 | 12 | Subclasses of :py:class:`Column` automatically register themselves as handlers of certain 13 | model fields, using :py:attr:`.model_field_class` and :py:attr:`.handles_field_classes` to offer 14 | support for whichever ``ModelField`` types they wish. 15 | 16 | External subclasses will automatically override those found in this module. 17 | 18 | Custom columns are not necessarily required when a third-party ``ModelField`` subclasses a 19 | built-in one, like ``CharField``. If the field however offers special query lookups, a dedicated 20 | column can be declared and the query lookups specified by its :py:attr:`lookup_types` list. 21 | 22 | :param str label: The verbose name shown on the table header. If omitted, the first item in 23 | ``sources`` is checked for a ``verbose_name`` of its own. 24 | :param list sources: A list of strings that define which fields on an object instance will be 25 | supply the value for this column. Model field names (including query 26 | language syntax) and model attribute, method, and property names are all 27 | valid source names. All sources in the list should share a common model 28 | field class. If they do not, see :py:class:`CompoundColumn` for information 29 | on separating the sources by type. 30 | :param str source: A convenience parameter for specifying just one source name. Cannot be used 31 | at the same time as ``sources``. 32 | :param processor: A reference to a callback that can modify the column source data before 33 | serialization and transmission to the client. Direct callable references will 34 | be used as-is, but strings will be used to look up that callable as a method of 35 | the column's :py:class:`~datatableview.datatables.Datatable` (or failing that, 36 | the view that is serving the table). 37 | :type processor: callable or str 38 | :param str separator: The string that joins multiple source values together if more than one 39 | source is declared. This is primarily a zero-configuration courtesy, and 40 | in most situations the developer should provide a :py:attr:`processor` 41 | callback that does the right thing with compound columns. 42 | :param str empty_value: The string shown as the column value when the column's source(s) are 43 | ``None``. 44 | :param bool sortable: Controls whether the rendered table will allow user sorting. 45 | :param bool visible: Controls whether the ``bVisible`` flag is set for the frontend. A 46 | ``visible=False`` column is still generated and transmitted over ajax 47 | requests for the table. 48 | :param bool localize: A special hint sent to processor callbacks to use. 49 | :param bool allow_regex: Adds ``__iregex`` as a query lookup type for this instance of the 50 | column. 51 | :param bool allow_full_text_search: Adds ``__search`` as a query lookup type for this instance of 52 | the column. Make sure your database backend and column type 53 | support this query type before enabling it. 54 | 55 | **Class Attributes** 56 | 57 | .. autoattribute:: model_field_class 58 | 59 | References the core ``ModelField`` type that can represent this column's data in the backend. 60 | 61 | .. autoattribute:: handles_field_classes 62 | 63 | References to additional ``ModelField`` types that can be supported by this column. 64 | 65 | .. autoattribute:: lookup_types 66 | 67 | ORM query types supported by the underlying :py:attr:`model_field_class`. The default types 68 | show bias for text-type fields in order for custom fields that don't subclass Django's 69 | ``CharField`` to be handled automatically. 70 | 71 | Subclasses should provide query lookups that make sense in the context of searching. If 72 | required, input coercion (from a string to some other type) should be handled in 73 | :py:meth:`.prep_search_value`, where you have the option to reject invalid search terms for a 74 | given lookup type. 75 | 76 | **Instance Attributes** 77 | 78 | .. attribute:: sources 79 | 80 | A list of strings that define which fields on an object instance will be supply the value for 81 | this column. Model field names and extra :py:class:`Datatable` column declarations are valid 82 | source names. 83 | 84 | .. attribute:: processor 85 | 86 | A reference to a callback that can modify the column source data before serialization and 87 | transmission to the client. Direct callable references will be used as-is, but strings will 88 | be used to look up that callable as a method of the column's :py:class:`Datatable` (or 89 | failing that, the view that is serving the table). 90 | 91 | **Properties** 92 | 93 | .. autoattribute:: attributes 94 | 95 | **Methods** 96 | 97 | .. automethod:: __str__ 98 | 99 | Renders a simple ```` element with ``data-name`` attribute. All items found in the 100 | ``self.attributes`` dict are also added as attributes. 101 | 102 | .. automethod:: search 103 | .. automethod:: prep_search_value 104 | .. automethod:: value 105 | .. automethod:: get_initial_value 106 | .. automethod:: get_source_value 107 | .. automethod:: get_processor_kwargs 108 | 109 | **Internal Methods** 110 | 111 | .. automethod:: get_db_sources 112 | .. automethod:: get_virtual_sources 113 | .. automethod:: get_sort_fields 114 | .. automethod:: get_lookup_types 115 | 116 | 117 | Available Columns 118 | ----------------- 119 | 120 | Model fields that subclass model fields shown here are automatically covered by these columns, which 121 | is why not all built-in model fields require their own column class, or are even listed in the 122 | handled classes. 123 | 124 | 125 | TextColumn 126 | ~~~~~~~~~~ 127 | 128 | .. autoclass:: TextColumn(**kwargs) 129 | 130 | .. autoattribute:: model_field_class 131 | :annotation: = CharField 132 | .. autoattribute:: handles_field_classes 133 | :annotation: = [CharField, TextField, FileField] 134 | .. autoattribute:: lookup_types 135 | 136 | IntegerColumn 137 | ~~~~~~~~~~~~~ 138 | 139 | .. autoclass:: IntegerColumn(**kwargs) 140 | 141 | .. autoattribute:: model_field_class 142 | :annotation: = IntegerField 143 | .. autoattribute:: handles_field_classes 144 | :annotation: = [IntegerField, AutoField] 145 | .. autoattribute:: lookup_types 146 | 147 | FloatColumn 148 | ~~~~~~~~~~~ 149 | 150 | .. autoclass:: FloatColumn(**kwargs) 151 | 152 | .. autoattribute:: model_field_class 153 | :annotation: = FloatField 154 | .. autoattribute:: handles_field_classes 155 | :annotation: = [FloatField, DecimalField] 156 | .. autoattribute:: lookup_types 157 | 158 | DateColumn 159 | ~~~~~~~~~~ 160 | 161 | .. autoclass:: DateColumn(**kwargs) 162 | 163 | .. autoattribute:: model_field_class 164 | :annotation: = DateField 165 | .. autoattribute:: handles_field_classes 166 | :annotation: = [DateField] 167 | .. autoattribute:: lookup_types 168 | 169 | DateTimeColumn 170 | ~~~~~~~~~~~~~~ 171 | 172 | .. autoclass:: DateTimeColumn(**kwargs) 173 | 174 | .. autoattribute:: model_field_class 175 | :annotation: = DateTimeField 176 | .. autoattribute:: handles_field_classes 177 | :annotation: = [DateTimeField] 178 | .. autoattribute:: lookup_types 179 | 180 | BooleanColumn 181 | ~~~~~~~~~~~~~ 182 | 183 | .. autoclass:: BooleanColumn(**kwargs) 184 | 185 | .. autoattribute:: model_field_class 186 | :annotation: = BooleanField 187 | .. autoattribute:: handles_field_classes 188 | :annotation: = [BooleanField, NullBooleanField] 189 | .. autoattribute:: lookup_types 190 | 191 | DisplayColumn 192 | ~~~~~~~~~~~~~ 193 | 194 | .. autoclass:: DisplayColumn(**kwargs) 195 | 196 | .. autoattribute:: model_field_class 197 | :annotation: = None 198 | .. autoattribute:: handles_field_classes 199 | :annotation: = [] 200 | .. autoattribute:: lookup_types 201 | 202 | CompoundColumn 203 | ~~~~~~~~~~~~~~ 204 | 205 | .. autoclass:: CompoundColumn(**kwargs) 206 | 207 | .. autoattribute:: model_field_class 208 | :annotation: = None 209 | .. autoattribute:: handles_field_classes 210 | :annotation: = [] 211 | .. autoattribute:: lookup_types 212 | -------------------------------------------------------------------------------- /docs/datatableview/forms.rst: -------------------------------------------------------------------------------- 1 | ``forms`` 2 | ========= 3 | .. py:module:: datatableview.forms 4 | 5 | 6 | XEditableUpdateForm 7 | ------------------- 8 | 9 | The X-Editable mechanism works by sending events to the view that indicate the user's desire to 10 | open a field for editing, and their intent to save a new value to the active record. 11 | 12 | The ajax ``request.POST['name']`` data field name that tells us which of the model 13 | fields should be targetted by this form. An appropriate formfield is looked up for that model 14 | field, and the ``request.POST['value']`` data will be inserted as the field's value. 15 | 16 | .. autoclass:: XEditableUpdateForm 17 | 18 | :param Model model: The model class represented in the datatable. 19 | 20 | .. automethod:: set_value_field 21 | .. automethod:: clean_name 22 | -------------------------------------------------------------------------------- /docs/datatableview/helpers.rst: -------------------------------------------------------------------------------- 1 | ``helpers`` 2 | =========== 3 | .. py:module:: datatableview.helpers 4 | 5 | 6 | The ``helpers`` module contains functions that can be supplied directly as a column's :py:attr:`~datatableview.columns.Column.processor`. 7 | 8 | Callbacks need to accept the object instance, and arbitrary other ``**kwargs``, because the ``Datatable`` instance will send it contextual information about the column being processed, such as the default value the column contains, the originating view, and any custom keyword arguments supplied by you from :py:meth:`~datatableview.datatables.Datatable.preload_record_data`. 9 | 10 | link_to_model 11 | ------------- 12 | 13 | .. autofunction:: link_to_model(instance, text=None, **kwargs) 14 | 15 | make_boolean_checkmark 16 | ---------------------- 17 | 18 | .. autofunction:: make_boolean_checkmark(value, true_value="✔", false_value="✘", *args, **kwargs) 19 | 20 | itemgetter 21 | ---------- 22 | 23 | .. autofunction:: itemgetter 24 | 25 | attrgetter 26 | ---------- 27 | 28 | .. autofunction:: attrgetter 29 | 30 | format_date 31 | ----------- 32 | 33 | .. autofunction:: format_date 34 | 35 | format 36 | ------ 37 | 38 | .. autofunction:: format(format_string, cast=) 39 | 40 | make_xeditable 41 | -------------- 42 | 43 | .. autofunction:: make_xeditable 44 | 45 | make_processor 46 | -------------- 47 | 48 | .. autofunction:: make_processor 49 | -------------------------------------------------------------------------------- /docs/datatableview/index.rst: -------------------------------------------------------------------------------- 1 | ``datatableview`` module documentation 2 | ====================================== 3 | 4 | .. toctree:: 5 | views 6 | datatables 7 | columns 8 | forms 9 | helpers 10 | -------------------------------------------------------------------------------- /docs/datatableview/views.rst: -------------------------------------------------------------------------------- 1 | ``views`` 2 | ========= 3 | 4 | DatatableView 5 | ------------- 6 | 7 | .. py:module:: datatableview.views.base 8 | 9 | 10 | .. autoclass:: DatatableMixin 11 | :members: 12 | 13 | .. autoclass:: DatatableView 14 | 15 | 16 | ``views.xeditable`` 17 | ------------------- 18 | .. py:module:: datatableview.views.xeditable 19 | 20 | 21 | .. autoclass:: XEditableMixin 22 | :members: 23 | 24 | .. autoclass:: XEditableDatatableView 25 | 26 | 27 | ``views.legacy`` 28 | ---------------- 29 | .. py:module:: datatableview.views.legacy 30 | 31 | 32 | The ``legacy`` module holds most of the support utilities required to make the old tuple-based configuration syntax work. 33 | 34 | Use :py:class:`LegacyDatatableView` as your view's base class instead of :py:class:`DatatableView`, and then declare a class attribute ``datatable_options`` as usual. This strategy simply translates the old syntax to the new syntax. Certain legacy internal hooks and methods will no longer be available. 35 | 36 | .. autoclass:: LegacyDatatableMixin 37 | 38 | .. autoattribute:: datatable_options 39 | :annotation: = {} 40 | 41 | .. autoattribute:: datatable_class 42 | :annotation: = LegacyDatatable 43 | 44 | The :py:class:`~datatableview.datatables.LegacyDatatable` will help convert the more 45 | extravagant legacy tuple syntaxes into full :py:class:`~datatableview.columns.Column` 46 | instances. 47 | 48 | .. autoclass:: LegacyDatatableView 49 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. django-datatable-view documentation master file, created by 2 | sphinx-quickstart on Tue Oct 20 13:00:37 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to django-datatable-view's documentation! 7 | ================================================= 8 | 9 | For working demos and example code with explanations on common configurations, visit the demo site at http://example.com. 10 | 11 | Contents: 12 | 13 | .. toctree:: 14 | :maxdepth: 4 15 | 16 | migration-guide 17 | topics/index 18 | datatableview/index 19 | 20 | Indices and tables 21 | ================== 22 | 23 | * :ref:`genindex` 24 | * :ref:`modindex` 25 | * :ref:`search` 26 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 2> nul 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-datatable-view.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-datatable-view.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /docs/migration-guide.rst: -------------------------------------------------------------------------------- 1 | 0.9 Migration Guide 2 | =================== 3 | 4 | The jump from the 0.8.x series to 0.9 is covered in sections below. 5 | 6 | dataTables.js 1.10 7 | ------------------ 8 | 9 | :Note: See `the official 1.10 announcement`__ if you've been living under a rock! 10 | 11 | __ http://datatables.net/blog/2014-05-01 12 | 13 | DataTables 1.10 provides a brand new api for getting things done, and it's a good thing too, because doing anything fancy in the old api pretty much required Allan to write yet another block of example code that everyone just copies and pastes. 14 | 15 | For our 0.9 release of django-datatable-view, we still use the "legacy" constructor to get things going, but that's okay, because the legacy api is still completely supported (even if all of its Hungarian notation keeps us up at night). The drawback at this stage is that we can't yet accept configuration settings that are "new-style only". 16 | 17 | Despite the fact that we're using the legacy constructor for a while longer, you can access the table's fancy new API object with one simple line:: 18 | 19 | // Standard initialization 20 | var opts = {}; 21 | var datatable = datatableview.initialize($('.datatable'), opts); 22 | 23 | // Get a reference to the new API object 24 | var table = datatable.api(); 25 | 26 | Update configuration style 27 | -------------------------- 28 | 29 | :Note: See `Datatable object and Meta`__ for examples. 30 | 31 | __ {% url "configure-datatable-object" %} 32 | 33 | The preferred way to configure columns for a view is now to use the :py:class:`~datatableview.datatables.Datatable` class. It has similarities to the Django ``ModelForm``: the class uses an inner :py:class:`~datatableview.datatables.Meta` class to specify all of the options that we used to provide in your view's ``datatable_options`` dict. 34 | 35 | You want to just unpack the keys and values from your existing ``datatable_options`` dict and set those as attributes on a :py:class:`~datatableview.datatables.Meta`. Then just assign this :py:class:`~datatableview.datatables.Datatable` subclass on your view:: 36 | 37 | class MyDatatable(Datatable): 38 | class Meta: 39 | columns = [ ... ] 40 | search_fields = [ ... ] 41 | # etc 42 | 43 | class MyDatatableView(DatatableView): 44 | datatable_class = MyDatatable 45 | 46 | An alternate abbreviated style is available: as with class-based views that use Django forms, you can set these ``Meta`` attributes directly on the view class, `shown in more detail here`__. Please note that if you're declaring anything fancier than simple model fields or methods as columns (typically anything that would have required the 2-tuple or 3-tuple column syntax), please use the new ``Datatable`` object strategy. 47 | 48 | __ {% url "configure-inline" %} 49 | 50 | The new ``Datatable`` object doubles as the old 0.8 ``DatatableOptions`` template renderable object. ``DatatableOptions`` and ``utils.get_datatable_structure()`` have both been removed, since ``Datatable`` itself is all you need. 51 | 52 | 53 | New vocabulary 54 | -------------- 55 | 56 | :Celebrate: We're becoming more sophisticated! 57 | 58 | Now that we spent a bunch of time learning how to use the tools we created, it felt like a good 59 | time to change some of the terms used internally. 60 | 61 | In connection with the new ``Datatable`` object that helps you design the datatable, **we've started referring to column data callbacks as "processors"**. This means that we will stop relying on callbacks in the documentation being named in the pattern of ``'get_column_FOO_data()'``. Instead, you'll notice names like ``'get_FOO_data()'``, and we'll be specifying the callback in a column definition via a ``processor`` keyword argument. See `Postprocessors`__ for a examples of this. 62 | 63 | __ {% url "processors" %} 64 | 65 | 66 | No more automatic column callbacks 67 | ---------------------------------- 68 | 69 | :The Zen of Python: Explicit is better than implicit. 70 | 71 | We knew that implicit callbacks was a bad idea, but in our defense, `the deprecated column format was really cumbersome to use`__, and implicit callbacks were saving us some keystrokes. **This behavior is going away in version 1.0.** We continue to support implicit callbacks so that 0.9 is a backwards-compatible release with 0.8. If you have any column callbacks (we're calling them "processors" now) that aren't explicitly named in the column definition, please update your code soon! 72 | 73 | __ {% url "column-formats" %} 74 | 75 | 76 | No more automatic dataTables.js initialization 77 | ---------------------------------------------- 78 | 79 | :Note: Bye bye ``function confirm_datatable_options(options){ ... }`` 80 | 81 | Automatic initialization has gone the way of the buffalo, meaning that it doesn't exist anymore. The global JavaScript function ``confirm_datatable_options`` only ever existed because auto initialization took away your chance to set custom options during the init process. You should initialize your datatables via a simple call to the global function ``datatableview.initialize($('.datatable'), opts)``. This JS function reads DOM attributes from the table structure and builds some of the column options for you, but you can pass literally any other supported option in as the second argument. Just give it an object, and everything will be normal. 82 | 83 | There is a configurable Javascript flag ``datatableview.auto_initialize`` that 84 | previously defaulted to ``true``, but in 0.9 its default value is now 85 | ``false``. If you need 0.9 to behave the way it did in 0.8, set this flag globally 86 | or per-page as needed. (Be careful not to do it in a ``$(document).ready()`` 87 | handler, since auto initialization runs during that hook. You might end up flagging for 88 | auto initialization after datatableview.js has already finished checking it, and nothing 89 | will happen.) 90 | 91 | 92 | Double check your default structure template 93 | -------------------------------------------- 94 | 95 | :Note: See `Custom render template`__ for examples. 96 | 97 | __ {% url "customized-template" %} 98 | 99 | If you haven't gone out of your way to override the default structure template or create your own template, this shouldn't apply to you. 100 | 101 | The 0.9 default structure template at ``datatableview/default_structure.html`` has been modified to include a reference to a ``{% templatetag openvariable %} config {% templatetag closevariable %}`` variable, which holds all of the configuration values for the table. The render context for this template previously held a few select loose values for putting ``data-*`` attributes on the main ```` tag, but the template should now read from the following values (note the leading ``config.``: 102 | 103 | * ``{{ config.result_counter_id }}`` 104 | * ``{{ config.page_length }}`` 105 | 106 | 107 | Update complex column definitions 108 | --------------------------------- 109 | 110 | :Note: See `Custom verbose names`__, `Model method-backed columns`__, `Postprocessing values`__, and `Compound columns`__ for examples. 111 | 112 | __ /pretty-names/ 113 | __ /column-backed-by-method/ 114 | __ /processors/ 115 | __ /compound-columns/ 116 | 117 | The `now-deprecated 0.8 column definition format`__ had a lot of overloaded syntax. It grew out of a desire for a simple zero-configuration example, but became unwieldy, using nested tuples and optional tuple lengths to mean different things. 118 | 119 | __ /column-formats/ 120 | 121 | The new format can be thought of as a clone of the built-in Django forms framework. In that comparison, the new ``Datatable`` class is like a Form, complete with Meta options that describe its features, and it defines ``Column`` objects instead of FormFields. A ``Datatable`` configuration object is then given to the view in the place of the old ``datatable_options`` dictionary. 122 | 123 | In summary, the old ``datatable_options`` dict is replaced by making a ``Datatable`` configuration object that has a ``Meta``. 124 | 125 | The task of `showing just a few specific columns`__ is made a bit heavier than before, but (as with the forms framework) the new Meta options can all be provided as class attributes on the view to keep the simplest cases simple. 126 | 127 | __ /specific-columns/ 128 | 129 | 130 | Custom model fields 131 | ------------------- 132 | 133 | :Note: See `Custom model fields`__ for new registration strategy. 134 | 135 | __ /custom-model-fields/ 136 | 137 | Custom model fields were previously registered in a dict in ``datatableview.utils.FIELD_TYPES``, where the type (such as ``'text'``) would map to a list of model fields that conformed to the text-style ORM query types (such as ``__icontains``). 138 | 139 | In 0.9, the registration mechanism has changed to a priority system list, which associates instances of the new ``Column`` class to the model fields it can handle. See `Custom model fields`__ for examples showing how to register model fields to a built-in ``Column`` class, and how to write a new ``Column`` subclass if there are custom ORM query types that the field should support. 140 | 141 | __ /custom-model-fields/ 142 | 143 | 144 | Experiment with the new ``ValuesDatatable`` 145 | ------------------------------------------- 146 | 147 | :Note: See `ValuesDatatable object`__ for examples. 148 | 149 | __ {% url "configure-values-datatable-object" %} 150 | 151 | An elegant simplification of the datatable strategy is to select the values you want to show directly from the database and just put them through to the frontend with little or no processing. If you can give up declaration of column sources as model methods and properties, and rely just on the data itself to be usable, try swapping in a ``ValuesDatatable`` as the base class for your table, rather than the default ``Datatable``. 152 | 153 | This saves Django the trouble of instantiating model instances for each row, and might even encourage the developer to think about their data with fewer layers of abstraction. 154 | -------------------------------------------------------------------------------- /docs/topics/caching.rst: -------------------------------------------------------------------------------- 1 | Caching 2 | ======= 3 | 4 | The caching system is opt-in on a per-:py:attr:`~datatableview.datatables.Datatable` basis. 5 | 6 | Each Datatable can specify in its :py:attr:`~datatableview.datatables.Meta` options a value for the :py:attr:`~datatableview.datatables.Meta.cache_type` option. 7 | 8 | 9 | Caching Strategies 10 | ------------------ 11 | 12 | The possible values are available as constants on ``datatableview.datatables.cache_types``. Regardless of strategy, your `Settings`_ will control which Django-defined caching backend to use, and therefore the expiry time and other backend characteristics. 13 | 14 | ``cache_types.DEFAULT`` 15 | ~~~~~~~~~~~~~~~~~~~~~~~ 16 | 17 | A stand-in for whichever strategy ``DATATABLEVIEW_DEFAULT_CACHE_TYPE`` in your `Settings`_ specifies. That setting defaults to ``SIMPLE``. 18 | 19 | ``cache_types.SIMPLE`` 20 | ~~~~~~~~~~~~~~~~~~~~~~ 21 | 22 | Passes the ``object_list`` (usually a queryset) directly to the cache backend for pickling. This is a more faithful caching strategy than ``PK_LIST`` but becomes noticeably slower as the number of cached objects grows. 23 | 24 | ``cache_types.PK_LIST`` 25 | ~~~~~~~~~~~~~~~~~~~~~~~ 26 | 27 | Assumes that ``object_list`` is a queryset and stores in the cache only the list of ``pk`` values for each object. Reading from the cache therefore requires a database query to re-initialize the queryset, but because that query may be substantially faster than producing the original queryset, it is tolerated. 28 | 29 | Because this strategy must regenerate the queryset, extra information on the original queryset will be lost, such as calls to ``select_related()``, ``prefetch_related()``, and ``annotate()``. 30 | 31 | ``cache_types.NONE`` 32 | ~~~~~~~~~~~~~~~~~~~~ 33 | 34 | An explicit option that disables a caching strategy for a table. Useful when subclassing a Datatable to provide customized options. 35 | 36 | 37 | Settings 38 | -------- 39 | 40 | There are a few project settings you can use to control features of the caching system when activated on a :py:attr:`~datatableview.datatables.Datatable`. 41 | 42 | ``DATATABLEVIEW_CACHE_BACKEND`` 43 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 44 | :Default: ``'default'`` 45 | 46 | The name of the Django ``CACHES`` backend to use. This is where cache expiry information will be 47 | specified. 48 | 49 | ``DATATABLEVIEW_CACHE_PREFIX`` 50 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 51 | :Default: ``'datatableview_'`` 52 | 53 | The prefix added to every cache key generated by a table's :py:meth:`~datatableview.datatables.Datatable.get_cache_key` value. 54 | 55 | ``DATATABLEVIEW_DEFAULT_CACHE_TYPE`` 56 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 57 | :Default: ``'simple'`` (``datatableview.datatables.cache_types.SIMPLE``) 58 | 59 | The caching strategy to use when a Datatable's Meta option :py:attr:`~datatableview.datatables.Meta.cache_type` is set to ``cache_types.DEFAULT``. 60 | 61 | ``DATATABLEVIEW_CACHE_KEY_HASH`` 62 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 63 | :Default: ``True`` 64 | 65 | Controls whether the values that go into the cache key will be hashed or placed directly into the cache key string. 66 | 67 | This may be required for caching backends with requirements about cache key length. 68 | 69 | When ``False``, a cache key might resemble the following:: 70 | 71 | datatableview_datatable_myproj.myapp.datatables.MyDatatable__view_myproj.myapp.views.MyView__user_77 72 | 73 | When ``True``, the cache key will be a predictable length, and might resemble the following:: 74 | 75 | datatableview_datatable_3da541559918a808c2402bba5012f6c60b27661c__view_1161e6ffd3637b302a5cd74076283a7bd1fc20d3__user_77 76 | 77 | 78 | ``DATATABLEVIEW_CACHE_KEY_HASH_LENGTH`` 79 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 80 | :Default: ``None`` 81 | 82 | When `DATATABLEVIEW_CACHE_KEY_HASH`_ is ``True``, setting this to an integer will slice each hash substring to the first N characters, allowing you to further control the cache key length. 83 | 84 | For example, if set to ``10``, the hash-enabled cache key might resemble:: 85 | 86 | datatableview_datatable_3da5415599__view_1161e6ffd3__user_77 87 | -------------------------------------------------------------------------------- /docs/topics/custom-columns.rst: -------------------------------------------------------------------------------- 1 | Third-party model fields 2 | ======================== 3 | 4 | Registering fields with custom columns 5 | -------------------------------------- 6 | 7 | Any model field that subclasses a built-in Django field is automatically supported out of the box, as long as it supports the same query types (``__icontains``, ``__year``, etc) as the original field. 8 | 9 | A third-party field that is defined from scratch generally needs to become registered with a :py:class:`~datatableview.columns.Column`. The most straightforward thing to do is to subclass the base :py:class:`~datatableview.columns.Column`, and set the class attribute :py:attr:`~datatableview.columns.Column.model_field_class` to the third-party field. This will allow any uses of that model field to automatically select this new column as the handler for its values. 10 | 11 | Just by defining the column class, it will be registered as a valid candidate when model fields are automatically paired to column classes. 12 | 13 | **Important gotcha**: Make sure the custom class is imported somewhere in the project if you're not already explicitly using it on a table declaration. If the column is never imported, it won't be registered. 14 | 15 | If the column needs to indicate support for new query filter types, declare the class attribute :py:attr:`~datatableview.columns.Column.lookup_types` as a list of those operators (without any leading ``__``). You should only list query types that make sense when performing a search. For example, an ``IntegerField`` supports ``__lt``, but using that in searches would be unintuitive and confusing, so it is not included in the default implementation of :py:class:`~datatableview.columns.IntegerColumn`. You may find that ``exact`` is often the only sensible query type. 16 | 17 | New column subclasses are automatically inserted at the top of the priority list when the column system needs to discover a suitable column for a given model field. This is done to make sure that the system doesn't mistake a third-party field that subclasses a built-in one like ``CharField`` isn't actually mistaken for a simple ``CharField``. 18 | 19 | Skipping column registration 20 | ---------------------------- 21 | 22 | Some column subclasses are not suitable for registration. For example, a custom column that is intended for use on only *some* ``CharField`` fields should definitely not attempt to register itself, since this would imply that all instances of ``CharField`` should use the new column. An example of this is the built-in :py:class:`~datatableview.columns.DisplayColumn`, which is a convenience class for representing a column that has no sources. 23 | 24 | By explicitly setting :py:attr:`~datatableview.columns.Column.model_field_class` to ``None``, the column will be unable to register itself as a handler for any specific model field. Consequently, it will be up to you to import and use the column where on tables where it makes sense. 25 | -------------------------------------------------------------------------------- /docs/topics/index.rst: -------------------------------------------------------------------------------- 1 | Topics 2 | ====== 3 | 4 | .. toctree:: 5 | interaction-model 6 | searching 7 | sorting 8 | custom-columns 9 | caching 10 | -------------------------------------------------------------------------------- /docs/topics/interaction-model.rst: -------------------------------------------------------------------------------- 1 | The client and server interaction model 2 | ======================================= 3 | 4 | High-level description 5 | ---------------------- 6 | 7 | Traditionally, developers using ``dataTables.js`` have approached their table designs from the client side. An ajax backend is just an implementation detail that can be enabled "if you need one." 8 | 9 | From the perspective of a Django application, however, we want to flip things around: the ``datatableview`` module has all of the tools required to build a server-side representation of your table, such as the column names, how it derives the information each column holds, and which sorting and filtering features it will expose. 10 | 11 | The execution steps for a server-driven table look like this: 12 | 13 | * The developer declares a view. 14 | * The view holds a table configuration object (like a Django ``ModelForm``). 15 | * The view puts the table object in the template rendering context. 16 | * The template renders the table object directly into the HTML, which includes its own template fragment to put the basic table structure on the page. (We happen to render a few ``data-*`` attributes on the ``
`` headers in the default template, but otherwise, the template isn't very interesting.) 17 | * The developer uses a javascript one-liner to initialize the table to get ``dataTables.js`` involved. 18 | 19 | From then on, the process is a loop of the user asking for changes to the table, and the server responding with the new data set: 20 | 21 | * The client sends an ajax request with ``GET`` parameters to the current page url. 22 | * The view uses the same table configuration object as before. 23 | * The view gives the table object the initial queryset. 24 | * The table configuration object overrides its default settings with any applicable ``GET`` parameters (sorting, searches, current page number, etc). 25 | * The table configuration object applies changes to the queryset. 26 | * The view serializes the final result set and responds to the client. 27 | 28 | Expanded details about some of these phases are found below. 29 | 30 | The table configuration object 31 | ------------------------------ 32 | 33 | The :py:class:`~datatableview.datatables.Datatable` configuration object encapsulates everything that the server understands about the table. It knows how to render its initial skeleton as HTML, and it knows what to do with a queryset based on incoming ``GET`` parameter data from the client. It is designed to resemble the Django ``ModelForm``. 34 | 35 | The resemblance with ``ModelForm`` includes the use of an inner :py:class:`~datatableview.datatables.Meta` class, which can specify which model class the table is working with, which fields from that model to import, which column is sorted by default, which template is used to render the table's HTML skeleton, etc. 36 | 37 | :py:class:`~datatableview.columns.Column` s can be added to the table that aren't just simple model fields, however. Columns can declare any number of :py:attr:`~datatableview.columns.Column.sources`, including the output of instance methods and properties, all of which can then be formatted to a desired HTML result. Columns need not correspond to just a single model field! 38 | 39 | The column is responsible for revealing the data about an object (based on the ``sources`` it was given), and then formatting that data as a suitable final result (including HTML). 40 | 41 | Update the configuration from ``GET`` parameters 42 | ------------------------------------------------ 43 | 44 | Many of the options declared on a :py:class:`~datatableview.datatables.Datatable` are considered protected. The column definitions themselves, for example, cannot be changed by a client playing with ``GET`` data. Similarly, the table knows which columns it holds, and it will not allow filters or sorting on data that it hasn't been instructed to inspect. ``GET`` parameters are normalized and ultimately thrown out if they don't agree with what the server-side table knows about the table. 45 | 46 | Generating the queryset filter 47 | ------------------------------ 48 | 49 | Because each column in the table has its :py:attr:`~datatableview.columns.Column.sources` plainly declared by the developer, the table gathers all of the sources that represent model fields (even across relationships). For each such source, the table matches it to a core column type and uses that as an interface to ask for a ``Q()`` filter for a given search term. 50 | 51 | The table combines all of the discovered filters together, making a single ``Q()`` object, and then filters the queryset in a single step. 52 | 53 | Read :doc:`searching` for more information about how a column builds its ``Q()`` object. 54 | 55 | The client table HTML and javascript of course don't know anything about the server's notion of column sources, even when using column-specific filter widgets. 56 | 57 | Sorting the table by column 58 | --------------------------- 59 | 60 | Because a column is allowed to refer to more than one supporting data source, "sorting by a column" actually means that the list of sources is considered as a whole. 61 | 62 | Read :doc:`sorting` to understand the different ways sorting can be handled based on the composition of the column's sources. 63 | 64 | As with searching, the client table HTML and javascript have no visibility into the column's underlying sources. It simply asks for a certain column index to be sorted, and the server's table representation decides what that means. 65 | -------------------------------------------------------------------------------- /docs/topics/searching.rst: -------------------------------------------------------------------------------- 1 | Searching 2 | ========= 3 | 4 | All searching takes place on the server. Your view's :py:attr:`~datatableview.datatables.Datatable` is designed to have all the information it needs to respond to the ajax requests from the client, thanks to each column's :py:attr:`~datatableview.columns.Column.sources` list. The order in which the individual sources are listed does not matter (although it does matter for :doc:`sorting`). 5 | 6 | Sources that refer to non-``ModelField`` attributes (such as methods and properties of the object) are not included in searches. Manual searches would mean fetching the full, unfiltered queryset on every single ajax request, just to be sure that no results were excluded before a call to ``queryset.filter()``. 7 | 8 | Important terms concerning column :py:attr:`~datatableview.columns.Column.sources`: 9 | 10 | * **db sources**: Sources that are just fields managed by Django, supporting standard queryset lookups. 11 | * **Virtual sources**: Sources that reference not a model field, but an object instance method or property. 12 | * **Compound column**: A Column that declares more than one source. 13 | * **Pure db column, db-backed column**: A Column that defines only db-backed sources. 14 | * **Pure virtual column, virtual column**: A Column that defines only virtual sources. 15 | * **Sourceless column**: A Column that declares no sources at all (likely relying on its processor callback to compute some display value from the model instance). 16 | 17 | Parsing the search string 18 | ------------------------- 19 | 20 | When given a search string, the :py:attr:`~datatableview.datatables.Datatable` splits up the string on spaces (except for quoted strings, which are protected). Each "term" is required to be satisfied somewhere in the object's collection of column :py:attr:`~datatableview.columns.Column.sources`. 21 | 22 | For each term, the table's :py:attr:`~datatableview.columns.Column` objects are asked to each provide a filter ``Q()`` object for that term. 23 | 24 | Deriving the ``Q()`` filter 25 | --------------------------- 26 | 27 | Terms are just free-form strings from the user, and may not be suitable for the column's data type. For example, the user could search for ``"54C-NN"``, and a integer-based column simply cannot coerce that term to something usable. Similar, searching for ``"13"`` is an integer, but isn't suitable for a ``DateTimeField`` to query as a ``__month``. 28 | 29 | Consequently, a column has the right to reject any search term that it is asked to build a query for. This allows columns to protect themselves from building invalid queries, and gives the developer a way to modify their own columns to decide what terms mean in the context of the data type they hold. 30 | 31 | A column's :py:meth:`~datatableview.columns.Column.search` method is called once per term. The default implementation narrows its :py:attr:`~datatableview.columns.Column.sources` down to just those that represent model fields, and then builds a query for each source, combining them with an ``OR`` operator. All of the different column ``Q()`` objects are then also combined with the ``OR`` operator, because global search terms can appear in any column. 32 | 33 | The only place an ``AND`` operator is used is from within the :py:attr:`~datatableview.datatables.Datatable`, which is combining all the results from the individual per-column term queries to make sure all terms are found. 34 | 35 | Compound columns with different data types 36 | ------------------------------------------ 37 | 38 | Multiple sources in a single column don't need to be the same data type. This is a quirk of the column system. Each source is automatically matched to one of the provided :py:class:`~datatableview.columns.Column` classes, looked up based on the source's model field class. This allows the column to ask internal copies of those column classes for query information, respecting the differences between data types and coercion requirements. 39 | -------------------------------------------------------------------------------- /docs/topics/sorting.rst: -------------------------------------------------------------------------------- 1 | Sorting 2 | ======= 3 | 4 | All sorting takes place on the server. Your view's :py:attr:`~datatableview.datatables.Datatable` is designed to have all the information it needs to respond to the ajax requests from the client, thanks to each column's :py:attr:`~datatableview.columns.Column.sources` list. Unlike for searching, the order in which the individual sources are listed might matter to the user. 5 | 6 | Important terms concerning column :py:attr:`~datatableview.columns.Column.sources`: 7 | 8 | * **db sources**: Sources that are just fields managed by Django, supporting standard queryset lookups. 9 | * **Virtual sources**: Sources that reference not a model field, but an object instance method or property. 10 | * **Compound column**: A Column that declares more than one source. 11 | * **Pure db column, db-backed column**: A Column that defines only db-backed sources. 12 | * **Pure virtual column, virtual column**: A Column that defines only virtual sources. 13 | * **Sourceless column**: A Column that declares no sources at all (likely relying on its processor callback to compute some display value from the model instance). 14 | 15 | Pure database columns 16 | --------------------- 17 | 18 | The ideal scenario for speed and simplicity is that all :py:attr:`~datatableview.columns.Column.sources` are simply queryset lookup paths (to a local model field or to one that is related). When this is true, the sources list can be sent directly to ``queryset.order_by()``. 19 | 20 | Reversing the sort order will reverse all source components, converting a sources list such as ``['id', 'name']`` to ``['-id', '-name']``. This can be sent directly to ``queryset.order_by()`` as well. 21 | 22 | Mixed database and virtual sources 23 | ---------------------------------- 24 | 25 | When a column has more than one source, the ``Datatable`` seeks to determine if there are ANY database sources at all. If there are, then the virtual ones are discarded for the purposes of sorting, and the strategy for pure database sorting can be followed. 26 | 27 | The strategic decision to keep or discard virtual sources is a complex one. We can't, in fact, just sort by the database fields first, and then blindly do a Python ``sort()`` on the resulting list, because the work performed by ``queryset.order_by()`` would be immediately lost. Any strategy that involves manually sorting on a virtual column must give up queryset ordering entirely, which makes the rationale for abandoning virtual sources easy to see. 28 | 29 | Pure virtual columns 30 | -------------------- 31 | 32 | When a column provides only virtual sources, the whole queryset will in fact be evaluated as a list and the results sorted in Python accordingly. 33 | 34 | Please note that the performance penalty for this is undefined: the larger the queryset (after search filters have been applied), the harder the memory and speed penalty will be. 35 | 36 | Columns without sources 37 | ----------------------- 38 | 39 | When no sources are available, the column automatically become unsortable by default. This is done to avoid allowing the column to claim the option to sort, yet do nothing when the user clicks on it. 40 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling", "hatch-vcs"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "django-datatable-view" 7 | dynamic = ["version"] 8 | description = "This package is used in conjunction with the jQuery plugin (http://http://datatables.net/), and supports state-saving detection with (http://datatables.net/plug-ins/api). The package consists of a class-based view, and a small collection of utilities for rendering table data from models." 9 | readme = "README.md" 10 | requires-python = ">=3.11" 11 | authors = [ 12 | { name = "Pivotal Energy Solutions", email = "steve@pivotal.energy" }, 13 | ] 14 | keywords = [ 15 | "django", 16 | ] 17 | classifiers = [ 18 | "Development Status :: 5 - Production/Stable", 19 | "Environment :: Web Environment", 20 | "Framework :: Django", 21 | "Framework :: Django :: 5.0", 22 | "Intended Audience :: Developers", 23 | "License :: OSI Approved :: Apache Software License", 24 | "Operating System :: OS Independent", 25 | "Programming Language :: Python", 26 | "Programming Language :: Python :: 3.11", 27 | "Programming Language :: Python :: 3.12", 28 | "Programming Language :: Python :: 3.13", 29 | "Topic :: Utilities", 30 | ] 31 | dependencies = [ 32 | "django>=5.0", 33 | "python-dateutil", 34 | ] 35 | 36 | [project.optional-dependencies] 37 | test = [ 38 | "django-environ", 39 | "mysqlclient", 40 | "coverage", 41 | "pre-commit", 42 | "black", 43 | "bandit", 44 | "ruff" 45 | ] 46 | docs = [ 47 | "Sphinx", 48 | "sphinx-rtd-theme" 49 | ] 50 | 51 | [project.urls] 52 | Homepage = "https://github.com/pivotal-energy-solutions/django-datatable-view" 53 | Download = "https://github.com/pivotal-energy-solutions/django-datatable-view" 54 | Thanks = "https://saythanks.io/to/rh0dium" 55 | Source = "https://github.com/pivotal-energy-solutions/django-datatable-view/" 56 | 57 | [tool.hatch.version] 58 | source = "vcs" 59 | 60 | [tool.hatch.build.targets.sdist] 61 | include = [ 62 | "/datatableview", 63 | "/datatableview/static/**/*", 64 | "/datatableview/templates/**/*", 65 | ] 66 | 67 | [tool.hatch.build.targets.wheel] 68 | packages = ['datatableview'] 69 | include = [ 70 | "/datatableview/static/**/*", 71 | "/datatableview/templates/**/*", 72 | ] 73 | 74 | [tool.black] 75 | line-length = 100 76 | target-version = ['py311'] 77 | include = '\.pyi?$' 78 | exclude = '(\.git|.venv|_build|build|dist|.*\/__pycache__\/)' 79 | 80 | [tool.ruff] 81 | line-length = 100 82 | lint.ignore = ["F401"] 83 | 84 | [tool.bandit] 85 | targets = ['datatableview', "demo_app"] 86 | exclude_dirs = ["datatableview/tests"] 87 | skips = ["B303", "B308", "B323", "B324", "B703"] 88 | 89 | [tool.coverage.run] 90 | branch = true 91 | command_line = "demo_app/manage.py test --noinput --settings=demo_app.settings_test datatableview" 92 | omit = [ 93 | "*/demo_app/**", 94 | "*/migrations/*", 95 | "*/tests/**", 96 | ] 97 | 98 | [tool.coverage.report] 99 | fail_under = 69 100 | precision = 1 101 | skip_covered = true 102 | skip_empty = true 103 | ignore_errors = true 104 | sort = "cover" 105 | 106 | [tool.bumper] 107 | exclude = [".idea", ".github", "demo_app"] 108 | version_files = ["datatableview/__init__.py"] 109 | repo = "pivotal-energy-solutions/django-datatable-view" 110 | report = "out.json" 111 | --------------------------------------------------------------------------------