├── .editorconfig ├── .env.example ├── .envrc ├── .gitattributes ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── SECURITY.md ├── config.yml ├── dependabot.yml └── workflows │ ├── build.yml │ ├── check-requirements.yml │ ├── check.yml │ ├── coveralls.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .python-version ├── AUTHORS ├── MANIFEST.in ├── MIT-LICENSE ├── Makefile ├── README.rst ├── read_only_admin ├── __init__.py ├── admin.py ├── apps.py ├── conf.py ├── locale │ ├── en │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── uk │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── models.py ├── signals.py ├── templates │ ├── admin │ │ └── pagination.html │ └── read_only_admin │ │ ├── change_form.html │ │ └── includes │ │ └── fieldset.html ├── templatetags │ ├── __init__.py │ └── read_only_admin_tags.py └── utils.py ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── settings.py ├── templatetags ├── __init__.py └── test_read_only_admin_tags.py ├── test_admin.py ├── test_signals.py └── test_utils.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # django-read-only-admin 2 | # .editorconfig 3 | 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | charset = utf-8 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | 15 | [*.py] 16 | indent_style = space 17 | indent_size = 4 18 | 19 | 20 | [Makefile] 21 | indent_style = tab 22 | 23 | 24 | [*.json] 25 | indent_style = space 26 | indent_size = 2 27 | 28 | 29 | [*.{yml,yaml}] 30 | indent_style = space 31 | indent_size = 2 32 | 33 | 34 | [*.ini] 35 | indent_style = space 36 | indent_size = 4 37 | 38 | 39 | [*.cfg] 40 | indent_style = space 41 | indent_size = 4 42 | 43 | 44 | [*.{htm,html}] 45 | indent_style = space 46 | indent_size = 4 47 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # django-read-only-admin 2 | # .env.example 3 | 4 | 5 | NAME=read_only_admin 6 | COVERALLS_REPO_TOKEN= 7 | TWINE_USERNAME=__token__ 8 | TWINE_PASSWORD= 9 | TEST_TWINE_PASSWORD= 10 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | # django-read-only-admin 2 | # .envrc 3 | 4 | 5 | dotenv 6 | layout python 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # django-read-only-admin 2 | # .gitattributes 3 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # django-read-only-admin 2 | # .github/CODEOWNERS 3 | 4 | 5 | * @vint21h 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ## Expected Behavior 8 | 9 | 10 | ## Current Behavior 11 | 12 | 13 | ## Possible Solution 14 | 15 | 16 | ## Steps to Reproduce 17 | 18 | 19 | 1. 20 | 2. 21 | 3. 22 | 23 | ## Context (Environment) 24 | 25 | 26 | 27 | 28 | 29 | ## Detailed Description 30 | 31 | 32 | ## Possible Implementation 33 | 34 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ## Description 8 | 9 | 10 | ## Related Issue 11 | 12 | 13 | 14 | 15 | ## Motivation and Context 16 | 17 | 18 | 19 | ## How Has This Been Tested? 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | # Security Policy 6 | 7 | ## Supported Versions 8 | 9 | So, because this is not a thing that can produce security problems in the project using it, 10 | security policy is very simple: supported only the last major or minor branch. 11 | 12 | | Version | Supported | 13 | |---------|--------------------| 14 | | 0.17.x | :white_check_mark: | 15 | 16 | ## Reporting a Vulnerability 17 | 18 | If you found something that in your opinion may produce any security problem, please create a [new issue](https://github.com/vint21h/django-read-only-admin/issues/new/). 19 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | # django-read-only-admin 2 | # .github/config.yml 3 | 4 | 5 | blank_issues_enabled: true 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # django-read-only-admin 2 | # .github/dependabot.yml 3 | 4 | 5 | version: 2 6 | updates: 7 | - package-ecosystem: "pip" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | allow: 12 | - dependency-name: "all" 13 | dependency-type: "direct" 14 | - package-ecosystem: "github-actions" 15 | directory: "/" 16 | schedule: 17 | interval: "daily" 18 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # django-read-only-admin 2 | # .github/workflows/build.yml 3 | 4 | 5 | name: "build" 6 | on: 7 | push: 8 | branches: 9 | - "*" 10 | tags: 11 | - "*" 12 | pull_request: 13 | branches: 14 | - "*" 15 | workflow_dispatch: 16 | workflow_call: 17 | env: 18 | NAME: "read_only_admin" 19 | DEFAULT_PYTHON_VERSION: "3.10" 20 | jobs: 21 | check-requirements: 22 | uses: "./.github/workflows/check-requirements.yml" 23 | check: 24 | name: "${{ matrix.toxenv }}" 25 | uses: "./.github/workflows/check.yml" 26 | test: 27 | name: "${{ matrix.python-version }}" 28 | uses: "./.github/workflows/test.yml" 29 | if: success() 30 | needs: ["check"] 31 | coveralls: 32 | uses: "./.github/workflows/coveralls.yml" 33 | if: success() 34 | needs: ["test", "check"] 35 | -------------------------------------------------------------------------------- /.github/workflows/check-requirements.yml: -------------------------------------------------------------------------------- 1 | # django-read-only-admin 2 | # .github/workflows/check-requirements.yml 3 | 4 | 5 | name: "check-requirements" 6 | on: 7 | schedule: 8 | - cron: "0 3 * * *" 9 | workflow_dispatch: 10 | workflow_call: 11 | env: 12 | DEFAULT_PYTHON_VERSION: "3.10" 13 | jobs: 14 | check-requirements: 15 | name: "check-requirements" 16 | runs-on: "ubuntu-latest" 17 | steps: 18 | - name: "Git clone" 19 | id: "git-clone" 20 | uses: "actions/checkout@v2" 21 | - name: "Set up Python ${{ env.DEFAULT_PYTHON_VERSION }}" 22 | id: "set-up-python" 23 | uses: "actions/setup-python@v2" 24 | with: 25 | python-version: "${{ env.DEFAULT_PYTHON_VERSION }}" 26 | - name: "Pip cache" 27 | id: "pip-cache" 28 | uses: "actions/cache@v2" 29 | with: 30 | path: "~/.cache/pip" 31 | key: "${{ runner.os }}-pip-${{ hashFiles('setup.cfg') }}" 32 | restore-keys: | 33 | "${{ runner.os }}-pip-" 34 | - name: "Install requirements" 35 | id: "install-requirements" 36 | run: "make install" 37 | - name: "Check outdated requirements" 38 | id: "check-outdated-requirements" 39 | continue-on-error: true 40 | run: "pip-outdated" 41 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | # django-read-only-admin 2 | # .github/workflows/check.yml 3 | 4 | 5 | name: "check" 6 | on: 7 | workflow_dispatch: 8 | workflow_call: 9 | env: 10 | NAME: "read_only_admin" 11 | DEFAULT_PYTHON_VERSION: "3.10" 12 | jobs: 13 | check: 14 | name: "${{ matrix.toxenv }}" 15 | runs-on: "ubuntu-latest" 16 | strategy: 17 | matrix: 18 | toxenv: ["check", "check-build"] 19 | steps: 20 | - name: "Git clone" 21 | id: "git-clone" 22 | uses: "actions/checkout@v2" 23 | - name: "Set up Python ${{ env.DEFAULT_PYTHON_VERSION }}" 24 | id: "set-up-python" 25 | uses: "actions/setup-python@v2" 26 | with: 27 | python-version: "${{ env.DEFAULT_PYTHON_VERSION }}" 28 | - name: "Pip cache" 29 | id: "pip-cache" 30 | uses: "actions/cache@v2" 31 | with: 32 | path: "~/.cache/pip" 33 | key: "${{ runner.os }}-pip-${{ matrix.toxenv }}-${{ hashFiles('setup.cfg') }}" 34 | restore-keys: | 35 | "${{ runner.os }}-pip-${{ matrix.toxenv }}-" 36 | "${{ runner.os }}-pip-" 37 | - name: "Pre-commit cache" 38 | id: "pre-commit-cache" 39 | uses: "actions/cache@v2" 40 | with: 41 | path: "~/.cache/pre-commit" 42 | key: "${{ runner.os }}-pre-commit-${{ matrix.toxenv }}-${{ hashFiles('.pre-commit-config.yaml') }}" 43 | restore-keys: | 44 | "${{ runner.os }}-pre-commit-${{ matrix.toxenv }}-" 45 | "${{ runner.os }}-pre-commit-" 46 | - name: "Install requirements" 47 | id: "install-requirements" 48 | run: "make install" 49 | - name: "Run ${{ matrix.toxenv }}" 50 | id: "run" 51 | run: "tox -e ${{ matrix.toxenv }}" 52 | -------------------------------------------------------------------------------- /.github/workflows/coveralls.yml: -------------------------------------------------------------------------------- 1 | # django-read-only-admin 2 | # .github/workflows/coveralls.yml 3 | 4 | 5 | name: "coveralls" 6 | on: 7 | workflow_dispatch: 8 | workflow_call: 9 | env: 10 | NAME: "read_only_admin" 11 | DEFAULT_PYTHON_VERSION: "3.10" 12 | jobs: 13 | coveralls: 14 | name: "coveralls" 15 | runs-on: "ubuntu-latest" 16 | steps: 17 | - name: "Git clone" 18 | id: "git-clone" 19 | uses: "actions/checkout@v2" 20 | - name: "Set up Python ${{ env.DEFAULT_PYTHON_VERSION }}" 21 | id: "set-up-python" 22 | uses: "actions/setup-python@v2" 23 | with: 24 | python-version: "${{ env.DEFAULT_PYTHON_VERSION }}" 25 | - name: "Pip cache" 26 | id: "pip-cache" 27 | uses: "actions/cache@v2" 28 | with: 29 | path: "~/.cache/pip" 30 | key: "${{ runner.os }}-pip-${{ hashFiles('setup.cfg') }}" 31 | restore-keys: | 32 | "${{ runner.os }}-pip-" 33 | - name: "Temporarily save coverage statistics" 34 | id: "temporarily-save-coverage-statistics" 35 | uses: "actions/cache@v2" 36 | with: 37 | path: ".coverage" 38 | key: ".coverage-${{ github.run_id }}-${{ github.run_number }}" 39 | - name: "Install requirements" 40 | id: "install-requirements" 41 | run: "make install" 42 | - name: "Upload coverage report to Coveralls" 43 | id: "upload-coverage-report-to-coveralls" 44 | env: 45 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 46 | run: "coveralls --service=github" 47 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # django-read-only-admin 2 | # .github/workflows/test.yml 3 | 4 | 5 | name: "test" 6 | on: 7 | workflow_dispatch: 8 | workflow_call: 9 | env: 10 | NAME: "read_only_admin" 11 | jobs: 12 | test: 13 | name: "${{ matrix.python-version }}" 14 | runs-on: "ubuntu-latest" 15 | strategy: 16 | matrix: 17 | python-version: ["3.7", "3.8", "3.9", "3.10"] 18 | steps: 19 | - name: "Git clone" 20 | id: "git-clone" 21 | uses: "actions/checkout@v2" 22 | - name: "Set up Python ${{ matrix.python-version }}" 23 | id: "set-up-python" 24 | uses: "actions/setup-python@v2" 25 | with: 26 | python-version: "${{ matrix.python-version }}" 27 | - name: "Pip cache" 28 | id: "pip-cache" 29 | uses: "actions/cache@v2" 30 | with: 31 | path: "~/.cache/pip" 32 | key: "${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('setup.cfg') }}" 33 | restore-keys: | 34 | "${{ runner.os }}-pip-${{ matrix.python-version }}-" 35 | "${{ runner.os }}-pip-" 36 | - name: "Temporarily save coverage statistics" 37 | id: "temporarily-save-coverage-statistics" 38 | uses: "actions/cache@v2" 39 | with: 40 | path: ".coverage" 41 | key: ".coverage-${{ github.run_id }}-${{ github.run_number }}" 42 | - name: "Install requirements" 43 | id: "install-requirements" 44 | run: "make install" 45 | - name: "Run tests" 46 | id: "run-tests" 47 | env: 48 | PLATFORM: "${{ matrix.platform }}" 49 | run: "make tox" 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # django-read-only-admin 2 | # .gitignore 3 | 4 | 5 | # Autogenerated 6 | .*.swp 7 | *.pyc 8 | *.log 9 | *.pot 10 | *.pyo 11 | *.pyd 12 | *.pyx 13 | *.pyi 14 | __pycache__/* 15 | *~ 16 | *.orig 17 | *.bak 18 | .DS_Store 19 | *.save 20 | .mypy_cache/* 21 | .pytest_cache/* 22 | 23 | 24 | # IDE's 25 | .idea/* 26 | *sublime* 27 | *geany* 28 | *komodo* 29 | .vscode/* 30 | .project 31 | .pydevproject 32 | .settings/* 33 | 34 | 35 | # Project 36 | build/* 37 | dist/* 38 | *.egg-info/* 39 | .tox/* 40 | tmp/* 41 | log/* 42 | parts 43 | media/* 44 | *env*/ 45 | static/* 46 | *.pid 47 | .credentials 48 | .env 49 | .coverage 50 | htmlcov 51 | .direnv 52 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # django-read-only-admin 2 | # .pre-commit-config.yaml 3 | 4 | 5 | repos: 6 | - repo: "local" 7 | hooks: 8 | - id: "seed-isort-config" 9 | name: "seed-isort-config" 10 | stages: ["commit"] 11 | language: "system" 12 | pass_filenames: false 13 | entry: "seed-isort-config" 14 | types: ["python"] 15 | - id: "removestar" 16 | name: "removestar" 17 | stages: ["commit"] 18 | language: "system" 19 | entry: "removestar -i \"${NAME}\" tests" 20 | types: ["python"] 21 | - id: "isort" 22 | name: "isort" 23 | stages: ["commit"] 24 | language: "system" 25 | entry: "isort" 26 | types: ["python"] 27 | - id: "black" 28 | name: "black" 29 | stages: ["commit"] 30 | language: "system" 31 | entry: "bash -c 'black \"${NAME}\"'" 32 | types: ["python"] 33 | - id: "yesqa" 34 | name: "yesqa" 35 | stages: ["commit"] 36 | language: "system" 37 | pass_filenames: false 38 | entry: "yesqa" 39 | types: ["python"] 40 | - id: "flake8" 41 | name: "flake8" 42 | stages: ["commit"] 43 | language: "system" 44 | entry: "flake8" 45 | types: ["python"] 46 | - id: "interrogate" 47 | name: "interrogate" 48 | stages: ["commit"] 49 | language: "system" 50 | entry: "bash -c 'interrogate \"${NAME}\" -I -M'" 51 | types: ["python"] 52 | - id: "pylint" 53 | name: "pylint" 54 | stages: ["commit"] 55 | language: "system" 56 | entry: "pylint \"${NAME}\" tests" 57 | types: ["python"] 58 | - id: "bandit" 59 | name: "bandit" 60 | stages: ["commit"] 61 | language: "system" 62 | entry: "bandit -r \"${NAME}\" tests --ini=setup.cfg" 63 | types: ["python"] 64 | pass_filenames: false 65 | - id: "dodgy" 66 | name: "dodgy" 67 | stages: ["commit"] 68 | language: "system" 69 | entry: "dodgy" 70 | types: ["python"] 71 | pass_filenames: false 72 | - id: "darglint" 73 | name: "darglint" 74 | stages: ["commit"] 75 | language: "system" 76 | entry: "darglint" 77 | types: ["python"] 78 | - id: "polint" 79 | name: "polint" 80 | stages: ["commit"] 81 | language: "system" 82 | entry: "polint --ignore=location,unsorted --show-msg" 83 | types: ["pofile"] 84 | pass_filenames: true 85 | - id: "dennis" 86 | name: "dennis" 87 | stages: ["commit"] 88 | language: "system" 89 | entry: "dennis-cmd lint" 90 | types: ["pofile"] 91 | pass_filenames: true 92 | - id: "curlylint" 93 | name: "curlylint" 94 | stages: ["commit"] 95 | language: "system" 96 | entry: "curlylint" 97 | types: ["html", "xml"] 98 | pass_filenames: true 99 | - id: "mypy" 100 | name: "mypy" 101 | stages: ["commit"] 102 | language: "system" 103 | entry: "bash -c 'PYTHONPATH=\"${PYTHONPATH}:${PWD}\" mypy'" 104 | types: ["python"] 105 | pass_filenames: false 106 | - id: "check-manifest" 107 | name: "check-manifest" 108 | stages: ["commit"] 109 | language: "system" 110 | pass_filenames: false 111 | entry: "check-manifest" 112 | - id: "pyroma" 113 | name: "pyroma" 114 | stages: ["commit"] 115 | language: "system" 116 | pass_filenames: false 117 | entry: "pyroma -d ." 118 | types: ["python"] 119 | - id: "check-wheel-contents" 120 | name: "check-wheel-contents" 121 | stages: ["commit"] 122 | language: "system" 123 | pass_filenames: false 124 | entry: "bash -c 'make build -B && check-wheel-contents dist'" 125 | types: ["python"] 126 | - id: "dotenv-linter" 127 | name: "dotenv-linter" 128 | entry: "dotenv-linter" 129 | language: "system" 130 | stages: ["commit"] 131 | types: ["text"] 132 | files: '\.env*' 133 | exclude: ".envrc" 134 | - repo: "https://github.com/pre-commit/pre-commit-hooks" 135 | rev: "v4.1.0" 136 | hooks: 137 | - id: "check-added-large-files" 138 | - id: "check-ast" 139 | - id: "check-case-conflict" 140 | - id: "check-docstring-first" 141 | - id: "check-executables-have-shebangs" 142 | - id: "check-merge-conflict" 143 | - id: "check-symlinks" 144 | - id: "check-yaml" 145 | - id: "debug-statements" 146 | - id: "detect-private-key" 147 | - id: "end-of-file-fixer" 148 | - id: "forbid-new-submodules" 149 | - id: "trailing-whitespace" 150 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.10.2 2 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # django-read-only-admin 2 | # AUTHORS 3 | 4 | 5 | Authors ordered by first contribution. 6 | 7 | Vinit Kumar 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # django-read-only-admin 2 | # MANIFEST.in 3 | 4 | 5 | prune tests* 6 | recursive-include read_only_admin *.po 7 | recursive-include read_only_admin *.mo 8 | include read_only_admin/templates/read_only_admin/change_form.html 9 | include read_only_admin/templates/read_only_admin/includes/fieldset.html 10 | include read_only_admin/templates/admin/pagination.html 11 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022, Alexei Andrushievich 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # django-read-only-admin 2 | # Makefile 3 | 4 | 5 | .ONESHELL: 6 | PHONY: install tox test makemessages compilemessages bumpversion build sign check check-build check-upload upload clean coveralls release help 7 | TEST_PYPI_URL ?= https://test.pypi.org/legacy/ 8 | NAME ?= read_only_admin 9 | EXTENSIONS ?= py,html,txt,xml 10 | TRASH_DIRS ?= build dist *.egg-info .tox .mypy_cache .pytest_cache __pycache__ htmlcov 11 | TRASH_FILES ?= .coverage 12 | BUILD_TYPES ?= bdist_wheel sdist 13 | VERSION ?= `python -c "import configparser; config = configparser.ConfigParser(); config.read('setup.cfg'); print(config['metadata']['version']);"` 14 | 15 | 16 | install: 17 | pip install .[test];\ 18 | 19 | 20 | tox: 21 | tox;\ 22 | 23 | 24 | test: 25 | bash -c 'PYTHONPATH="$${PYTHONPATH}:$${PWD}" py.test --cov=$(NAME) --modules-durations=0 --functions-durations=0 --instafail $(TESTS)';\ 26 | 27 | 28 | makemessages: 29 | for locale in `ls $(NAME)/locale`; do\ 30 | django-admin makemessages --locale=$${locale} --extension=$(EXTENSIONS);\ 31 | done;\ 32 | 33 | 34 | compilemessages: 35 | django-admin compilemessages;\ 36 | 37 | 38 | bumpversion: 39 | git tag -a $(VERSION) -m "v$(VERSION)";\ 40 | 41 | 42 | build: 43 | python setup.py $(BUILD_TYPES);\ 44 | 45 | 46 | sign: 47 | for package in `ls dist`; do\ 48 | gpg -a --detach-sign dist/$${package};\ 49 | done;\ 50 | 51 | 52 | check: 53 | bash -c 'NAME="$(NAME)" pre-commit run --all-files';\ 54 | 55 | 56 | check-build: 57 | twine check dist/*;\ 58 | 59 | 60 | check-upload: 61 | twine upload --skip-existing -s --repository-url $(TEST_PYPI_URL) -u __token__ -p $${TEST_TWINE_PASSWORD} dist/*;\ 62 | 63 | 64 | upload: 65 | twine upload --skip-existing -s dist/*;\ 66 | 67 | 68 | clean: 69 | for file in $(TRASH_FILES); do\ 70 | find -iname $${file} -print0 | xargs -0 rm -rf;\ 71 | done;\ 72 | for dir in $(TRASH_DIRS); do\ 73 | find -type d -name $${dir} ! -path "*/.direnv/*" -print0 | xargs -0 rm -rf;\ 74 | done;\ 75 | 76 | 77 | coveralls: 78 | coveralls;\ 79 | 80 | 81 | release: 82 | make clean && \ 83 | make bumpversion && \ 84 | git co master && \ 85 | git merge dev && \ 86 | git co dev && \ 87 | git push --all && \ 88 | git push --tags && \ 89 | make build && \ 90 | make sign && \ 91 | make check-build && \ 92 | make check-upload && \ 93 | make upload && \ 94 | make clean;\ 95 | 96 | 97 | help: 98 | @echo " help:" 99 | @echo " Show this help." 100 | @echo " install:" 101 | @echo " Install requirements." 102 | @echo " tox:" 103 | @echo " Run tox." 104 | @echo " test:" 105 | @echo " Run tests, can specify tests with 'TESTS' variable." 106 | @echo " makemessages:" 107 | @echo " Harvest translations." 108 | @echo " compilemessages:" 109 | @echo " Compile translations." 110 | @echo " bumpversion:" 111 | @echo " Tag current code revision with version." 112 | @echo " build:" 113 | @echo " Build python packages, can specify packages types with 'BUILD_TYPES' variable." 114 | @echo " sign:" 115 | @echo " Sign python packages." 116 | @echo " check:" 117 | @echo " Perform some code checks." 118 | @echo " check-build:" 119 | @echo " Run twine checks." 120 | @echo " check-upload:" 121 | @echo " Upload package to test PyPi using twine." 122 | @echo " upload:" 123 | @echo " Upload package to PyPi using twine." 124 | @echo " clean:" 125 | @echo " Recursively delete useless autogenerated files and directories, directories and files lists can be overriden through 'TRASH_DIRS' and 'TRASH_FILES' variables." 126 | @echo " coveralls:" 127 | @echo " Upload coverage report to Coveralls." 128 | @echo " release:" 129 | @echo " Release code." 130 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. django-read-only-admin 2 | .. README.rst 3 | 4 | 5 | A django-read-only-admin documentation 6 | ====================================== 7 | 8 | |GitHub|_ |Coveralls|_ |pypi-license|_ |pypi-version|_ |pypi-python-version|_ |pypi-django-version|_ |pypi-format|_ |pypi-wheel|_ |pypi-status|_ 9 | 10 | *django-read-only-admin is a Django reusable application that fully implement read only admin* 11 | 12 | .. contents:: 13 | 14 | Installation 15 | ------------ 16 | * Obtain your copy of source code from the git repository: ``$ git clone https://github.com/vint21h/django-read-only-admin.git``. Or download the latest release from https://github.com/vint21h/django-read-only-admin/tags/. 17 | * Run ``$ python ./setup.py install`` from the repository source tree or the unpacked archive. Or use pip: ``$ pip install django-read-only-admin``. 18 | 19 | Configuration 20 | ------------- 21 | * Add ``"read_only_admin"`` to ``settings.INSTALLED_APPS``. 22 | 23 | .. code-block:: python 24 | 25 | # settings.py 26 | 27 | INSTALLED_APPS += [ 28 | "read_only_admin", 29 | ] 30 | 31 | * Run ``$ python ./manage.py migrate``. 32 | * Then add ``user/group`` ``change/delete/add/readonly`` model permissions. 33 | 34 | Settings 35 | -------- 36 | ``READ_ONLY_ADMIN_PERMISSION_PREFIX`` 37 | Read-only permission prefix. Defaults to: ``"readonly"``. 38 | 39 | ``READ_ONLY_ADMIN_PERMISSION_NAME_PREFIX`` 40 | Read-only permission name prefix. Defaults to: ``"Read only"``. 41 | 42 | ``READ_ONLY_ADMIN_EMPTY_ACTIONS`` 43 | Empty admin actions list (exclude superusers) or just remove delete selected action. Defaults to: ``True``. 44 | 45 | Usage 46 | ----- 47 | Just inherit your custom Django admin class from ``read_only_admin.admin.ReadonlyAdmin``. 48 | 49 | .. code-block:: python 50 | 51 | # admin.py 52 | 53 | from read_only_admin.admin import ReadonlyAdmin 54 | 55 | 56 | class MyCustomAdmin(ReadonlyAdmin): 57 | 58 | ... 59 | 60 | Also tabular and stacked inlines are supported. 61 | 62 | .. code-block:: python 63 | 64 | # admin.py 65 | 66 | from read_only_admin.admin import ( 67 | ReadonlyStackedInline, 68 | ReadonlyTabularInline, 69 | ) 70 | 71 | 72 | class MyCustomTabularInline(ReadonlyTabularInline): 73 | 74 | model: Type[Model] = MyModel 75 | extra: int = 0 76 | 77 | 78 | class MyCustomStackedInline(ReadonlyStackedInline): 79 | 80 | model: Type[Model] = MyModel 81 | extra: int = 0 82 | 83 | If you use ``list_editable`` in your custom admin classes, copy ``read_only_admin/templates/admin/pagination.html`` to your project ``templates/admin`` directory. 84 | 85 | Contributing 86 | ------------ 87 | 1. `Fork it `_ 88 | 2. Install `GNU Make `_ 89 | 3. Install and configure `pyenv `_ and `pyenv-virtualenv plugin `_ 90 | 4. Install and configure `direnv `_ 91 | 5. Create environment config from example 92 | 93 | .. code-block:: bash 94 | 95 | cp .env.example .env 96 | 97 | 6. Install development dependencies: 98 | 99 | .. code-block:: bash 100 | 101 | make install 102 | 103 | 7. Create your fix/feature branch: 104 | 105 | .. code-block:: bash 106 | 107 | git checkout -b my-new-fix-or-feature 108 | 109 | 8. Check code style and moreover: 110 | 111 | .. code-block:: bash 112 | 113 | make check 114 | 115 | 9. Run tests: 116 | 117 | .. code-block:: bash 118 | 119 | make test 120 | 121 | 10. Push to the branch: 122 | 123 | .. code-block:: bash 124 | 125 | git push origin my-new-fix-or-feature 126 | 127 | 11. `Create a new Pull Request `_ 128 | 129 | Licensing 130 | --------- 131 | django-read-only-admin uses the MIT license. Please check the MIT-LICENSE file for more details. 132 | 133 | Some part of code fairly stolen from teh internets with reference to the source. So, if you author of this code, please contact me. 134 | 135 | Contacts 136 | -------- 137 | **Project Website**: https://github.com/vint21h/django-read-only-admin/ 138 | 139 | **Author**: Alexei Andrushievich 140 | 141 | For other authors list see AUTHORS file. 142 | 143 | .. |GitHub| image:: https://github.com/vint21h/django-read-only-admin/workflows/build/badge.svg 144 | :alt: GitHub 145 | .. |Coveralls| image:: https://coveralls.io/repos/github/vint21h/django-read-only-admin/badge.svg?branch=master 146 | :alt: Coveralls 147 | .. |pypi-license| image:: https://img.shields.io/pypi/l/django-read-only-admin 148 | :alt: License 149 | .. |pypi-version| image:: https://img.shields.io/pypi/v/django-read-only-admin 150 | :alt: Version 151 | .. |pypi-django-version| image:: https://img.shields.io/pypi/djversions/django-read-only-admin 152 | :alt: Supported Django version 153 | .. |pypi-python-version| image:: https://img.shields.io/pypi/pyversions/django-read-only-admin 154 | :alt: Supported Python version 155 | .. |pypi-format| image:: https://img.shields.io/pypi/format/django-read-only-admin 156 | :alt: Package format 157 | .. |pypi-wheel| image:: https://img.shields.io/pypi/wheel/django-read-only-admin 158 | :alt: Python wheel support 159 | .. |pypi-status| image:: https://img.shields.io/pypi/status/django-read-only-admin 160 | :alt: Package status 161 | .. _GitHub: https://github.com/vint21h/django-read-only-admin/actions/ 162 | .. _Coveralls: https://coveralls.io/github/vint21h/django-read-only-admin?branch=master 163 | .. _pypi-license: https://pypi.org/project/django-read-only-admin/ 164 | .. _pypi-version: https://pypi.org/project/django-read-only-admin/ 165 | .. _pypi-django-version: https://pypi.org/project/django-read-only-admin/ 166 | .. _pypi-python-version: https://pypi.org/project/django-read-only-admin/ 167 | .. _pypi-format: https://pypi.org/project/django-read-only-admin/ 168 | .. _pypi-wheel: https://pypi.org/project/django-read-only-admin/ 169 | .. _pypi-status: https://pypi.org/project/django-read-only-admin/ 170 | -------------------------------------------------------------------------------- /read_only_admin/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # django-read-only-admin 4 | # read_only_admin/__init__.py 5 | 6 | 7 | from typing import List 8 | 9 | 10 | __all__: List[str] = ["default_app_config"] 11 | 12 | 13 | default_app_config: str = "read_only_admin.apps.DjangoReadOnlyAdminConfig" 14 | -------------------------------------------------------------------------------- /read_only_admin/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # django-read-only-admin 4 | # read_only_admin/admin.py 5 | 6 | 7 | from functools import partial 8 | from collections import OrderedDict 9 | from typing import Any, Dict, List, Type, Tuple, Union, Callable, Optional 10 | 11 | from django.db.models import Model 12 | from django.http import HttpRequest 13 | from django import __version__ as django_version 14 | from django.core.handlers.wsgi import WSGIRequest 15 | from django.contrib.admin.views.main import ChangeList 16 | from django.contrib.auth import get_permission_codename 17 | from django.contrib.admin.utils import flatten_fieldsets 18 | from django.contrib.admin.filters import SimpleListFilter 19 | from django.contrib.admin import ModelAdmin, TabularInline 20 | from django.forms.models import BaseModelFormSet, modelformset_factory 21 | 22 | from read_only_admin.conf import settings 23 | from read_only_admin.utils import get_read_only_permission_codename 24 | 25 | 26 | __all__: List[str] = [ 27 | "ReadonlyAdmin", 28 | "ReadonlyStackedInline", 29 | "ReadonlyTabularInline", 30 | ] 31 | 32 | 33 | class ReadonlyChangeList(ChangeList): 34 | """Readonly admin change list.""" 35 | 36 | def __init__( # noqa: CFQ002, CCR001 37 | self, 38 | request: WSGIRequest, 39 | model: Type[Model], 40 | list_display: Union[List[Union[Callable, str]], Tuple[str]], # type: ignore 41 | list_display_links: Optional[ # type: ignore 42 | Union[List[Callable], List[str], Tuple[str]] 43 | ], 44 | list_filter: Union[List[Type[SimpleListFilter]], List[str], Tuple[str]], 45 | date_hierarchy: Optional[str], 46 | search_fields: Union[List[str], Tuple[str]], 47 | list_select_related: Union[Tuple[str], bool], 48 | list_per_page: int, 49 | list_max_show_all: int, 50 | list_editable: Union[List[str], Tuple[str]], 51 | model_admin: ModelAdmin, # type: ignore 52 | sortable_by: Union[List[Callable], List[str], Tuple[str]], # type: ignore 53 | *args: List[Any], 54 | **kwargs: Dict[str, Any], 55 | ) -> None: 56 | """ 57 | Overridden to set extra readonly property. 58 | 59 | :param request: django WSGI request object 60 | :type request: WSGIRequest 61 | :param model: django related model 62 | :type model: Type[Model] 63 | :param list_display: list of fields to display 64 | :type list_display: Union[List[Union[Callable, str]], Tuple[str]] 65 | :param list_display_links: list of fields to display as links 66 | :type list_display_links: Optional[Union[List[Callable], List[str], Tuple[str]]] 67 | :param list_filter: list of fields by which can be filtering do 68 | :type list_filter: Union[List[Type[SimpleListFilter]], List[str], Tuple[str]] 69 | :param date_hierarchy: generate date hierarchy for field name 70 | :type date_hierarchy: Optional[str] 71 | :param search_fields: list of fields by which can be search do 72 | :type search_fields: Union[List[str], Tuple[str]] 73 | :param list_select_related: select related fields 74 | :type list_select_related: Union[Tuple[str], bool] 75 | :param list_per_page: items on page number 76 | :type list_per_page: int 77 | :param list_max_show_all: how many items can appear on a show all change list page 78 | :type list_max_show_all: int 79 | :param list_editable: list of inline editable fields 80 | :type list_editable: Union[List[str], Tuple[str]] 81 | :param model_admin: django related admin 82 | :type model_admin: ModelAdmin 83 | :param sortable_by: brute enable/disable sorting for list of fields 84 | :type sortable_by: Union[List[Callable], List[str], Tuple[str]] 85 | :param args: additional args 86 | :type args: List[Any] 87 | :param kwargs: additional args 88 | :type kwargs: Dict[str, Any] 89 | """ # noqa: E501 90 | # dealing with Django 4.x backward incompatibility 91 | if django_version.startswith("4"): 92 | super(ReadonlyChangeList, self).__init__( 93 | request=request, 94 | model=model, 95 | list_display=list_display, 96 | list_display_links=list_display_links, 97 | list_filter=list_filter, 98 | date_hierarchy=date_hierarchy, 99 | search_fields=search_fields, 100 | list_select_related=list_select_related, 101 | list_per_page=list_per_page, 102 | list_max_show_all=list_max_show_all, 103 | list_editable=list_editable, 104 | model_admin=model_admin, 105 | sortable_by=sortable_by, 106 | search_help_text=kwargs.get("search_help_text", ""), # type: ignore 107 | ) 108 | else: 109 | super(ReadonlyChangeList, self).__init__( # pylint: disable=E1120 110 | request=request, 111 | model=model, 112 | list_display=list_display, 113 | list_display_links=list_display_links, 114 | list_filter=list_filter, 115 | date_hierarchy=date_hierarchy, 116 | search_fields=search_fields, 117 | list_select_related=list_select_related, 118 | list_per_page=list_per_page, 119 | list_max_show_all=list_max_show_all, 120 | list_editable=list_editable, 121 | model_admin=model_admin, 122 | sortable_by=sortable_by, 123 | ) 124 | 125 | self.readonly = False 126 | 127 | for permission in request.user.get_all_permissions(): 128 | head, sep, tail = permission.partition( # pylint: disable=W0612 129 | "." 130 | ) # type: str, str, str 131 | if ( 132 | get_read_only_permission_codename(model=self.model.__name__.lower()) 133 | == tail # noqa: W503 134 | ) and ( 135 | request.user.has_perm(permission) 136 | and not request.user.is_superuser # noqa: W503 137 | ): 138 | self.readonly = True 139 | 140 | 141 | class ReadonlyAdmin(ModelAdmin): # type: ignore 142 | """Readonly admin.""" 143 | 144 | change_form_template: str = "read_only_admin/change_form.html" 145 | 146 | def get_changelist( # pylint: disable=R0201 147 | self, request: HttpRequest, **kwargs: Dict[str, Any] 148 | ) -> Type[ReadonlyChangeList]: 149 | """ 150 | Returns the ReadonlyChangeList class for use on the changelist page. 151 | 152 | :param request: django HTTP request object 153 | :type request: HttpRequest 154 | :param kwargs: additional args 155 | :type kwargs: Dict[str, Any] 156 | :return: readonly change list 157 | :rtype: ReadonlyChangeList 158 | """ 159 | return ReadonlyChangeList 160 | 161 | def get_changelist_formset( 162 | self, request: HttpRequest, **kwargs: Dict[str, Any] 163 | ) -> Type[BaseModelFormSet]: 164 | """ 165 | Empty FormSet class for use on the changelist page if list_editable and readonly permission is used. 166 | 167 | :param request: django HTTP request object 168 | :type request: HttpRequest 169 | :param kwargs: additional args 170 | :type kwargs: Dict[str, Any] 171 | :return: FormSet for changelist 172 | :rtype: BaseModelFormSet 173 | """ # noqa: E501 174 | for permission in request.user.get_all_permissions(): 175 | head, sep, tail = permission.partition( # pylint: disable=W0612 176 | "." 177 | ) # type: str, str, str 178 | if ( 179 | get_read_only_permission_codename(model=self.model.__name__.lower()) 180 | == tail # noqa: W503 181 | ) and ( 182 | request.user.has_perm(permission) 183 | and not request.user.is_superuser # noqa: W503 184 | ): 185 | defaults = { 186 | "formfield_callback": partial( 187 | self.formfield_for_dbfield, request=request 188 | ) 189 | } 190 | defaults.update(kwargs) # type: ignore 191 | 192 | return modelformset_factory( 193 | self.model, 194 | self.get_changelist_form(request), 195 | extra=0, 196 | fields=(), 197 | **defaults, # type: ignore 198 | ) 199 | 200 | return super(ReadonlyAdmin, self).get_changelist_formset( 201 | request=request, **kwargs 202 | ) 203 | 204 | def get_readonly_fields( # noqa: CCR001 205 | self, request: HttpRequest, obj: Optional[Model] = None 206 | ) -> Union[List[str], Tuple[str]]: 207 | """ 208 | Get readonly fields. 209 | 210 | Get from: https://github.com/anupamshakya7/django-admin-hack/. 211 | 212 | :param request: django HTTP request object 213 | :type request: HttpRequest 214 | :param obj: an object 215 | :type obj: Model 216 | :return: readonly fields 217 | :rtype: Union[List[str], Tuple[str]] 218 | """ 219 | for permission in request.user.get_all_permissions(): 220 | head, sep, tail = permission.partition( # pylint: disable=W0612 221 | "." 222 | ) # type: str, str, str 223 | if ( 224 | get_read_only_permission_codename(model=self.model.__name__.lower()) 225 | == tail # noqa: W503 226 | ) and ( 227 | request.user.has_perm(permission) 228 | and not request.user.is_superuser # noqa: W503 229 | ): 230 | if self.get_fieldsets(request=request, obj=obj): 231 | 232 | return flatten_fieldsets( # type: ignore 233 | self.get_fieldsets(request=request, obj=obj) 234 | ) 235 | else: 236 | 237 | return list( 238 | set( 239 | [field.name for field in self.opts.local_fields] 240 | + [ # noqa: W503 241 | field.name for field in self.opts.local_many_to_many 242 | ] 243 | ) 244 | ) 245 | 246 | return self.readonly_fields # type: ignore 247 | 248 | def get_actions( # noqa: CCR001 249 | self, request: HttpRequest 250 | ) -> "OrderedDict[str, Any]": 251 | """ 252 | Get actions. 253 | 254 | Get from: https://vinitkumar.me/articles/2014/05/18/Get-Readonly-Mode-IN-Django.html. 255 | 256 | :param request: django HTTP request object 257 | :type request: HttpRequest 258 | :return: admin actions 259 | :rtype: OrderedDict[str, Any] 260 | """ # noqa: E501 261 | actions = super(ReadonlyAdmin, self).get_actions(request) 262 | label: str = self.model._meta.app_label 263 | model: str = self.model.__name__.lower() 264 | perm = f"{label}.{settings.READ_ONLY_ADMIN_PERMISSION_PREFIX}_{model}" # noqa: E501 265 | if ( # noqa: SIM102 266 | request.user.has_perm(perm) and not request.user.is_superuser 267 | ): 268 | if "delete_selected" in actions: 269 | del actions["delete_selected"] 270 | 271 | return ( 272 | OrderedDict() 273 | if settings.READ_ONLY_ADMIN_EMPTY_ACTIONS and not request.user.is_superuser 274 | else actions 275 | ) 276 | 277 | 278 | class ReadonlyInline(TabularInline): # type: ignore 279 | """Readonly admin inline.""" 280 | 281 | def has_add_permission( # pylint: disable=W0221 # noqa: CCR001 282 | self, request: HttpRequest, obj: Optional[Model] = None 283 | ) -> bool: 284 | """ 285 | Overridden for custom readonly permission. 286 | 287 | :param request: django HTTP request object 288 | :type request: HttpRequest 289 | :param obj: an object 290 | :type obj: Model 291 | :return: has user add permission 292 | :rtype: bool 293 | """ 294 | if self.opts.auto_created: 295 | # We're checking the rights to an auto-created intermediate model, 296 | # which doesn't have its own individual permissions. The user needs 297 | # to have the change permission for the related model in order to 298 | # be able to do anything with the intermediate model. 299 | return self.has_change_permission(request, obj) 300 | 301 | for permission in request.user.get_all_permissions(): 302 | head, sep, tail = permission.partition( # pylint: disable=W0612 303 | "." 304 | ) # type: str, str, str 305 | if ( 306 | get_read_only_permission_codename(model=self.model.__name__.lower()) 307 | == tail # noqa: W503 308 | ) and ( 309 | request.user.has_perm(permission) 310 | and not request.user.is_superuser # noqa: W503 311 | ): 312 | 313 | return False 314 | 315 | codename = get_permission_codename("add", self.opts) 316 | 317 | return request.user.has_perm(f"{self.opts.app_label}.{codename}") 318 | 319 | def has_delete_permission(self, request, obj=None) -> bool: # noqa: CCR001 320 | """ 321 | Overridden for custom readonly permission. 322 | 323 | :param request: django HTTP request object 324 | :type request: HttpRequest 325 | :param obj: an object 326 | :type obj: Model 327 | :return: has user delete permission 328 | :rtype: bool 329 | """ 330 | if self.opts.auto_created: 331 | # We're checking the rights to an auto-created intermediate model, 332 | # which doesn't have its own individual permissions. The user needs 333 | # to have the change permission for the related model in order to 334 | # be able to do anything with the intermediate model. 335 | return self.has_change_permission(request, obj) 336 | 337 | for permission in request.user.get_all_permissions(): 338 | head, sep, tail = permission.partition( # pylint: disable=W0612 339 | "." 340 | ) # type: str, str, str 341 | if ( 342 | get_read_only_permission_codename(model=self.model.__name__.lower()) 343 | == tail # noqa: W503 344 | ) and ( 345 | request.user.has_perm(permission) 346 | and not request.user.is_superuser # noqa: W503 347 | ): 348 | 349 | return False 350 | 351 | codename = get_permission_codename("delete", self.opts) 352 | 353 | return request.user.has_perm(f"{self.opts.app_label}.{codename}") 354 | 355 | def get_readonly_fields(self, request, obj=None) -> Union[List[str], Tuple[str]]: 356 | """ 357 | Get readonly fields. 358 | 359 | Get from: https://github.com/anupamshakya7/django-admin-hack/. 360 | 361 | :param request: django HTTP request object 362 | :type request: HttpRequest 363 | :param obj: an object 364 | :type obj: Model 365 | :return: readonly fields 366 | :rtype: Union[List[str], Tuple[str]] 367 | """ 368 | for permission in request.user.get_all_permissions(): 369 | head, sep, tail = permission.partition( # pylint: disable=W0612 370 | "." 371 | ) # type: str, str, str 372 | if ( 373 | get_read_only_permission_codename(model=self.model.__name__.lower()) 374 | == tail # noqa: W503 375 | ) and ( 376 | request.user.has_perm(permission) 377 | and not request.user.is_superuser # noqa: W503 378 | ): 379 | 380 | return list( 381 | set( 382 | [field.name for field in self.opts.local_fields] 383 | + [ # noqa: W503 384 | field.name for field in self.opts.local_many_to_many 385 | ] 386 | ) 387 | ) 388 | 389 | return self.readonly_fields # type: ignore 390 | 391 | 392 | class ReadonlyStackedInline(ReadonlyInline): 393 | """Stacked readonly inline.""" 394 | 395 | template: str = "admin/edit_inline/stacked.html" 396 | 397 | 398 | class ReadonlyTabularInline(ReadonlyInline): 399 | """Tabular readonly inline.""" 400 | 401 | template: str = "admin/edit_inline/tabular.html" 402 | -------------------------------------------------------------------------------- /read_only_admin/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # django-read-only-admin 4 | # read_only_admin/apps.py 5 | 6 | 7 | from typing import List 8 | 9 | from django.apps import AppConfig 10 | from django.utils.translation import gettext_lazy as _ 11 | 12 | 13 | __all__: List[str] = ["DjangoReadOnlyAdminConfig"] 14 | 15 | 16 | class DjangoReadOnlyAdminConfig(AppConfig): 17 | """Application config.""" 18 | 19 | name: str = "read_only_admin" 20 | verbose_name: str = _("Django read only admin") 21 | -------------------------------------------------------------------------------- /read_only_admin/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # django-read-only-admin 4 | # read_only_admin/conf.py 5 | 6 | 7 | from typing import List 8 | 9 | from appconf import AppConf 10 | from django.conf import settings 11 | 12 | 13 | __all__: List[str] = ["settings"] 14 | 15 | 16 | class DjangoReadOnlyAdminAppConf(AppConf): 17 | """Django read only admin settings.""" 18 | 19 | PERMISSION_PREFIX: str = getattr( 20 | settings, "READ_ONLY_ADMIN_PERMISSION_PREFIX", "readonly" 21 | ) 22 | PERMISSION_NAME_PREFIX: str = getattr( 23 | settings, "READ_ONLY_ADMIN_PERMISSION_NAME_PREFIX", "Read only" 24 | ) 25 | EMPTY_ACTIONS: bool = getattr(settings, "READ_ONLY_ADMIN_EMPTY_ACTIONS", True) 26 | 27 | class Meta: 28 | """Config settings.""" 29 | 30 | prefix: str = "read_only_admin" 31 | -------------------------------------------------------------------------------- /read_only_admin/locale/en/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vint21h/django-read-only-admin/e3edebb2081a50b9da708e06d3cbd5fb953cf66c/read_only_admin/locale/en/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /read_only_admin/locale/en/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # django-read-only-admin 2 | # read_only_admin/locale/en/LC_MESSAGES/django.po 3 | # 4 | #, fuzzy 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: django-read-only-admin 0.17.5\n" 8 | "Report-Msgid-Bugs-To: Alexei Andrushievich \n" 9 | "POT-Creation-Date: 2022-02-19 22:28+0200\n" 10 | "PO-Revision-Date: 2019-08-26 16:48+0300\n" 11 | "Last-Translator: Alexei Andrushievich \n" 12 | "Language-Team: Alexei Andrushievich \n" 13 | "Language: en\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | 18 | #: read_only_admin/apps.py:20 19 | msgid "Django read only admin" 20 | msgstr "Django read only admin" 21 | 22 | #: read_only_admin/templates/admin/pagination.html:16 23 | msgid "Show all" 24 | msgstr "Show all" 25 | 26 | #: read_only_admin/templates/admin/pagination.html:17 27 | msgid "Save" 28 | msgstr "Save" 29 | -------------------------------------------------------------------------------- /read_only_admin/locale/uk/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vint21h/django-read-only-admin/e3edebb2081a50b9da708e06d3cbd5fb953cf66c/read_only_admin/locale/uk/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /read_only_admin/locale/uk/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # django-read-only-admin 2 | # read_only_admin/locale/uk/LC_MESSAGES/django.po 3 | # 4 | #, fuzzy 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: django-read-only-admin 0.17.5\n" 8 | "Report-Msgid-Bugs-To: Alexei Andrushievich \n" 9 | "POT-Creation-Date: 2022-02-19 22:28+0200\n" 10 | "PO-Revision-Date: 2019-08-26 16:48+0300\n" 11 | "Last-Translator: Alexei Andrushievich \n" 12 | "Language-Team: Alexei Andrushievich \n" 13 | "Language: uk\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 18 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" 19 | 20 | #: read_only_admin/apps.py:20 21 | msgid "Django read only admin" 22 | msgstr "Django read only admin" 23 | 24 | #: read_only_admin/templates/admin/pagination.html:16 25 | msgid "Show all" 26 | msgstr "Показати всі" 27 | 28 | #: read_only_admin/templates/admin/pagination.html:17 29 | msgid "Save" 30 | msgstr "Зберегти" 31 | -------------------------------------------------------------------------------- /read_only_admin/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # django-read-only-admin 4 | # read_only_admin/models.py 5 | 6 | 7 | from typing import List 8 | 9 | from django.db.models import signals 10 | 11 | from read_only_admin.signals import add_readonly_permissions 12 | 13 | 14 | __all__: List[str] = [] 15 | 16 | 17 | signals.post_migrate.connect(add_readonly_permissions) 18 | -------------------------------------------------------------------------------- /read_only_admin/signals.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # django-read-only-admin 4 | # read_only_admin/signals.py 5 | 6 | 7 | from typing import List, Iterable, Optional 8 | 9 | from django.apps import AppConfig 10 | from django.db.utils import DEFAULT_DB_ALIAS 11 | from django.contrib.auth.models import Permission 12 | from django.contrib.contenttypes.models import ContentType 13 | 14 | from read_only_admin.utils import ( 15 | get_read_only_permission_name, 16 | get_read_only_permission_codename, 17 | ) 18 | 19 | 20 | __all__: List[str] = ["add_readonly_permissions"] 21 | 22 | 23 | def add_readonly_permissions( # noqa: CFQ002 24 | sender: AppConfig, 25 | app_config: AppConfig, 26 | verbosity: int = 1, 27 | interactive: bool = False, 28 | using: str = DEFAULT_DB_ALIAS, 29 | plan: Optional[Iterable[str]] = None, 30 | apps: Optional[Iterable[str]] = None, 31 | *args, 32 | **kwargs, 33 | ) -> None: 34 | """ 35 | This migrate hooks takes care of adding a read only permission to all of your content types. 36 | 37 | Get from: https://github.com/anupamshakya7/django-admin-hack/. 38 | 39 | :param sender: installed application config instance 40 | :type sender: AppConfig 41 | :param app_config: same as sender 42 | :type app_config: AppConfig 43 | :param verbosity: verbosity level 44 | :type verbosity: int 45 | :param interactive: prompt user to input things 46 | :type interactive: bool 47 | :param using: db name 48 | :type using: str 49 | :param plan: migration plan 50 | :type plan: Optional[Iterable[str]] 51 | :param apps: applications 52 | :type apps: Optional[Iterable[str]] 53 | :param args: additional arguments 54 | :type args: list 55 | :param kwargs: additional arguments 56 | :type kwargs: dict 57 | """ # noqa: E501 58 | for content_type in ContentType.objects.all(): 59 | Permission.objects.get_or_create( 60 | content_type=content_type, 61 | codename=get_read_only_permission_codename(model=content_type.model), 62 | name=get_read_only_permission_name(model=content_type.model), 63 | ) 64 | -------------------------------------------------------------------------------- /read_only_admin/templates/admin/pagination.html: -------------------------------------------------------------------------------- 1 | {# django-read-only-admin #} 2 | {# read_only_admin/templates/admin/pagination.html #} 3 | 4 | 5 | {% load admin_list %} 6 | {% load i18n %} 7 | 8 | 9 |

10 | {% if pagination_required %} 11 | {% for i in page_range %} 12 | {% paginator_number cl i %} 13 | {% endfor %} 14 | {% endif %} 15 | {{ cl.result_count }} {% if cl.result_count == 1 %}{{ cl.opts.verbose_name }}{% else %}{{ cl.opts.verbose_name_plural }}{% endif %} 16 | {% if show_all_url %}  {% trans "Show all" %}{% endif %} 17 | {% if cl.formset and cl.result_count and not cl.readonly %}{% endif %} 18 |

19 | -------------------------------------------------------------------------------- /read_only_admin/templates/read_only_admin/change_form.html: -------------------------------------------------------------------------------- 1 | {# django-read-only-admin #} 2 | {# read_only_admin/templates/read_only_admin/change_form.html #} 3 | 4 | 5 | {% extends "admin/change_form.html" %} 6 | 7 | 8 | {% load read_only_admin_tags %} 9 | 10 | 11 | {% block field_sets %} 12 | {% for fieldset in adminform %} 13 | {% include "read_only_admin/includes/fieldset.html" %} 14 | {% endfor %} 15 | {% endblock %} 16 | 17 | {% block submit_buttons_bottom %} 18 | {% readonly_submit_row %} 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /read_only_admin/templates/read_only_admin/includes/fieldset.html: -------------------------------------------------------------------------------- 1 | {# django-read-only-admin #} 2 | {# read_only_admin/templates/read_only_admin/includes/fieldset.html #} 3 | 4 | 5 | {% load read_only_admin_tags %} 6 | 7 | 8 |
9 | {% if fieldset.name %} 10 |

{{ fieldset.name }}

11 | {% endif %} 12 | {% if fieldset.description %} 13 |
{{ fieldset.description|safe }}
14 | {% endif %} 15 | {% for line in fieldset %} 16 |
17 | {% if line.fields|length_is:"1" %} 18 | {{ line.errors }} 19 | {% endif %} 20 | {% for field in line %} 21 | 22 | {% if not line.fields|length_is:"1" and not field.is_readonly %}{{ field.errors }}{% endif %} 23 | {% if field.is_checkbox %} 24 | {{ field.field }}{{ field.label_tag }} 25 | {% else %} 26 | {{ field.label_tag }} 27 | {% if field.is_readonly %} 28 | {% autoescape off %} 29 |
{{ field.contents|unescape|safe }}
30 | {% endautoescape %} 31 | {% else %} 32 | {{ field.field }} 33 | {% endif %} 34 | {% endif %} 35 | {% if field.field.help_text %} 36 |
{{ field.field.help_text|safe }}
37 | {% endif %} 38 |
39 | {% endfor %} 40 | 41 | {% endfor %} 42 |
43 | -------------------------------------------------------------------------------- /read_only_admin/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # django-read-only-admin 4 | # read_only_admin/templatetags/__init__.py 5 | 6 | 7 | from typing import List 8 | 9 | 10 | __all__: List[str] = [] 11 | -------------------------------------------------------------------------------- /read_only_admin/templatetags/read_only_admin_tags.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # django-read-only-admin 4 | # read_only_admin/templatetags/read_only_admin_tags.py 5 | 6 | 7 | from typing import Dict, List 8 | 9 | from django.template import Context, Library, RequestContext 10 | from django.contrib.admin.templatetags.admin_modify import submit_row 11 | 12 | from read_only_admin.utils import get_read_only_permission_codename 13 | 14 | 15 | __all__: List[str] = ["unescape", "readonly_submit_row"] 16 | 17 | 18 | register = Library() 19 | 20 | _HTML_UNESCAPES: Dict[str, str] = { 21 | "'": "'", 22 | """: '"', 23 | ">": ">", 24 | "<": "<", 25 | "&": "&", 26 | } 27 | 28 | 29 | @register.filter() 30 | def unescape(value: str) -> str: 31 | """ 32 | Returns the ASCII decoded version of the given HTML string. This does NOT remove normal HTML tags like

. 33 | 34 | Get from: https://stackoverflow.com/questions/275174/how-do-i-perform-html-decoding-encoding-using-python-django. 35 | 36 | :param value: string wanted to decoded 37 | :type value: str 38 | :return: decoded string 39 | :rtype: str 40 | """ # noqa: E501 41 | for code, char in _HTML_UNESCAPES.items(): 42 | value = value.replace(code, char) 43 | 44 | return value 45 | 46 | 47 | @register.inclusion_tag("admin/submit_line.html", takes_context=True) 48 | def readonly_submit_row(context: RequestContext) -> Context: 49 | """ 50 | Read only submit row templatetag. 51 | 52 | Get from: http://anupamshakya.blogspot.com/2013/07/create-readonly-permission-for-all.html. 53 | 54 | :param context: template context 55 | :type context: RequestContext 56 | :return: updated context 57 | :rtype: Context 58 | """ # noqa: E501 59 | ctx: Context = submit_row(context=context) 60 | app, separator, model = context["opts"].partition( # pylint: disable=W0612 61 | "." 62 | ) # type: str, str, str 63 | user = context["request"].user 64 | 65 | for permission in user.get_all_permissions(): 66 | head, sep, tail = permission.partition( # pylint: disable=W0612 67 | "." 68 | ) # type: str, str, str 69 | if get_read_only_permission_codename(model=model) == tail and ( 70 | user.has_perm(permission) and not user.is_superuser 71 | ): 72 | ctx.update( 73 | { 74 | "show_delete_link": False, 75 | "show_save_and_add_another": False, 76 | "show_save_and_continue": False, 77 | "show_save": False, 78 | } 79 | ) 80 | 81 | return ctx 82 | 83 | return ctx 84 | -------------------------------------------------------------------------------- /read_only_admin/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # django-read-only-admin 4 | # read_only_admin/utils.py 5 | 6 | 7 | from typing import List 8 | 9 | from read_only_admin.conf import settings 10 | 11 | 12 | __all__: List[str] = [ 13 | "get_read_only_permission_codename", 14 | "get_read_only_permission_name", 15 | ] 16 | 17 | 18 | def get_read_only_permission_codename(model: str) -> str: 19 | """ 20 | Create read only permission code name. 21 | 22 | :param model: model name 23 | :type model: str 24 | :return: read only permission code name 25 | :rtype: str 26 | """ 27 | return f"{settings.READ_ONLY_ADMIN_PERMISSION_PREFIX}_{model}" 28 | 29 | 30 | def get_read_only_permission_name(model: str) -> str: 31 | """ 32 | Create read only permission human readable name. 33 | 34 | :param model: model name 35 | :type model: str 36 | :return: read only permission human readable name 37 | :rtype: str 38 | """ 39 | return f"{settings.READ_ONLY_ADMIN_PERMISSION_NAME_PREFIX.capitalize()} {model}" 40 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # django-read-only-admin 2 | # setup.cfg 3 | 4 | 5 | [metadata] 6 | name = django-read-only-admin 7 | version = 0.17.5 8 | description = Really full Django read only admin implementation 9 | python_requires = >=3.7 10 | license_file = MIT-LICENSE 11 | author = Alexei Andrushievich 12 | author_email = vint21h@vint21h.pp.ua 13 | url = https://github.com/vint21h/django-read-only-admin/ 14 | classifiers = 15 | Development Status :: 5 - Production/Stable 16 | Environment :: Plugins 17 | Framework :: Django :: 2.2 18 | Framework :: Django :: 3.2 19 | Framework :: Django :: 4.0 20 | License :: OSI Approved :: MIT License 21 | Operating System :: Unix 22 | Programming Language :: Python :: 3.7 23 | Programming Language :: Python :: 3.8 24 | Programming Language :: Python :: 3.9 25 | Programming Language :: Python :: 3.10 26 | Topic :: Utilities 27 | license = MIT 28 | long_description = file:README.rst 29 | long_description_content_type = text/x-rst 30 | keywords = django,django-admin,admin,read-only 31 | 32 | 33 | [options] 34 | packages = find: 35 | zip_safe = False 36 | include_package_data = True 37 | test_suite = tests 38 | install_requires = 39 | django>=2.2,<4.1 40 | django-appconf>=1.0.3 41 | 42 | 43 | [options.extras_require] 44 | test = 45 | attrs==21.4.0 46 | bandit==1.7.2 47 | black==22.1.0 48 | bpython==0.22.1 49 | check-manifest==0.47 50 | check-wheel-contents==0.3.4 51 | coverage==6.3.1 52 | coveralls==3.3.1 53 | curlylint==0.13.0 54 | darglint==1.8.1 55 | dennis==0.9 56 | django-stubs==1.9.0 57 | dlint==0.12.0 58 | dodgy==0.2.1 59 | dotenv-linter==0.3.0 60 | flake8-annotations-complexity==0.0.6 61 | flake8-annotations-coverage==0.0.5 62 | flake8-broken-line==0.4.0 63 | flake8-bugbear==22.1.11 64 | flake8-builtins==1.5.3 65 | flake8-coding==1.3.2 66 | flake8-cognitive-complexity==0.1.0 67 | flake8-comprehensions==3.8.0 68 | flake8-debugger==4.0.0 69 | flake8-django==1.1.2 70 | flake8-docstrings==1.6.0 71 | flake8-eradicate==1.2.0 72 | flake8-executable==2.1.1 73 | flake8-expression-complexity==0.0.9 74 | flake8-fixme==1.1.1 75 | flake8-functions==0.0.6 76 | flake8-mutable==1.2.0 77 | flake8-no-implicit-concat==0.3.3 78 | flake8-print==4.0.0 79 | flake8-return==1.1.3 80 | flake8-simplify==0.17.1 81 | flake8-typing-imports==1.12.0 82 | flake8-use-fstring==1.3 83 | flake8-variables-names==0.0.4 84 | flake8==3.9.2 # do not update before ALL plugins start 4.x support 85 | importlib-metadata==4.11.1 86 | interrogate==1.5.0 87 | isort==5.10.1 88 | mypy==0.931 89 | pep8-naming==0.12.1 90 | pip-outdated==0.5.0 91 | polint==0.4 92 | pre-commit-hooks==4.1.0 93 | pre-commit==2.17.0 94 | pygments==2.11.2 95 | pylint-django==2.5.2 96 | pylint==2.12.2 97 | pyroma==3.2 98 | pytest-cov==3.0.0 99 | pytest-django==4.5.2 100 | pytest-extra-durations==0.1.3 101 | pytest-instafail==0.4.2 102 | pytest-sugar==0.9.4 103 | pytest-timer[termcolor]==0.0.11 104 | pytest==7.0.1 105 | readme_renderer==32.0 106 | removestar==1.3.1 107 | seed-isort-config==2.2.0 108 | tabulate==0.8.9 109 | tox-gh-actions==2.9.1 110 | tox-pyenv==1.1.0 111 | tox==3.24.5 112 | twine==3.8.0 113 | wheel==0.37.1 114 | yesqa==1.3.0 115 | 116 | [options.packages.find] 117 | exclude = 118 | tests.* 119 | tests 120 | 121 | [bdist_wheel] 122 | universal = False 123 | 124 | [sdist] 125 | formats = zip 126 | 127 | 128 | [mypy] 129 | files = read_only_admin,tests 130 | check_untyped_defs = True 131 | disallow_any_generics = True 132 | disallow_untyped_calls = True 133 | ignore_errors = False 134 | ignore_missing_imports = True 135 | implicit_reexport = True 136 | strict_optional = True 137 | strict_equality = True 138 | no_implicit_optional = True 139 | warn_unused_ignores = True 140 | warn_redundant_casts = True 141 | warn_unused_configs = True 142 | warn_unreachable = True 143 | warn_no_return = True 144 | plugins = mypy_django_plugin.main 145 | 146 | [mypy.plugins.django-stubs] 147 | django_settings_module = tests.settings 148 | 149 | 150 | [flake8] 151 | max-line-length = 88 152 | select = B,C,D,E,F,W,T4,C4,TAE,T,M511,T100,TYP,A,EXE,CFQ,SIM,DUO,N,DJ,ECE,FS,R,CCR,NIC 153 | doctests = True 154 | ignore = D100,D104,D401 155 | max-annotations-complexity = 5 156 | max-expression-complexity = 5 157 | max-cognitive-complexity = 5 158 | min-python-version = 3.7 159 | exclude = setup.py 160 | 161 | 162 | [check-manifest] 163 | ignore = 164 | .editorconfig 165 | .env 166 | .env.example 167 | .envrc 168 | .gitattributes 169 | .github 170 | .gitignore 171 | .pre-commit-config.yaml 172 | .python-version 173 | AUTHORS 174 | Makefile 175 | MIT-LICENSE 176 | README.rst 177 | tests 178 | tests.* 179 | TODO 180 | ignore-bad-ideas = 181 | *.mo 182 | 183 | 184 | [coverage:run] 185 | branch = True 186 | source = read_only_admin 187 | omit = 188 | */__init__.py 189 | read_only_admin/apps.py 190 | 191 | [coverage:report] 192 | omit = 193 | */__init__.py 194 | read_only_admin/apps.py 195 | exclude_lines = 196 | pragma: no cover 197 | 198 | 199 | [isort] 200 | balanced_wrapping = True 201 | combine_as_imports = True 202 | default_section = FIRSTPARTY 203 | force_grid_wrap = 0 204 | force_sort_within_sections = True 205 | force_to_top = True 206 | include_trailing_comma = True 207 | known_first_party = read_only_admin 208 | line_length = 88 209 | lines_after_imports = 2 210 | length_sort = True 211 | multi_line_output = 3 212 | use_parentheses = True 213 | virtual_env = $VIRTUAL_ENV 214 | known_third_party = appconf,django,setuptools 215 | 216 | 217 | [darglint] 218 | docstring_style = sphinx 219 | 220 | 221 | [bandit] 222 | skips = B507 223 | tests = B101,B102,B103,B104,B105,B106,B107,B108,B110,B112,B201,B301,B302,B303,B304,B305,B306,B307,B308,B309,B310,B311,B312,B313,B314,B315,B316,B317,B318,B319,B320,B321,B323,B324,B325,B401,B402,B403,B404,B405,B406,B407,B408,B409,B410,B411,B412,B413,B501,B502,B503,B504,B505,B506,B601,B602,B603,B604,B605,B606,B607,B608,B609,B610,B611,B701,B702,B703 224 | 225 | 226 | [tool:check-wheel-contents] 227 | ignore = W004 228 | 229 | 230 | [tox:tox] 231 | skip_missing_interpreters = True 232 | tox_pyenv_fallback = False 233 | skipsdist = False 234 | envlist = 235 | {py37,py38,py39,py310}-2.2.x 236 | {py37,py38,py39,py310}-3.2.x 237 | {py38,py39,py310}-4.0.x 238 | check 239 | check-build 240 | 241 | [testenv] 242 | usedevelop = True 243 | setenv = 244 | PYTHONDONTWRITEBYTECODE=1 245 | deps = 246 | 2.2.x: Django>=2.2,<3.0 247 | 3.2.x: Django>=3.2,<4.0 248 | 4.0.x: Django>=4.0,<4.1 249 | commands= 250 | pip install .[test] 251 | make test 252 | whitelist_externals = 253 | make 254 | 255 | [testenv:check] 256 | skip_install = True 257 | commands = 258 | make check 259 | whitelist_externals = 260 | make 261 | 262 | [testenv:check-build] 263 | skip_install = True 264 | commands = 265 | make build -B 266 | make check-build -B 267 | whitelist_externals = 268 | make 269 | 270 | 271 | [gh-actions] 272 | python = 273 | 3.7: py37 274 | 3.8: py38 275 | 3.9: py39 276 | 3.10: py310 277 | 278 | 279 | [pylint.MASTER] 280 | ignore = CVS 281 | ignore-patterns = 282 | persistent = yes 283 | load-plugins = pylint_django 284 | jobs = 1 285 | unsafe-load-any-extension = no 286 | extension-pkg-whitelist = 287 | optimize-ast = no 288 | 289 | [pylint.'MESSAGES CONTROL'] 290 | confidence = 291 | disable = all 292 | enable = E0001,E0100,E0101,E0102,E0103,E0104,E0105,E0106,E0107,E0110, 293 | E0113,E0114,E0115,E0116,E0117,E0108,E0202,E0203,E0211,E0236, 294 | E0238,E0239,E0240,E0241,E0301,E0302,E0601,E0603,E0604,E0701, 295 | E0702,E0703,E0704,E0710,E0711,E0712,E1003,E1102,E1111,E0112, 296 | E1120,E1121,E1123,E1124,E1125,E1126,E1127,E1132,E1200,E1201, 297 | E1205,E1206,E1300,E1301,E1302,E1303,E1304,E1305,E1306, 298 | C0123,C0200,C0303,C1001, 299 | W0101,W0102,W0104,W0105,W0106,W0107,W0108,W0109,W0110,W0120, 300 | W0122,W0124,W0150,W0199,W0221,W0222,W0233,W0404,W0410,W0601, 301 | W0602,W0604,W0611,W0612,W0622,W0623,W0702,W0705,W0711,W1300, 302 | W1301,W1302,W1303,,W1305,W1306,W1307 303 | R0102,R0201,R0202,R0203 304 | 305 | [pylint.REPORTS] 306 | output-format = text 307 | files-output = no 308 | reports = yes 309 | evaluation = 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 310 | 311 | [pylint.BASIC] 312 | good-names = i,j,k,ex,Run,_ 313 | bad-names = foo,bar,baz,toto,tutu,tata 314 | name-group = 315 | include-naming-hint = no 316 | property-classes = abc.abstractproperty 317 | const-rgx = (([A-Z_][A-Z0-9_]*)|(__.*__))$ 318 | const-name-hint = (([A-Z_][A-Z0-9_]*)|(__.*__))$ 319 | class-rgx = [A-Z_][a-zA-Z0-9]+$ 320 | class-name-hint = [A-Z_][a-zA-Z0-9]+$ 321 | argument-rgx = [a-z_][a-z0-9_]{2,30}$ 322 | argument-name-hint = [a-z_][a-z0-9_]{2,30}$ 323 | variable-rgx = [a-z_][a-z0-9_]{2,30}$ 324 | variable-name-hint = [a-z_][a-z0-9_]{2,30}$ 325 | class-attribute-rgx = ([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 326 | class-attribute-name-hint = ([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 327 | method-rgx = [a-z_][a-z0-9_]{2,30}$ 328 | method-name-hint = [a-z_][a-z0-9_]{2,30}$ 329 | module-rgx = (([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 330 | module-name-hint = (([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 331 | function-rgx = [a-z_][a-z0-9_]{2,30}$ 332 | function-name-hint = [a-z_][a-z0-9_]{2,30}$ 333 | attr-rgx = [a-z_][a-z0-9_]{2,30}$ 334 | attr-name-hint = [a-z_][a-z0-9_]{2,30}$ 335 | inlinevar-rgx = [A-Za-z_][A-Za-z0-9_]*$ 336 | inlinevar-name-hint = [A-Za-z_][A-Za-z0-9_]*$ 337 | no-docstring-rgx = ^_ 338 | docstring-min-length = -1 339 | 340 | [pylint.ELIF] 341 | max-nested-blocks = 5 342 | 343 | [pylint.FORMAT] 344 | max-line-length = 88 345 | ignore-long-lines = ^\s*(# )??$ 346 | single-line-if-stmt = no 347 | no-space-check = trailing-comma,dict-separator 348 | max-module-lines = 1000 349 | indent-string=' ' 350 | indent-after-paren = 4 351 | expected-line-ending-format = LF 352 | 353 | [pylint.LOGGING] 354 | logging-modules = logging 355 | 356 | [pylint.MISCELLANEOUS] 357 | notes = FIXME,XXX,TODO 358 | 359 | [pylint.SIMILARITIES] 360 | min-similarity-lines = 4 361 | ignore-comments = yes 362 | ignore-docstrings = yes 363 | ignore-imports = no 364 | 365 | [pylint.SPELLING] 366 | spelling-dict = 367 | spelling-ignore-words = 368 | spelling-private-dict-file = 369 | spelling-store-unknown-words = no 370 | 371 | [pylint.TYPECHECK] 372 | ignore-mixin-members = yes 373 | ignored-modules = 374 | ignored-classes = optparse.Values,thread._local,_thread._local 375 | generated-members = 376 | contextmanager-decorators = contextlib.contextmanager 377 | 378 | [pylint.VARIABLES] 379 | init-import = no 380 | dummy-variables-rgx = (_+[a-zA-Z0-9]*?$)|dummy 381 | additional-builtins = 382 | callbacks = cb_,_cb 383 | redefining-builtins-modules = six.moves,future.builtins 384 | 385 | [pylint.CLASSES] 386 | defining-attr-methods = __init__,__new__,setUp 387 | valid-classmethod-first-arg = cls 388 | valid-metaclass-classmethod-first-arg = mcs 389 | exclude-protected = _asdict,_fields,_replace,_source,_make 390 | 391 | [pylint.DESIGN] 392 | max-args = 5 393 | ignored-argument-names = _.* 394 | max-locals = 15 395 | max-returns = 6 396 | max-branches = 12 397 | max-statements = 50 398 | max-parents = 7 399 | max-attributes = 7 400 | min-public-methods = 2 401 | max-public-methods = 20 402 | max-bool-expr = 5 403 | 404 | [pylint.IMPORTS] 405 | deprecated-modules = optparse 406 | import-graph = 407 | ext-import-graph = 408 | int-import-graph = 409 | known-standard-library = 410 | known-third-party = enchant 411 | analyse-fallback-blocks = no 412 | 413 | [pylint.EXCEPTIONS] 414 | overgeneral-exceptions = Exception 415 | 416 | 417 | [tool:pytest] 418 | DJANGO_SETTINGS_MODULE = tests.settings 419 | python_files = tests.py test_*.py *_tests.py 420 | python_classes = 421 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # -*- coding: utf-8 -*- 4 | 5 | # django-read-only-admin 6 | # setup.py 7 | 8 | 9 | import setuptools 10 | 11 | 12 | if __name__ == "__main__": 13 | setuptools.setup() 14 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # django-read-only-admin 4 | # tests/__init__.py 5 | 6 | 7 | from typing import List 8 | 9 | 10 | __all__: List[str] = [] 11 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # django-read-only-admin 4 | # tests/settings.py 5 | 6 | 7 | import sys 8 | import pathlib 9 | from random import SystemRandom 10 | from typing import Dict, List, Union 11 | 12 | 13 | # black magic to use imports from library code 14 | path = pathlib.Path(__file__).absolute() 15 | project = path.parent.parent.parent 16 | sys.path.insert(0, str(project)) 17 | 18 | # secret key 19 | SECRET_KEY: str = "".join( 20 | [ 21 | SystemRandom().choice("abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)") 22 | for i in range(50) 23 | ] 24 | ) 25 | 26 | # configure databases 27 | DATABASES: Dict[str, Dict[str, str]] = { 28 | "default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"} 29 | } 30 | 31 | # configure templates 32 | TEMPLATES: List[Dict[str, Union[str, List[str], bool, Dict[str, str]]]] = [ 33 | { 34 | "BACKEND": "django.template.backends.django.DjangoTemplates", 35 | "DIRS": [], 36 | "APP_DIRS": True, 37 | "OPTIONS": {}, 38 | } 39 | ] 40 | 41 | 42 | # add testing related apps 43 | INSTALLED_APPS: List[str] = [ 44 | "django.contrib.admin", 45 | "django.contrib.auth", 46 | "django.contrib.contenttypes", 47 | "read_only_admin", 48 | ] 49 | 50 | # configure urls 51 | ROOT_URLCONF: str = "read_only_admin.urls" 52 | 53 | # read only admin settings 54 | READ_ONLY_ADMIN_PERMISSION_PREFIX: str = "readonly" 55 | READ_ONLY_ADMIN_PERMISSION_NAME_PREFIX: str = "Read only" 56 | READ_ONLY_ADMIN_EMPTY_ACTIONS: bool = True 57 | -------------------------------------------------------------------------------- /tests/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # django-read-only-admin 4 | # tests/templatetags/__init__.py 5 | 6 | 7 | from typing import List 8 | 9 | 10 | __all__: List[str] = [] 11 | -------------------------------------------------------------------------------- /tests/templatetags/test_read_only_admin_tags.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # django-read-only-admin 4 | # tests/templatetags/test_read_only_admin_tags.py 5 | 6 | 7 | from typing import List 8 | 9 | from django.test import TestCase 10 | from django.http import HttpRequest 11 | from django.contrib.auth import get_user_model 12 | from django.contrib.auth.models import Permission 13 | from django.template import Context, RequestContext 14 | 15 | from read_only_admin.conf import settings 16 | from read_only_admin.templatetags.read_only_admin_tags import ( 17 | unescape, 18 | readonly_submit_row, 19 | ) 20 | 21 | 22 | __all__: List[str] = [ 23 | "UnescapeTemplatetagTest", 24 | "ReadonlySubmitRowTemplatetagTest", 25 | ] 26 | 27 | 28 | User = get_user_model() 29 | 30 | 31 | class UnescapeTemplatetagTest(TestCase): 32 | """Unescape templatetag tests.""" 33 | 34 | def test_unescape(self) -> None: 35 | """Test templatetag.""" 36 | escaped: str = """<script type="text/javascript">alert('PWND & HACKD!!1')</script>""" # noqa: E501 37 | unescaped: str = """""" # noqa: E501 38 | 39 | self.assertEqual(first=unescape(value=escaped), second=unescaped) 40 | 41 | def test_unescape__single_quote(self) -> None: 42 | """Test templatetag for single quote char.""" 43 | escaped: str = "'" 44 | unescaped: str = "'" 45 | 46 | self.assertEqual(first=unescape(value=escaped), second=unescaped) 47 | 48 | def test_unescape__double_quote(self) -> None: 49 | """Test templatetag for double quote char.""" 50 | escaped: str = """ 51 | unescaped: str = '"' 52 | 53 | self.assertEqual(first=unescape(value=escaped), second=unescaped) 54 | 55 | def test_unescape__less_than(self) -> None: 56 | """Test templatetag for less than char.""" 57 | escaped: str = "<" 58 | unescaped: str = "<" 59 | 60 | self.assertEqual(first=unescape(value=escaped), second=unescaped) 61 | 62 | def test_unescape__great_than(self) -> None: 63 | """Test templatetag for great than char.""" 64 | escaped: str = ">" 65 | unescaped: str = ">" 66 | 67 | self.assertEqual(first=unescape(value=escaped), second=unescaped) 68 | 69 | def test_unescape__ampersand(self) -> None: 70 | """Test templatetag for ampersand char.""" 71 | escaped: str = "&" 72 | unescaped: str = "&" 73 | 74 | self.assertEqual(first=unescape(value=escaped), second=unescaped) 75 | 76 | 77 | class ReadonlySubmitRowTemplatetagTest(TestCase): 78 | """Read only submit row templatetag tests.""" 79 | 80 | @classmethod 81 | def setUpTestData(cls) -> None: 82 | """Set up non-modified objects used by all test methods.""" 83 | user = User.objects.create( 84 | username="test", 85 | email="test@example.com", 86 | password=User.objects.make_random_password(), 87 | is_staff=True, 88 | ) 89 | user.user_permissions.add(*list(Permission.objects.all())) 90 | user.save() 91 | 92 | def test_readonly_submit_row__return_context(self) -> None: 93 | """Test templatetag return context.""" 94 | user = User.objects.first() 95 | request: HttpRequest = HttpRequest() 96 | request.user = user # type: ignore 97 | context: RequestContext = RequestContext( 98 | request=request, 99 | dict_={ 100 | "user": user, 101 | "add": True, 102 | "change": True, 103 | "is_popup": False, 104 | "save_as": True, 105 | "has_add_permission": True, 106 | "has_change_permission": True, 107 | "has_view_permission": True, 108 | "has_editable_inline_admin_formsets": False, 109 | "has_delete_permission": True, 110 | "opts": "auth.user", 111 | "request": request, 112 | }, 113 | ) 114 | result: Context = readonly_submit_row(context=context) 115 | 116 | self.assertIsInstance(obj=result, cls=Context) 117 | 118 | def test_readonly_submit_row(self) -> None: 119 | """Test templatetag.""" 120 | user = User.objects.first() 121 | request: HttpRequest = HttpRequest() 122 | request.user = user # type: ignore 123 | context: RequestContext = RequestContext( 124 | request=request, 125 | dict_={ 126 | "user": user, 127 | "add": True, 128 | "change": True, 129 | "is_popup": False, 130 | "save_as": True, 131 | "has_add_permission": True, 132 | "has_change_permission": True, 133 | "has_view_permission": True, 134 | "has_editable_inline_admin_formsets": False, 135 | "has_delete_permission": True, 136 | "opts": "auth.user", 137 | "request": request, 138 | }, 139 | ) 140 | result: Context = readonly_submit_row(context=context) 141 | 142 | self.assertFalse(expr=result["show_delete_link"]) 143 | self.assertFalse(expr=result["show_save_and_add_another"]) 144 | self.assertFalse(expr=result["show_save_and_continue"]) 145 | self.assertFalse(expr=result["show_save"]) 146 | 147 | def test_readonly_submit_row__for_superuser(self) -> None: 148 | """Test templatetag for superuser.""" 149 | user = User.objects.first() 150 | user.is_superuser = True # type: ignore 151 | user.save(update_fields=["is_superuser"]) # type: ignore 152 | request: HttpRequest = HttpRequest() 153 | request.user = user # type: ignore 154 | context: RequestContext = RequestContext( 155 | request=request, 156 | dict_={ 157 | "user": user, 158 | "add": True, 159 | "change": True, 160 | "is_popup": False, 161 | "save_as": True, 162 | "has_add_permission": True, 163 | "has_change_permission": True, 164 | "has_view_permission": True, 165 | "has_editable_inline_admin_formsets": False, 166 | "has_delete_permission": True, 167 | "opts": "auth.user", 168 | "request": request, 169 | }, 170 | ) 171 | result: Context = readonly_submit_row(context=context) 172 | 173 | self.assertTrue(expr=result["show_delete_link"]) 174 | self.assertTrue(expr=result["show_save_and_add_another"]) 175 | self.assertTrue(expr=result["show_save_and_continue"]) 176 | self.assertTrue(expr=result["show_save"]) 177 | 178 | def test_readonly_submit_row__without__read_only_permissions(self) -> None: 179 | """Test templatetag without read only permissions.""" 180 | Permission.objects.filter( 181 | codename__startswith=settings.READ_ONLY_ADMIN_PERMISSION_PREFIX 182 | ).delete() 183 | 184 | user = User.objects.first() 185 | request: HttpRequest = HttpRequest() 186 | request.user = user # type: ignore 187 | context: RequestContext = RequestContext( 188 | request=request, 189 | dict_={ 190 | "user": user, 191 | "add": True, 192 | "change": True, 193 | "is_popup": False, 194 | "save_as": True, 195 | "has_add_permission": True, 196 | "has_change_permission": True, 197 | "has_view_permission": True, 198 | "has_editable_inline_admin_formsets": False, 199 | "has_delete_permission": True, 200 | "opts": "auth.user", 201 | "request": request, 202 | }, 203 | ) 204 | result: Context = readonly_submit_row(context=context) 205 | 206 | self.assertTrue(expr=result["show_delete_link"]) 207 | self.assertTrue(expr=result["show_save_and_add_another"]) 208 | self.assertTrue(expr=result["show_save_and_continue"]) 209 | self.assertTrue(expr=result["show_save"]) 210 | 211 | def test_readonly_submit_row__without__read_only_permissions__for_superuser( 212 | self, 213 | ) -> None: 214 | """Test templatetag without read only permissions for superuser.""" 215 | user = User.objects.first() 216 | user.is_superuser = True # type: ignore 217 | user.save(update_fields=["is_superuser"]) # type: ignore 218 | request: HttpRequest = HttpRequest() 219 | request.user = user # type: ignore 220 | context: RequestContext = RequestContext( 221 | request=request, 222 | dict_={ 223 | "user": user, 224 | "add": True, 225 | "change": True, 226 | "is_popup": False, 227 | "save_as": True, 228 | "has_add_permission": True, 229 | "has_change_permission": True, 230 | "has_view_permission": True, 231 | "has_editable_inline_admin_formsets": False, 232 | "has_delete_permission": True, 233 | "opts": "auth.user", 234 | "request": request, 235 | }, 236 | ) 237 | result: Context = readonly_submit_row(context=context) 238 | 239 | self.assertTrue(expr=result["show_delete_link"]) 240 | self.assertTrue(expr=result["show_save_and_add_another"]) 241 | self.assertTrue(expr=result["show_save_and_continue"]) 242 | self.assertTrue(expr=result["show_save"]) 243 | -------------------------------------------------------------------------------- /tests/test_admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # django-read-only-admin 4 | # tests/test_admin.py 5 | 6 | 7 | from io import StringIO 8 | from collections import OrderedDict 9 | from typing import Any, List, Type, Iterable 10 | 11 | from django.test import TestCase 12 | from django.http import HttpRequest 13 | from django.forms.formsets import BaseFormSet 14 | from django.contrib.auth import get_user_model 15 | from django.contrib.auth.admin import UserAdmin 16 | from django.test.utils import override_settings 17 | from django.contrib.admin.sites import AdminSite 18 | from django.contrib.auth.models import Permission 19 | from django.core.handlers.wsgi import WSGIRequest 20 | from django.contrib.admin.actions import delete_selected 21 | 22 | from read_only_admin.admin import ReadonlyAdmin, ReadonlyChangeList 23 | 24 | 25 | __all__: List[str] = ["ReadonlyAdminTest", "ReadonlyChangeListTest"] 26 | 27 | 28 | User = get_user_model() 29 | 30 | 31 | class ReadOnlyUserAdmin(UserAdmin, ReadonlyAdmin): 32 | """Read only admin class.""" 33 | 34 | ... 35 | 36 | 37 | class ReadonlyChangeListTest(TestCase): 38 | """Read only change list tests.""" 39 | 40 | @classmethod 41 | def setUpTestData(cls) -> None: 42 | """Set up non-modified objects used by all test methods.""" 43 | user = User.objects.create( 44 | username="test", 45 | email="test@example.com", 46 | password=User.objects.make_random_password(), 47 | is_staff=True, 48 | ) 49 | user.user_permissions.add(*list(Permission.objects.all())) 50 | user.save() 51 | 52 | def test__init__(self) -> None: 53 | """Init method must set readonly property to True.""" 54 | request: WSGIRequest = WSGIRequest( 55 | {"REQUEST_METHOD": "GET", "PATH_INFO": "/", "wsgi.input": StringIO()} 56 | ) 57 | request.user = User.objects.first() # type: ignore 58 | result: ReadonlyChangeList = ReadonlyChangeList( 59 | request=request, 60 | model=User, 61 | list_display=[], 62 | list_display_links=None, 63 | list_filter=["is_active"], 64 | date_hierarchy=UserAdmin.date_hierarchy, 65 | search_fields=[], 66 | list_select_related=False, 67 | list_per_page=UserAdmin.list_per_page, 68 | list_max_show_all=UserAdmin.list_max_show_all, 69 | list_editable=[], 70 | model_admin=ReadOnlyUserAdmin( 71 | model=get_user_model(), admin_site=AdminSite() 72 | ), 73 | sortable_by=UserAdmin.sortable_by, # type: ignore 74 | ) 75 | 76 | self.assertTrue(expr=result.readonly) 77 | 78 | 79 | class ReadonlyAdminTest(TestCase): 80 | """Read only admin tests.""" 81 | 82 | @classmethod 83 | def setUpTestData(cls) -> None: 84 | """Set up non-modified objects used by all test methods.""" 85 | user = User.objects.create( 86 | username="test", 87 | email="test@example.com", 88 | password=User.objects.make_random_password(), 89 | is_staff=True, 90 | ) 91 | user.user_permissions.add(*list(Permission.objects.all())) 92 | user.save() 93 | 94 | def test_get_changelist(self) -> None: 95 | """Method must return read only change list class.""" 96 | request: HttpRequest = HttpRequest() 97 | request.user = User.objects.first() # type: ignore 98 | result: Type[ReadonlyChangeList] = ReadOnlyUserAdmin( 99 | model=get_user_model(), admin_site=AdminSite() 100 | ).get_changelist( 101 | request=request 102 | ) 103 | 104 | self.assertEqual(first=result, second=ReadonlyChangeList) 105 | 106 | def test_get_changelist_formset(self) -> None: 107 | """Method must return change list form set.""" 108 | request: HttpRequest = HttpRequest() 109 | request.user = User.objects.first() # type: ignore 110 | result: Type[BaseFormSet] = ReadOnlyUserAdmin( 111 | model=get_user_model(), admin_site=AdminSite() 112 | ).get_changelist_formset( 113 | request=request 114 | ) 115 | 116 | self.assertEqual(first=result.__name__, second="UserFormFormSet") 117 | 118 | def test_get_readonly_fields(self) -> None: 119 | """Method must return all form fields as read only.""" 120 | user = User.objects.first() 121 | request: HttpRequest = HttpRequest() 122 | request.user = user # type: ignore 123 | result = ReadOnlyUserAdmin( 124 | model=get_user_model(), admin_site=AdminSite() 125 | ).get_readonly_fields(request=request, obj=user) 126 | expected: List[str] = [ 127 | "username", 128 | "password", 129 | "first_name", 130 | "last_name", 131 | "email", 132 | "is_active", 133 | "is_staff", 134 | "is_superuser", 135 | "groups", 136 | "user_permissions", 137 | "last_login", 138 | "date_joined", 139 | ] 140 | 141 | self.assertListEqual(list1=result, list2=expected) # type: ignore 142 | 143 | def test_get_actions(self) -> None: 144 | """Method must return empty actions list.""" 145 | user = User.objects.first() 146 | request: HttpRequest = HttpRequest() 147 | request.user = user # type: ignore 148 | result: OrderedDict[str, Any] = ReadOnlyUserAdmin( 149 | model=get_user_model(), admin_site=AdminSite() 150 | ).get_actions( 151 | request=request 152 | ) 153 | 154 | self.assertDictEqual(d1=result, d2=OrderedDict()) 155 | 156 | @override_settings(READ_ONLY_ADMIN_EMPTY_ACTIONS=False) 157 | def test_get_actions__without_empty_actions(self) -> None: 158 | """Method must return actions list resolved by available permissions.""" 159 | user = User.objects.first() 160 | request: HttpRequest = HttpRequest() 161 | request.user = user # type: ignore 162 | result: OrderedDict[str, Any] = ReadOnlyUserAdmin( 163 | model=get_user_model(), admin_site=AdminSite() 164 | ).get_actions( 165 | request=request 166 | ) 167 | 168 | self.assertDictEqual(d1=result, d2=OrderedDict()) 169 | 170 | def test_get_readonly_fields__for_superuser(self) -> None: 171 | """Method must return empty read only fields for super user.""" 172 | user = User.objects.first() 173 | user.is_superuser = True # type: ignore 174 | user.save(update_fields=["is_superuser"]) # type: ignore 175 | request: HttpRequest = HttpRequest() 176 | request.user = user # type: ignore 177 | result: Iterable[str] = ReadOnlyUserAdmin( 178 | model=get_user_model(), admin_site=AdminSite() 179 | ).get_readonly_fields( 180 | request=request, obj=user 181 | ) 182 | 183 | self.assertEqual(first=result, second=()) 184 | 185 | def test_get_actions__for_superuser(self) -> None: 186 | """Method must return actions list resolved by available permissions.""" 187 | user = User.objects.first() 188 | user.is_superuser = True # type: ignore 189 | user.save(update_fields=["is_superuser"]) # type: ignore 190 | request: HttpRequest = HttpRequest() 191 | request.user = user # type: ignore 192 | result: OrderedDict[str, Any] = ReadOnlyUserAdmin( 193 | model=get_user_model(), admin_site=AdminSite() 194 | ).get_actions( 195 | request=request 196 | ) 197 | expected: OrderedDict[str, Any] = OrderedDict( 198 | [ 199 | ( 200 | "delete_selected", 201 | ( 202 | delete_selected, 203 | "delete_selected", 204 | "Delete selected %(verbose_name_plural)s", 205 | ), 206 | ) 207 | ] 208 | ) 209 | 210 | self.assertDictEqual(d1=result, d2=expected) 211 | 212 | @override_settings(READ_ONLY_ADMIN_EMPTY_ACTIONS=False) 213 | def test_get_actions__without_empty_actions__for_superuser(self) -> None: 214 | """Method must return actions list resolved by available permissions.""" 215 | user = User.objects.first() 216 | user.is_superuser = True # type: ignore 217 | user.save(update_fields=["is_superuser"]) # type: ignore 218 | request: HttpRequest = HttpRequest() 219 | request.user = user # type: ignore 220 | result: OrderedDict[str, Any] = ReadOnlyUserAdmin( 221 | model=get_user_model(), admin_site=AdminSite() 222 | ).get_actions( 223 | request=request 224 | ) 225 | expected: OrderedDict[str, Any] = OrderedDict( 226 | [ 227 | ( 228 | "delete_selected", 229 | ( 230 | delete_selected, 231 | "delete_selected", 232 | "Delete selected %(verbose_name_plural)s", 233 | ), 234 | ) 235 | ] 236 | ) 237 | 238 | self.assertDictEqual(d1=result, d2=expected) 239 | -------------------------------------------------------------------------------- /tests/test_signals.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # django-read-only-admin 4 | # tests/test_signals.py 5 | 6 | 7 | from typing import List 8 | 9 | from django.conf import settings 10 | from django.test import TestCase 11 | from django.contrib.auth.models import Permission 12 | 13 | 14 | __all__: List[str] = ["AddReadOnlyPermissionsSignalTest"] 15 | 16 | 17 | class AddReadOnlyPermissionsSignalTest(TestCase): 18 | """Add read only permissions signal tests.""" 19 | 20 | def test_add_readonly_permissions(self) -> None: 21 | """Test signal.""" 22 | self.assertListEqual( 23 | list1=list( 24 | Permission.objects.filter( 25 | codename__startswith=settings.READ_ONLY_ADMIN_PERMISSION_PREFIX 26 | ).values_list("codename", flat=True) 27 | ), 28 | list2=[ 29 | "readonly_logentry", 30 | "readonly_group", 31 | "readonly_permission", 32 | "readonly_user", 33 | "readonly_contenttype", 34 | ], 35 | ) 36 | 37 | def test_add_readonly_permissions__count(self) -> None: 38 | """Test signal create new permissions number.""" 39 | self.assertEqual( 40 | first=Permission.objects.filter( 41 | codename__startswith=settings.READ_ONLY_ADMIN_PERMISSION_PREFIX 42 | ).count(), 43 | second=5, 44 | ) 45 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # django-read-only-admin 4 | # tests/test_utils.py 5 | 6 | 7 | from typing import List 8 | 9 | from django.test import TestCase 10 | from django.test.utils import override_settings 11 | 12 | from read_only_admin.utils import ( 13 | get_read_only_permission_name, 14 | get_read_only_permission_codename, 15 | ) 16 | 17 | 18 | __all__: List[str] = [ 19 | "GetReadOnlyPermissionCodenameUtilTest", 20 | "GetReadOnlyPermissionNameUtilTest", 21 | ] 22 | 23 | 24 | class GetReadOnlyPermissionCodenameUtilTest(TestCase): 25 | """get_read_only_permission_codename util tests.""" 26 | 27 | def test_get_read_only_permission_codename(self) -> None: 28 | """Util must return model read only permission codename based on read only prefix setting.""" # noqa: E501 29 | self.assertEqual( 30 | get_read_only_permission_codename(model="user"), "readonly_user" 31 | ) 32 | 33 | @override_settings(READ_ONLY_ADMIN_PERMISSION_PREFIX="") 34 | def test_get_read_only_permission_codename__without_prefix(self) -> None: 35 | """Util must return model read only permission codename based on read only prefix setting with broken prefix settings.""" # noqa: E501 36 | self.assertEqual(get_read_only_permission_codename(model="user"), "_user") 37 | 38 | 39 | class GetReadOnlyPermissionNameUtilTest(TestCase): 40 | """get_read_only_permission_name util tests.""" 41 | 42 | def test_get_read_only_permission_name(self) -> None: 43 | """Util must return model read only permission name based on read only name prefix setting.""" # noqa: E501 44 | self.assertEqual(get_read_only_permission_name(model="user"), "Read only user") 45 | 46 | @override_settings(READ_ONLY_ADMIN_PERMISSION_NAME_PREFIX="") 47 | def test_get_read_only_permission_name__without_prefix(self) -> None: 48 | """Util must return model read only permission name based on read only name prefix setting with broken name prefix settings.""" # noqa: E501 49 | self.assertEqual(get_read_only_permission_name(model="user"), " user") 50 | --------------------------------------------------------------------------------