├── .coveragerc ├── .github ├── dependabot.yml └── workflows │ ├── add-depr-ticket-to-depr-board.yml │ ├── add-remove-label-on-comment.yml │ ├── ci.yml │ ├── commitlint.yml │ ├── publish_pypi.yml │ ├── self-assign-issue.yml │ └── upgrade-python-requirements.yml ├── .gitignore ├── .readthedocs.yaml ├── AUTHORS ├── CHANGELOG.rst ├── LICENSE.txt ├── MANIFEST.in ├── Makefile ├── README.rst ├── catalog-info.yaml ├── config_models ├── __init__.py ├── admin.py ├── apps.py ├── conf │ └── locale │ │ └── config.yaml ├── decorators.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── populate_model.py ├── models.py ├── templates │ └── config_models │ │ └── base.html ├── templatetags.py ├── urls.py ├── utils.py └── views.py ├── db_keyword_overrides.yml ├── docs ├── Makefile ├── _static │ └── theme_overrides.css ├── changelog.rst ├── conf.py ├── getting_started.rst ├── index.rst ├── internationalization.rst ├── make.bat ├── readme.rst └── testing.rst ├── manage.py ├── mock_apps └── example │ ├── __init__.py │ ├── apps.py │ ├── migrations │ ├── 0001_initial.py │ ├── 0002_exampledecoratorconfig_exampledeserializeconfig.py │ └── __init__.py │ └── models.py ├── openedx.yaml ├── pylintrc ├── pylintrc_tweaks ├── requirements ├── base.in ├── base.txt ├── ci.in ├── ci.txt ├── constraints.txt ├── dev.in ├── dev.txt ├── doc.in ├── doc.txt ├── pip-tools.in ├── pip-tools.txt ├── pip.in ├── pip.txt ├── private.readme ├── quality.in ├── quality.txt ├── test.in └── test.txt ├── setup.cfg ├── setup.py ├── test_settings.py ├── tests ├── __init__.py ├── conftest.py ├── data │ └── data.json ├── test_admin.py ├── test_config_models.py ├── test_decorators.py ├── test_model_deserialization.py └── utils.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | data_file = .coverage 4 | source=config_models 5 | omit = 6 | test_settings 7 | *migrations* 8 | *admin.py 9 | *static* 10 | *templates* 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Adding new check for github-actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | reviewers: 9 | - "openedx/arbi-bom" 10 | -------------------------------------------------------------------------------- /.github/workflows/add-depr-ticket-to-depr-board.yml: -------------------------------------------------------------------------------- 1 | # Run the workflow that adds new tickets that are either: 2 | # - labelled "DEPR" 3 | # - title starts with "[DEPR]" 4 | # - body starts with "Proposal Date" (this is the first template field) 5 | # to the org-wide DEPR project board 6 | 7 | name: Add newly created DEPR issues to the DEPR project board 8 | 9 | on: 10 | issues: 11 | types: [opened] 12 | 13 | jobs: 14 | routeissue: 15 | uses: openedx/.github/.github/workflows/add-depr-ticket-to-depr-board.yml@master 16 | secrets: 17 | GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }} 18 | GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }} 19 | SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }} 20 | -------------------------------------------------------------------------------- /.github/workflows/add-remove-label-on-comment.yml: -------------------------------------------------------------------------------- 1 | # This workflow runs when a comment is made on the ticket 2 | # If the comment starts with "label: " it tries to apply 3 | # the label indicated in rest of comment. 4 | # If the comment starts with "remove label: ", it tries 5 | # to remove the indicated label. 6 | # Note: Labels are allowed to have spaces and this script does 7 | # not parse spaces (as often a space is legitimate), so the command 8 | # "label: really long lots of words label" will apply the 9 | # label "really long lots of words label" 10 | 11 | name: Allows for the adding and removing of labels via comment 12 | 13 | on: 14 | issue_comment: 15 | types: [created] 16 | 17 | jobs: 18 | add_remove_labels: 19 | uses: openedx/.github/.github/workflows/add-remove-label-on-comment.yml@master 20 | 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Python CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: 8 | - '**' 9 | 10 | 11 | jobs: 12 | run_tests: 13 | name: tests 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest] 18 | python-version: 19 | - '3.11' 20 | - '3.12' 21 | toxenv: [quality, docs, django42-drflatest, django52-drflatest, check_keywords] 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: setup python 25 | uses: actions/setup-python@v5 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | 29 | - name: Install pip 30 | run: pip install -r requirements/pip.txt 31 | 32 | - name: Install Dependencies 33 | run: pip install -r requirements/ci.txt 34 | 35 | - name: Run Tests 36 | env: 37 | TOXENV: ${{ matrix.toxenv }} 38 | run: tox 39 | 40 | - name: Run Coverage 41 | if: matrix.python-version == '3.12' && matrix.toxenv=='django52-drflatest' 42 | uses: codecov/codecov-action@v5 43 | with: 44 | token: ${{ secrets.CODECOV_TOKEN }} 45 | files: htmlcov/index.html 46 | flags: unittests 47 | fail_ci_if_error: true 48 | -------------------------------------------------------------------------------- /.github/workflows/commitlint.yml: -------------------------------------------------------------------------------- 1 | # Run commitlint on the commit messages in a pull request. 2 | 3 | name: Lint Commit Messages 4 | 5 | on: 6 | - pull_request 7 | 8 | jobs: 9 | commitlint: 10 | uses: openedx/.github/.github/workflows/commitlint.yml@master 11 | -------------------------------------------------------------------------------- /.github/workflows/publish_pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish package to PyPi 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | 10 | push: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | - name: setup python 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: 3.11 20 | 21 | - name: Install pip 22 | run: pip install -r requirements/pip.txt 23 | 24 | - name: Build package 25 | run: python setup.py sdist bdist_wheel 26 | 27 | - name: Publish to PyPi 28 | uses: pypa/gh-action-pypi-publish@release/v1 29 | with: 30 | user: __token__ 31 | password: ${{ secrets.PYPI_UPLOAD_TOKEN }} 32 | -------------------------------------------------------------------------------- /.github/workflows/self-assign-issue.yml: -------------------------------------------------------------------------------- 1 | # This workflow runs when a comment is made on the ticket 2 | # If the comment starts with "assign me" it assigns the author to the 3 | # ticket (case insensitive) 4 | 5 | name: Assign comment author to ticket if they say "assign me" 6 | on: 7 | issue_comment: 8 | types: [created] 9 | 10 | jobs: 11 | self_assign_by_comment: 12 | uses: openedx/.github/.github/workflows/self-assign-issue.yml@master 13 | -------------------------------------------------------------------------------- /.github/workflows/upgrade-python-requirements.yml: -------------------------------------------------------------------------------- 1 | name: Upgrade Requirements 2 | 3 | on: 4 | schedule: 5 | # will start the job at 3 every monday (UTC) 6 | - cron: "0 3 * * 1" 7 | workflow_dispatch: 8 | inputs: 9 | branch: 10 | description: "Target branch to create requirements PR against" 11 | required: true 12 | default: 'master' 13 | 14 | jobs: 15 | upgrade_requirements: 16 | 17 | with: 18 | branch: ${{ github.event.inputs.branch || 'master' }} 19 | send_success_notification: false 20 | secrets: 21 | requirements_bot_github_token: ${{ secrets.REQUIREMENTS_BOT_GITHUB_TOKEN }} 22 | requirements_bot_github_email: ${{ secrets.REQUIREMENTS_BOT_GITHUB_EMAIL }} 23 | edx_smtp_username: ${{ secrets.EDX_SMTP_USERNAME }} 24 | edx_smtp_password: ${{ secrets.EDX_SMTP_PASSWORD }} 25 | uses: openedx/.github/.github/workflows/upgrade-python-requirements.yml@master 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | __pycache__ 3 | 4 | # C extensions 5 | *.so 6 | 7 | # Packages 8 | *.egg 9 | *.egg-info 10 | dist 11 | build 12 | eggs 13 | parts 14 | bin 15 | var 16 | sdist 17 | develop-eggs 18 | .installed.cfg 19 | lib 20 | lib64 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .cache/ 27 | .coverage 28 | .coverage.* 29 | .pytest_cache/ 30 | .tox 31 | coverage.xml 32 | htmlcov/ 33 | 34 | # Translations 35 | *.mo 36 | 37 | # IDEs and text editors 38 | *~ 39 | *.swp 40 | .idea/ 41 | .project 42 | .pycharm_helpers/ 43 | .pydevproject 44 | 45 | # The Silver Searcher 46 | .agignore 47 | 48 | # OS X artifacts 49 | *.DS_Store 50 | 51 | # Logging 52 | log/ 53 | logs/ 54 | chromedriver.log 55 | ghostdriver.log 56 | 57 | # Complexity 58 | output/*.html 59 | output/*/index.html 60 | 61 | # Sphinx 62 | docs/_build 63 | docs/modules.rst 64 | docs/config_models* 65 | 66 | # Private requirements 67 | requirements/private.in 68 | requirements/private.txt 69 | 70 | # tox environment temporary artifacts 71 | tests/__init__.py 72 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build documentation in the docs/ directory with Sphinx 9 | sphinx: 10 | configuration: docs/conf.py 11 | 12 | # Optionally build your docs in additional formats such as PDF 13 | formats: 14 | - pdf 15 | - epub 16 | 17 | # Set the version of python needed to build these docs. 18 | build: 19 | os: "ubuntu-22.04" 20 | tools: 21 | python: "3.11" 22 | 23 | # Optionally set the version of Python and requirements required to build your docs 24 | python: 25 | install: 26 | - requirements: requirements/doc.txt 27 | 28 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Calen Pennington 2 | Will Daly 3 | Nimisha Asthagiri 4 | Ned Batchelder 5 | Braden MacDonald 6 | Usman Khalid <2200617@gmail.com> 7 | Eric Fischer 8 | Christina Roberts 9 | Brandon DeRosier 10 | Julia Eskew 11 | Lucas Tadeu Teixeira 12 | Brian Mesick 13 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Change Log 2 | ---------- 3 | 4 | .. 5 | All enhancements and patches to django-config-models will be documented 6 | in this file. It adheres to the structure of http://keepachangelog.com/ , 7 | but in reStructuredText instead of Markdown (for ease of incorporation into 8 | Sphinx documentation and the PyPI description). 9 | 10 | This project adheres to Semantic Versioning (http://semver.org/). 11 | 12 | .. There should always be an "Unreleased" section for changes pending release. 13 | 14 | [2.9.0] - 2025-04-12 15 | ~~~~~~~~~~~~~~~~~~~~ 16 | 17 | * Added support for `Django 5.2`. 18 | 19 | [2.8.0] - 2025-02-10 20 | ~~~~~~~~~~~~~~~~~~~~ 21 | 22 | * Drop support for Python 3.8. 23 | 24 | [2.7.0] - 2024-04-03 25 | ~~~~~~~~~~~~~~~~~~~~ 26 | 27 | * Move build requirements out of base.in. 28 | 29 | [2.6.0] - 2024-03-30 30 | ~~~~~~~~~~~~~~~~~~~~ 31 | 32 | * Adding python3.11 and 3.12 support. 33 | 34 | 35 | [2.5.1] - 2023-09-22 36 | ~~~~~~~~~~~~~~~~~~~~ 37 | 38 | * Fixed issues with Django 4.2 39 | 40 | [2.5.0] - 2023-08-07 41 | ~~~~~~~~~~~~~~~~~~~~ 42 | 43 | * Fixed ``ConfigurationModel.current``: it will make sure that it does not return None for current configuration. 44 | 45 | [2.4.0] - 2023-07-19 46 | ~~~~~~~~~~~~~~~~~~~~ 47 | 48 | * Added support for Django42 in CI 49 | * Switch from ``edx-sphinx-theme`` to ``sphinx-book-theme`` since the former is 50 | deprecated 51 | 52 | [2.3.0] - 2022-01-19 53 | ~~~~~~~~~~~~~~~~~~~~ 54 | * Added Support for Django40 in CI 55 | * Dropped Support for Django22, 30, 31 56 | 57 | [2.2.2] - 2021-20-12 58 | ~~~~~~~~~~~~~~~~~~~~ 59 | * Updated dependencies after removing unnecessary constraint on edx-django-utils, so the constraint will no longer be advertised. 60 | 61 | [2.2.1] - 2021-20-12 62 | ~~~~~~~~~~~~~~~~~~~~ 63 | * Replaced deprecated 'django.utils.translation.ugettext' with 'django.utils.translation.gettext' 64 | 65 | [2.2.0] - 2021-07-14 66 | ~~~~~~~~~~~~~~~~~~~~ 67 | * Added support for django3.2 68 | 69 | [2.1.2] - 2021-06-24 70 | ~~~~~~~~~~~~~~~~~~~~ 71 | * Move out django pin from base.in. Now it is coming from global constraint. Ran make upgrade. 72 | 73 | [2.1.1] - 2021-01-28 74 | ~~~~~~~~~~~~~~~~~~~~ 75 | * Fix deprecated reference of ``util.memcache.safe_key`` 76 | 77 | [2.1.0] - 2021-01-12 78 | ~~~~~~~~~~~~~~~~~~~~ 79 | * Dropped Python 3.5 Support 80 | 81 | [2.0.2] - 2020-05-10 82 | ~~~~~~~~~~~~~~~~~~~~ 83 | * Fix html escaping of edit links in admin 84 | 85 | [2.0.1] - 2020-05-08 86 | ~~~~~~~~~~~~~~~~~~~~ 87 | * Dropped support for Django<2.2 88 | * Dropped support for python3.6 89 | * Added support for python3.8 90 | 91 | [2.0.0] - 2020-02-06 92 | ~~~~~~~~~~~~~~~~~~~~ 93 | * Dropping support for Python 2.7 94 | * Switch to using edx-django-utils TieredCache (a two-layer cache that uses both 95 | Django's cache and an internal request-level cache) to reduce the number of 96 | memcached roundtrips. This was a major performance issue that accounted for 97 | 10-20% of transaction time for certain courseware views in edx-platform. 98 | * It is now REQUIRED to add `RequestCacheMiddleware` `to middleware 99 | `_ 100 | to use ConfigModels. 101 | * Remove usage of the "configuration" cache setting. ConfigModels now always use 102 | the default Django cache. 103 | * Django Rest Framework 3.7 and 3.8 are no longer supported. 104 | 105 | [1.0.1] - 2019-04-23 106 | ~~~~~~~~~~~~~~~~~~~~ 107 | * Fix auto publishing to PyPI 108 | 109 | [1.0.0] - 2019-04-23 110 | ~~~~~~~~~~~~~~~~~~~~ 111 | Changed 112 | ------- 113 | * Unpin django-rest-framework requirements. This is a potentially **breaking change** if people were 114 | relying on this package to ensure the correct version of djangorestframework was being installed. 115 | 116 | 117 | [0.2.0] - 2018-07-13 118 | ~~~~~~~~~~~~~~~~~~~~ 119 | 120 | Added 121 | ----- 122 | * Support for Python 3.6 123 | 124 | Removed 125 | ------- 126 | * Testing against Django 1.8 - 1.10 127 | 128 | Changed 129 | ------- 130 | * Updated dependency management to follow OEP-18 131 | 132 | [0.1.10] - 2018-05-21 133 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 134 | 135 | Changed 136 | ------- 137 | * Don't assume the user model is Django's default auth.User 138 | 139 | 140 | [0.1.9] - 2017-08-07 141 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 142 | 143 | Changed 144 | ------- 145 | * Updated Django REST Framework dependency to 3.6 as we were not actually compatible with 3.2. 146 | 147 | 148 | [0.1.8] - 2017-06-19 149 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 150 | 151 | Added 152 | ----- 153 | * Support for Django 1.11. 154 | 155 | 156 | [0.1.7] - 2017-06-19 157 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 158 | * Unreleased version number 159 | 160 | 161 | [0.1.6] - 2017-06-01 162 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 163 | 164 | Added 165 | ----- 166 | * Support for Django 1.10. 167 | 168 | [0.1.1] - [0.1.5] - 2017-06-01 169 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 170 | 171 | Added 172 | _____ 173 | 174 | * Add quality testing to travis run. 175 | * Add encrypted password for package PyPI. 176 | 177 | Removed 178 | ------- 179 | 180 | * Remove the quality condition on deployment. 181 | * Remove the version combos known to fail. 182 | 183 | Changed 184 | ------- 185 | 186 | * Allow for lower versions of djangorestframework, to be compatible with edx-platform. 187 | * Constrict DRF to version that works. 188 | * Update versions of requirements via pip-compile. 189 | * Use different test target - test-all instead of validate. 190 | 191 | Fixed 192 | ----- 193 | 194 | * Fix name and supported versions. 195 | 196 | [0.1.0] - 2016-10-06 197 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 198 | 199 | Added 200 | _____ 201 | 202 | * First release on PyPI. 203 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include CHANGELOG.rst 3 | include CONTRIBUTING.rst 4 | include LICENSE.txt 5 | include README.rst 6 | include requirements/base.in 7 | recursive-include config_models *.html *.png *.gif *js *.css *jpg *jpeg *svg *py 8 | include requirements/constraints.txt 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean compile_translations coverage docs dummy_translations extract_translations \ 2 | fake_translations help pull_translations push_translations quality requirements test \ 3 | test-all validate check_keywords 4 | 5 | .DEFAULT_GOAL := help 6 | 7 | define BROWSER_PYSCRIPT 8 | import os, webbrowser, sys 9 | try: 10 | from urllib import pathname2url 11 | except: 12 | from urllib.request import pathname2url 13 | 14 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 15 | endef 16 | export BROWSER_PYSCRIPT 17 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 18 | 19 | help: ## display this help message 20 | @echo "Please use \`make ' where is one of" 21 | @perl -nle'print $& if m{^[a-zA-Z_-]+:.*?## .*$$}' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m %-25s\033[0m %s\n", $$1, $$2}' 22 | 23 | clean: ## remove generated byte code, coverage reports, and build artifacts 24 | find . -name '*.pyc' -exec rm -f {} + 25 | find . -name '*.pyo' -exec rm -f {} + 26 | find . -name '*~' -exec rm -f {} + 27 | coverage erase 28 | rm -fr build/ 29 | rm -fr dist/ 30 | rm -fr *.egg-info 31 | 32 | compile_translations: ## compile translation files, outputting .po files for each supported language 33 | ./manage.py compilemessages 34 | 35 | coverage: clean ## generate and view HTML coverage report 36 | pytest --cov-report html 37 | $(BROWSER) htmlcov/index.html 38 | 39 | docs: ## generate Sphinx HTML documentation, including API docs 40 | tox -e docs 41 | $(BROWSER) docs/_build/html/index.html 42 | 43 | dummy_translations: ## generate dummy translation (.po) files 44 | cd config_models && i18n_tool dummy 45 | 46 | extract_translations: ## extract strings to be translated, outputting .mo files 47 | ./manage.py makemessages -l en -v1 -d django 48 | ./manage.py makemessages -l en -v1 -d djangojs 49 | 50 | fake_translations: extract_translations dummy_translations compile_translations ## generate and compile dummy translation files 51 | 52 | upgrade: export CUSTOM_COMPILE_COMMAND=make upgrade 53 | 54 | # Define PIP_COMPILE_OPTS="-v" to get more information during make compile-requirements. 55 | compile-requirements: export CUSTOM_COMPILE_COMMAND=make upgrade 56 | compile-requirements: ## Re-compile requirements/*.in files to *.txt. 57 | pip install -r requirements/pip-tools.txt 58 | pip-compile $(COMPILE_OPTS) --allow-unsafe requirements/pip.in 59 | pip-compile $(COMPILE_OPTS) requirements/pip-tools.in 60 | pip install -qr requirements/pip.txt 61 | pip install -qr requirements/pip-tools.txt 62 | pip-compile $(COMPILE_OPTS) requirements/base.in 63 | pip-compile $(COMPILE_OPTS) requirements/test.in 64 | pip-compile $(COMPILE_OPTS) requirements/doc.in 65 | pip-compile $(COMPILE_OPTS) requirements/quality.in 66 | pip-compile $(COMPILE_OPTS) requirements/ci.in 67 | pip-compile $(COMPILE_OPTS) requirements/dev.in 68 | # Let tox control the Django and djangorestframework versions for tests 69 | sed -i.tmp '/^[d|D]jango==/d' requirements/test.txt 70 | sed -i.tmp '/^djangorestframework==/d' requirements/test.txt 71 | rm requirements/test.txt.tmp 72 | 73 | upgrade: ## update the requirements/*.txt files with the latest packages satisfying requirements/*.in 74 | $(MAKE) compile-requirements COMPILE_OPTS="--upgrade" 75 | 76 | pull_translations: ## pull translations from Transifex 77 | tx pull -t -a 78 | 79 | push_translations: ## push source translation files (.po) from Transifex 80 | tx push -s 81 | 82 | quality: ## check coding style with pycodestyle and pylint 83 | tox -e quality 84 | 85 | requirements: ## install development environment requirements 86 | pip install -qr requirements/pip.txt 87 | pip install -qr requirements/pip-tools.txt 88 | pip-sync requirements/dev.txt requirements/private.* 89 | 90 | test: clean ## run tests in the current virtualenv 91 | PYTHONPATH=./:.. pytest 92 | 93 | test-all: ## run tests on every supported Python/Django combination 94 | tox -e quality 95 | tox 96 | 97 | validate: quality test ## run tests and quality checks 98 | 99 | check_keywords: ## Scan the Django models in all installed apps in this project for restricted field names 100 | python manage.py check_reserved_keywords --override_file db_keyword_overrides.yml 101 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-config-models 2 | ******************** 3 | 4 | |CI|_ |Codecov|_ |pypi-badge| |doc-badge| |pyversions-badge| |license-badge| |status-badge| 5 | =========================================================================================== 6 | 7 | .. |CI| image:: https://github.com/openedx/django-config-models/workflows/Python%20CI/badge.svg?branch=master 8 | .. _CI: https://github.com/openedx/django-config-models/actions?query=workflow%3A%22Python+CI%22 9 | 10 | .. |Codecov| image:: http://codecov.io/github/openedx/django-config-models/coverage.svg?branch=master 11 | .. _Codecov: http://codecov.io/github/openedx/django-config-models?branch=master 12 | 13 | .. |pypi-badge| image:: https://img.shields.io/pypi/v/django-config-models.svg 14 | :target: https://pypi.python.org/pypi/django-config-models/ 15 | :alt: PyPI 16 | 17 | .. |doc-badge| image:: https://readthedocs.org/projects/django-config-models/badge/?version=latest 18 | :target: http://django-config-models.readthedocs.io/en/latest/ 19 | :alt: Documentation 20 | 21 | .. |pyversions-badge| image:: https://img.shields.io/pypi/pyversions/django-config-models.svg 22 | :target: https://pypi.python.org/pypi/django-config-models/ 23 | :alt: Supported Python versions 24 | 25 | .. |license-badge| image:: https://img.shields.io/github/license/edx/django-config-models.svg 26 | :target: https://github.com/openedx/django-config-models/blob/master/LICENSE.txt 27 | :alt: License 28 | 29 | .. |status-badge| image:: https://img.shields.io/badge/Status-Maintained-brightgreen 30 | :alt: Maintenance status 31 | 32 | 33 | Purpose 34 | ------- 35 | 36 | This app allows other apps to easily define a configuration model 37 | that can be hooked into the admin site to allow configuration management 38 | with auditing. 39 | 40 | Getting Started 41 | --------------- 42 | 43 | Add ``config_models`` to your ``INSTALLED_APPS`` list. 44 | 45 | Usage 46 | ----- 47 | 48 | Create a subclass of ``ConfigurationModel``, with fields for each 49 | value that needs to be configured:: 50 | 51 | class MyConfiguration(ConfigurationModel): 52 | frobble_timeout = IntField(default=10) 53 | frazzle_target = TextField(defalut="debug") 54 | 55 | This is a normal django model, so it must be synced and migrated as usual. 56 | 57 | The default values for the fields in the ``ConfigurationModel`` will be 58 | used if no configuration has yet been created. 59 | 60 | Register that class with the Admin site, using the ``ConfigurationAdminModel``:: 61 | 62 | from django.contrib import admin 63 | 64 | from config_models.admin import ConfigurationModelAdmin 65 | 66 | admin.site.register(MyConfiguration, ConfigurationModelAdmin) 67 | 68 | Use the configuration in your code:: 69 | 70 | def my_view(self, request): 71 | config = MyConfiguration.current() 72 | fire_the_missiles(config.frazzle_target, timeout=config.frobble_timeout) 73 | 74 | Use the admin site to add new configuration entries. The most recently created 75 | entry is considered to be ``current``. 76 | 77 | Configuration 78 | ------------- 79 | 80 | The current ``ConfigurationModel`` will be cached in the ``configuration`` django cache, 81 | or in the ``default`` cache if ``configuration`` doesn't exist. The ``configuration`` and ``default`` caches 82 | are specified in the django ``CACHES`` setting. The caching can be per-process, per-machine, per-cluster, or 83 | some other strategy, depending on the cache configuration. 84 | 85 | You can specify the cache timeout in each ``ConfigurationModel`` by setting the ``cache_timeout`` property. 86 | 87 | You can change the name of the cache key used by the ``ConfigurationModel`` by overriding 88 | the ``cache_key_name`` function. 89 | 90 | Extension 91 | --------- 92 | 93 | ``ConfigurationModels`` are just django models, so they can be extended with new fields 94 | and migrated as usual. Newly added fields must have default values and should be nullable, 95 | so that rollbacks to old versions of configuration work correctly. 96 | 97 | Documentation 98 | ------------- 99 | 100 | The full documentation is at https://django-config-models.readthedocs.org. 101 | 102 | License 103 | ------- 104 | 105 | The code in this repository is licensed under the AGPL 3.0 unless 106 | otherwise noted. 107 | 108 | Please see ``LICENSE.txt`` for details. 109 | 110 | Getting Help 111 | ------------ 112 | 113 | If you're having trouble, we have discussion forums at 114 | `discuss.openedx.org `_ where you can connect with others in the 115 | community. 116 | 117 | Our real-time conversations are on Slack. You can request a `Slack 118 | invitation`_, then join our `community Slack workspace`_. 119 | 120 | For anything non-trivial, the best path is to `open an issue`__ in this 121 | repository with as many details about the issue you are facing as you 122 | can provide. 123 | 124 | __ https://github.com/openedx/django-config-models /issues 125 | 126 | For more information about these options, see the `Getting Help`_ page. 127 | 128 | .. _Slack invitation: https://openedx.org/slack 129 | .. _community Slack workspace: https://openedx.slack.com/ 130 | .. _Getting Help: https://openedx.org/getting-help 131 | 132 | How To Contribute 133 | ----------------- 134 | 135 | Contributions are very welcome. 136 | 137 | Please read `How To Contribute `_ for details. 138 | 139 | 140 | This project is currently accepting all types of contributions, bug fixes, security fixes, maintenance work, or new features. However, please make sure to have a discussion about your new feature idea with the maintainers prior to beginning development to maximize the chances of your change being accepted. You can start a conversation by creating a new issue on this repo summarizing your idea. 141 | 142 | Open edX Code of Conduct 143 | ------------------------ 144 | All community members are expected to follow the `Open edX Code of Conduct`_. 145 | 146 | .. _Open edX Code of Conduct: https://openedx.org/code-of-conduct/ 147 | 148 | People 149 | ------ 150 | The assigned maintainers for this component and other project details may be 151 | found in `Backstage`_. Backstage pulls this data from the ``catalog-info.yaml`` 152 | file in this repo. 153 | 154 | .. _Backstage: https://backstage.openedx.org/catalog/default/component/django-config-models 155 | 156 | Reporting Security Issues 157 | ------------------------- 158 | 159 | Please do not report security issues in public. Please email security@openedx.org. 160 | -------------------------------------------------------------------------------- /catalog-info.yaml: -------------------------------------------------------------------------------- 1 | # This file records information about this repo. Its use is described in OEP-55: 2 | # https://open-edx-proposals.readthedocs.io/en/latest/processes/oep-0055-proc-project-maintainers.html 3 | 4 | apiVersion: backstage.io/v1alpha1 5 | kind: Component 6 | metadata: 7 | name: django-config-models 8 | description: "This app allows other apps to easily define a configuration model that can be hooked into the admin site to allow configuration management with auditing." 9 | links: 10 | - url: "https://django-config-models.readthedocs.io/en/latest/" 11 | title: "Library documentation" 12 | icon: "Web" 13 | annotations: 14 | openedx.org/arch-interest-groups: "" 15 | spec: 16 | owner: "user:usamasadiq" 17 | type: 'library' 18 | lifecycle: 'production' 19 | -------------------------------------------------------------------------------- /config_models/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration models for Django allowing config management with auditing. 3 | """ 4 | 5 | __version__ = '2.9.0' 6 | -------------------------------------------------------------------------------- /config_models/admin.py: -------------------------------------------------------------------------------- 1 | """ 2 | Admin site models for managing :class:`.ConfigurationModel` subclasses. 3 | """ 4 | 5 | 6 | from django.contrib import admin 7 | from django.contrib.admin import ListFilter 8 | from django.core.cache import InvalidCacheBackendError, caches 9 | from django.core.files.base import File 10 | from django.forms import models 11 | from django.http import HttpResponseRedirect 12 | from django.shortcuts import get_object_or_404 13 | from django.urls import reverse 14 | from django.utils.html import format_html 15 | from django.utils.translation import gettext_lazy as _ 16 | 17 | try: 18 | cache = caches['configuration'] 19 | except InvalidCacheBackendError: 20 | from django.core.cache import cache 21 | 22 | 23 | class ConfigurationModelAdmin(admin.ModelAdmin): 24 | """ 25 | :class:`~django.contrib.admin.ModelAdmin` for :class:`.ConfigurationModel` subclasses 26 | """ 27 | 28 | date_hierarchy = 'change_date' 29 | 30 | def get_actions(self, request): 31 | """ 32 | Get the actions. 33 | """ 34 | return { 35 | 'revert': (ConfigurationModelAdmin.revert, 'revert', _('Revert to the selected configuration')) 36 | } 37 | 38 | def get_list_display(self, request): 39 | """ 40 | Get the list display. 41 | """ 42 | return self.get_displayable_field_names() 43 | 44 | def get_displayable_field_names(self): 45 | """ 46 | Return all field names, excluding reverse foreign key relationships. 47 | """ 48 | return [ 49 | f.name 50 | for f in self.model._meta.get_fields() 51 | if not f.one_to_many 52 | ] 53 | 54 | # Don't allow deletion of configuration 55 | def has_delete_permission(self, request, obj=None): 56 | return False 57 | 58 | # Make all fields read-only when editing an object 59 | def get_readonly_fields(self, request, obj=None): 60 | if obj: # editing an existing object 61 | return self.get_displayable_field_names() 62 | return self.readonly_fields 63 | 64 | def add_view(self, request, form_url='', extra_context=None): 65 | # Prepopulate new configuration entries with the value of the current config 66 | get = request.GET.copy() 67 | get.update(models.model_to_dict(self.model.current())) 68 | request.GET = get 69 | return super().add_view(request, form_url, extra_context) 70 | 71 | # Hide the save buttons in the change view 72 | def change_view(self, request, object_id, form_url='', extra_context=None): 73 | extra_context = extra_context or {} 74 | extra_context['readonly'] = True 75 | return super().change_view( 76 | request, 77 | object_id, 78 | form_url, 79 | extra_context=extra_context 80 | ) 81 | 82 | def save_model(self, request, obj, form, change): 83 | obj.changed_by = request.user 84 | super().save_model(request, obj, form, change) 85 | cache.delete(obj.cache_key_name(*(getattr(obj, key_name) for key_name in obj.KEY_FIELDS))) 86 | cache.delete(obj.key_values_cache_key_name()) 87 | 88 | def revert(self, request, queryset): 89 | """ 90 | Admin action to revert a configuration back to the selected value 91 | """ 92 | if queryset.count() != 1: 93 | self.message_user(request, _("Please select a single configuration to revert to.")) 94 | return None 95 | 96 | target = queryset[0] 97 | target.id = None 98 | self.save_model(request, target, None, False) 99 | self.message_user(request, _("Reverted configuration.")) 100 | 101 | return HttpResponseRedirect( 102 | reverse( 103 | f"admin:{self.model._meta.app_label}_{self.model._meta.model_name}_change", 104 | args=(target.id,), 105 | ) 106 | ) 107 | 108 | 109 | class ShowHistoryFilter(ListFilter): 110 | """ 111 | Admin change view filter to show only the most recent (i.e. the "current") row for each 112 | unique key value. 113 | """ 114 | title = _('Status') 115 | parameter_name = 'show_history' 116 | 117 | def __init__(self, request, params, model, model_admin): 118 | super().__init__(request, params, model, model_admin) 119 | if self.parameter_name in params: 120 | value = params.pop(self.parameter_name) 121 | self.used_parameters[self.parameter_name] = value 122 | 123 | def has_output(self): 124 | """ Should this filter be shown? """ 125 | return True 126 | 127 | def choices(self, changelist): 128 | """ Returns choices ready to be output in the template. """ 129 | show_all = self.used_parameters.get(self.parameter_name) == "1" 130 | return ( 131 | { 132 | 'display': _('Current Configuration'), 133 | 'selected': not show_all, 134 | 'query_string': changelist.get_query_string({}, [self.parameter_name]), 135 | }, 136 | { 137 | 'display': _('All (Show History)'), 138 | 'selected': show_all, 139 | 'query_string': changelist.get_query_string({self.parameter_name: "1"}, []), 140 | } 141 | ) 142 | 143 | def queryset(self, request, queryset): 144 | """ Filter the queryset. No-op since it's done by KeyedConfigurationModelAdmin """ 145 | return queryset 146 | 147 | def expected_parameters(self): 148 | """ List the query string params used by this filter """ 149 | return [self.parameter_name] 150 | 151 | 152 | class KeyedConfigurationModelAdmin(ConfigurationModelAdmin): 153 | """ 154 | :class:`~django.contrib.admin.ModelAdmin` for :class:`.ConfigurationModel` subclasses that 155 | use extra keys (i.e. they have KEY_FIELDS set). 156 | """ 157 | date_hierarchy = None 158 | list_filter = (ShowHistoryFilter, ) 159 | 160 | def get_queryset(self, request): 161 | """ 162 | Annote the queryset with an 'is_active' property that's true iff that row is the most 163 | recently added row for that particular set of KEY_FIELDS values. 164 | Filter the queryset to show only is_active rows by default. 165 | """ 166 | if request.GET.get(ShowHistoryFilter.parameter_name) == '1': 167 | queryset = self.model.objects.with_active_flag() 168 | else: 169 | # Show only the most recent row for each key. 170 | queryset = self.model.objects.current_set() 171 | ordering = self.get_ordering(request) 172 | if ordering: 173 | return queryset.order_by(*ordering) 174 | return queryset 175 | 176 | def get_list_display(self, request): 177 | """ Add a link to each row for creating a new row using the chosen row as a template """ 178 | return self.get_displayable_field_names() + ['edit_link'] 179 | 180 | def add_view(self, request, form_url='', extra_context=None): 181 | # Prepopulate new configuration entries with the value of the current config, if given: 182 | if 'source' in request.GET: 183 | get = request.GET.copy() 184 | source_id = int(get.pop('source')[0]) 185 | source = get_object_or_404(self.model, pk=source_id) 186 | source_dict = models.model_to_dict(source) 187 | for field_name, field_value in source_dict.items(): 188 | # read files into request.FILES, if: 189 | # * user hasn't ticked the "clear" checkbox 190 | # * user hasn't uploaded a new file 191 | if field_value and isinstance(field_value, File): 192 | clear_checkbox_name = f'{field_name}-clear' 193 | if request.POST.get(clear_checkbox_name) != 'on': 194 | request.FILES.setdefault(field_name, field_value) 195 | get[field_name] = field_value 196 | request.GET = get 197 | # Call our grandparent's add_view, skipping the parent code 198 | # because the parent code has a different way to prepopulate new configuration entries 199 | # with the value of the latest config, which doesn't make sense for keyed models. 200 | return super(ConfigurationModelAdmin, self).add_view(request, form_url, extra_context) 201 | 202 | @admin.display( 203 | description=_('Update') 204 | ) 205 | def edit_link(self, inst): 206 | """ Edit link for the change view """ 207 | if not inst.is_active: 208 | return '--' 209 | update_url = reverse(f'admin:{self.model._meta.app_label}_{self.model._meta.model_name}_add') 210 | update_url += f"?source={inst.pk}" 211 | return format_html('{}', update_url, _('Update')) 212 | -------------------------------------------------------------------------------- /config_models/apps.py: -------------------------------------------------------------------------------- 1 | """ 2 | config_models Django application initialization. 3 | """ 4 | 5 | from django.apps import AppConfig 6 | 7 | 8 | class ConfigModelsConfig(AppConfig): 9 | """ 10 | Configuration for the config_models Django application. 11 | """ 12 | 13 | name = 'config_models' 14 | -------------------------------------------------------------------------------- /config_models/conf/locale/config.yaml: -------------------------------------------------------------------------------- 1 | # Configuration for i18n workflow. 2 | 3 | locales: 4 | - en # English - Source Language 5 | - am # Amharic 6 | - ar # Arabic 7 | - az # Azerbaijani 8 | - bg_BG # Bulgarian (Bulgaria) 9 | - bn_BD # Bengali (Bangladesh) 10 | - bn_IN # Bengali (India) 11 | - bs # Bosnian 12 | - ca # Catalan 13 | - ca@valencia # Catalan (Valencia) 14 | - cs # Czech 15 | - cy # Welsh 16 | - da # Danish 17 | - de_DE # German (Germany) 18 | - el # Greek 19 | - en_GB # English (United Kingdom) 20 | # Don't pull these until we figure out why pages randomly display in these locales, 21 | # when the user's browser is in English and the user is not logged in. 22 | # - en@lolcat # LOLCAT English 23 | # - en@pirate # Pirate English 24 | - es_419 # Spanish (Latin America) 25 | - es_AR # Spanish (Argentina) 26 | - es_EC # Spanish (Ecuador) 27 | - es_ES # Spanish (Spain) 28 | - es_MX # Spanish (Mexico) 29 | - es_PE # Spanish (Peru) 30 | - et_EE # Estonian (Estonia) 31 | - eu_ES # Basque (Spain) 32 | - fa # Persian 33 | - fa_IR # Persian (Iran) 34 | - fi_FI # Finnish (Finland) 35 | - fil # Filipino 36 | - fr # French 37 | - gl # Galician 38 | - gu # Gujarati 39 | - he # Hebrew 40 | - hi # Hindi 41 | - hr # Croatian 42 | - hu # Hungarian 43 | - hy_AM # Armenian (Armenia) 44 | - id # Indonesian 45 | - it_IT # Italian (Italy) 46 | - ja_JP # Japanese (Japan) 47 | - kk_KZ # Kazakh (Kazakhstan) 48 | - km_KH # Khmer (Cambodia) 49 | - kn # Kannada 50 | - ko_KR # Korean (Korea) 51 | - lt_LT # Lithuanian (Lithuania) 52 | - ml # Malayalam 53 | - mn # Mongolian 54 | - mr # Marathi 55 | - ms # Malay 56 | - nb # Norwegian Bokmål 57 | - ne # Nepali 58 | - nl_NL # Dutch (Netherlands) 59 | - or # Oriya 60 | - pl # Polish 61 | - pt_BR # Portuguese (Brazil) 62 | - pt_PT # Portuguese (Portugal) 63 | - ro # Romanian 64 | - ru # Russian 65 | - si # Sinhala 66 | - sk # Slovak 67 | - sl # Slovenian 68 | - sq # Albanian 69 | - sr # Serbian 70 | - ta # Tamil 71 | - te # Telugu 72 | - th # Thai 73 | - tr_TR # Turkish (Turkey) 74 | - uk # Ukranian 75 | - ur # Urdu 76 | - uz # Uzbek 77 | - vi # Vietnamese 78 | - zh_CN # Chinese (China) 79 | - zh_HK # Chinese (Hong Kong) 80 | - zh_TW # Chinese (Taiwan) 81 | 82 | # The locales used for fake-accented English, for testing. 83 | dummy_locales: 84 | - eo 85 | -------------------------------------------------------------------------------- /config_models/decorators.py: -------------------------------------------------------------------------------- 1 | """ 2 | Decorators for model-based configuration. 3 | """ 4 | 5 | 6 | from functools import wraps 7 | 8 | from django.http import HttpResponseNotFound 9 | 10 | 11 | def require_config(config_model): 12 | """ 13 | View decorator that enables/disables a view based on configuration. 14 | 15 | Arguments: 16 | config_model (ConfigurationModel subclass): The class of the configuration 17 | model to check. 18 | 19 | Returns: 20 | HttpResponse: 404 if the configuration model is disabled, 21 | otherwise returns the response from the decorated view. 22 | 23 | """ 24 | def _decorator(func): 25 | """ 26 | Decorator implementation. 27 | """ 28 | @wraps(func) 29 | def _inner(*args, **kwargs): 30 | """ 31 | Wrapper implementation. 32 | """ 33 | if not config_model.current().enabled: 34 | return HttpResponseNotFound() 35 | return func(*args, **kwargs) 36 | return _inner 37 | return _decorator 38 | -------------------------------------------------------------------------------- /config_models/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openedx/django-config-models/9097f36b0c54fef87ff9ae35f426e44ded6437ed/config_models/management/__init__.py -------------------------------------------------------------------------------- /config_models/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openedx/django-config-models/9097f36b0c54fef87ff9ae35f426e44ded6437ed/config_models/management/commands/__init__.py -------------------------------------------------------------------------------- /config_models/management/commands/populate_model.py: -------------------------------------------------------------------------------- 1 | """ 2 | Populates a ConfigurationModel by deserializing JSON data contained in a file. 3 | """ 4 | import os 5 | 6 | from django.core.management.base import BaseCommand, CommandError 7 | from django.utils.translation import gettext_lazy as _ 8 | 9 | from config_models.utils import deserialize_json 10 | 11 | 12 | class Command(BaseCommand): 13 | """ 14 | This command will deserialize the JSON data in the supplied file to populate 15 | a ConfigurationModel. Note that this will add new entries to the model, but it 16 | will not delete any entries (ConfigurationModel entries are read-only). 17 | """ 18 | help = """ 19 | Populates a ConfigurationModel by deserializing the supplied JSON. 20 | 21 | JSON should be in a file, with the following format: 22 | 23 | { "model": "config_models.ExampleConfigurationModel", 24 | "data": 25 | [ 26 | { "enabled": True, 27 | "color": "black" 28 | ... 29 | }, 30 | { "enabled": False, 31 | "color": "yellow" 32 | ... 33 | }, 34 | ... 35 | ] 36 | } 37 | 38 | A username corresponding to an existing user must be specified to indicate who 39 | is executing the command. 40 | 41 | $ ... populate_model -f path/to/file.json -u username 42 | """ 43 | 44 | def add_arguments(self, parser): 45 | parser.add_argument( 46 | '-f', 47 | '--file', 48 | metavar='JSON_FILE', 49 | dest='file', 50 | default=False, 51 | help='JSON file to import ConfigurationModel data' 52 | ) 53 | 54 | parser.add_argument( 55 | '-u', 56 | '--username', 57 | metavar='USERNAME', 58 | dest='username', 59 | default=False, 60 | help='username to specify who is executing the command' 61 | ) 62 | 63 | def handle(self, *args, **options): 64 | if 'file' not in options or not options['file']: 65 | raise CommandError(_("A file containing JSON must be specified.")) 66 | 67 | if 'username' not in options or not options['username']: 68 | raise CommandError(_("A valid username must be specified.")) 69 | 70 | json_file = options['file'] 71 | if not os.path.exists(json_file): 72 | raise CommandError(_("File {0} does not exist").format(json_file)) 73 | 74 | self.stdout.write(_("Importing JSON data from file {0}").format(json_file)) 75 | with open(json_file, "rb") as data: 76 | created_entries = deserialize_json(data, options['username']) 77 | self.stdout.write(_("Import complete, {0} new entries created").format(created_entries)) 78 | -------------------------------------------------------------------------------- /config_models/models.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django Model baseclass for database-backed configuration. 3 | """ 4 | 5 | 6 | from django.conf import settings 7 | # The following import exists for backwards compatibility (because a number of 8 | # library users assume config_models.models.cache is importable), but 9 | # ConfigModels will now ignore the custom 'configuration' cache setting and just 10 | # use TieredCache, which will make use of a local request cache + the default 11 | # Django cache. 12 | from django.core.cache import cache # pylint: disable=unused-import 13 | from django.db import models 14 | from django.utils.translation import gettext_lazy as _ 15 | from edx_django_utils.cache.utils import TieredCache 16 | from rest_framework.utils import model_meta 17 | 18 | 19 | class ConfigurationModelManager(models.Manager): 20 | """ 21 | Query manager for ConfigurationModel 22 | """ 23 | def _current_ids_subquery(self): 24 | """ 25 | Internal helper method to return an SQL string that will get the IDs of 26 | all the current entries (i.e. the most recent entry for each unique set 27 | of key values). Only useful if KEY_FIELDS is set. 28 | """ 29 | return self.values(*self.model.KEY_FIELDS).annotate(max=models.Max('pk')).values('max') 30 | 31 | def current_set(self): 32 | """ 33 | A queryset for the active configuration entries only. Only useful if KEY_FIELDS is set. 34 | 35 | Active means the means recent entries for each unique combination of keys. It does not 36 | necessaryily mean enbled. 37 | """ 38 | assert self.model.KEY_FIELDS != (), "Just use model.current() if there are no KEY_FIELDS" 39 | return self.get_queryset().filter( 40 | pk__in=self._current_ids_subquery() 41 | ).annotate( 42 | is_active=models.Value(1, output_field=models.IntegerField()) 43 | ) 44 | 45 | def with_active_flag(self): 46 | """ 47 | A query set where each result is annotated with an 'is_active' field that indicates 48 | if it's the most recent entry for that combination of keys. 49 | """ 50 | if self.model.KEY_FIELDS: 51 | return self.get_queryset().annotate( 52 | is_active=models.ExpressionWrapper( 53 | models.Q(pk__in=self._current_ids_subquery()), 54 | output_field=models.IntegerField(), 55 | ) 56 | ) 57 | return self.get_queryset().annotate( 58 | is_active=models.ExpressionWrapper( 59 | models.Q(pk=self.model.current().pk), 60 | output_field=models.IntegerField(), 61 | ) 62 | ) 63 | 64 | 65 | class ConfigurationModel(models.Model): 66 | """ 67 | Abstract base class for model-based configuration 68 | 69 | Properties: 70 | cache_timeout (int): The number of seconds that this configuration 71 | should be cached 72 | """ 73 | 74 | class Meta: 75 | abstract = True 76 | ordering = ("-change_date", ) 77 | 78 | objects = ConfigurationModelManager() 79 | 80 | KEY_FIELDS = () 81 | 82 | # The number of seconds 83 | cache_timeout = 600 84 | 85 | change_date = models.DateTimeField(auto_now_add=True, verbose_name=_("Change date")) 86 | changed_by = models.ForeignKey( 87 | settings.AUTH_USER_MODEL, 88 | editable=False, 89 | null=True, 90 | on_delete=models.PROTECT, 91 | # Translators: this label indicates the name of the user who made this change: 92 | verbose_name=_("Changed by"), 93 | ) 94 | enabled = models.BooleanField(default=False, verbose_name=_("Enabled")) 95 | 96 | def save(self, force_insert=False, force_update=False, using=None, 97 | update_fields=None): 98 | """ 99 | Clear the cached value when saving a new configuration entry 100 | """ 101 | # Always create a new entry, instead of updating an existing model 102 | self.pk = None 103 | super().save( 104 | force_insert, 105 | force_update, 106 | using, 107 | update_fields 108 | ) 109 | TieredCache.delete_all_tiers(self.cache_key_name(*[getattr(self, key) for key in self.KEY_FIELDS])) 110 | if self.KEY_FIELDS: 111 | TieredCache.delete_all_tiers(self.key_values_cache_key_name()) 112 | 113 | @classmethod 114 | def cache_key_name(cls, *args): 115 | """Return the name of the key to use to cache the current configuration""" 116 | if cls.KEY_FIELDS != (): # pylint: disable=use-implicit-booleaness-not-comparison 117 | if len(args) != len(cls.KEY_FIELDS): 118 | raise TypeError( 119 | f"cache_key_name() takes exactly {len(cls.KEY_FIELDS)} arguments ({len(args)} given)" 120 | ) 121 | return f"configuration/{cls.__name__}/current/{','.join(str(arg) for arg in args)}" 122 | else: 123 | return f'configuration/{cls.__name__}/current' 124 | 125 | @classmethod 126 | def current(cls, *args): 127 | """ 128 | Return the active configuration entry, either from cache, 129 | from the database, or by creating a new empty entry (which is not 130 | persisted). 131 | """ 132 | cache_key = cls.cache_key_name(*args) 133 | cached_response = TieredCache.get_cached_response(cache_key) 134 | if cached_response.is_found and cached_response.value is not None: 135 | return cached_response.value 136 | 137 | key_dict = dict(zip(cls.KEY_FIELDS, args)) 138 | try: 139 | current = cls.objects.filter(**key_dict).order_by('-change_date')[0] 140 | except IndexError: 141 | current = cls(**key_dict) 142 | 143 | TieredCache.set_all_tiers(cache_key, current, cls.cache_timeout) 144 | return current 145 | 146 | @classmethod 147 | def is_enabled(cls, *key_fields): 148 | """ 149 | Returns True if this feature is configured as enabled, else False. 150 | 151 | Arguments: 152 | key_fields: The positional arguments are the KEY_FIELDS used to identify the 153 | configuration to be checked. 154 | """ 155 | return cls.current(*key_fields).enabled 156 | 157 | @classmethod 158 | def key_values_cache_key_name(cls, *key_fields): 159 | """ Key for fetching unique key values from the cache """ 160 | key_fields = key_fields or cls.KEY_FIELDS 161 | return f"configuration/{cls.__name__}/key_values/{','.join(key_fields)}" 162 | 163 | @classmethod 164 | def key_values(cls, *key_fields, **kwargs): 165 | """ 166 | Get the set of unique values in the configuration table for the given 167 | key[s]. Calling cls.current(*value) for each value in the resulting 168 | list should always produce an entry, though any such entry may have 169 | enabled=False. 170 | 171 | Arguments: 172 | key_fields: The positional arguments are the KEY_FIELDS to return. For example if 173 | you had a course embargo configuration where each entry was keyed on (country, 174 | course), then you might want to know "What countries have embargoes configured?" 175 | with cls.key_values('country'), or "Which courses have country restrictions?" 176 | with cls.key_values('course'). You can also leave this unspecified for the 177 | default, which returns the distinct combinations of all keys. 178 | flat: If you pass flat=True as a kwarg, it has the same effect as in Django's 179 | 'values_list' method: Instead of returning a list of lists, you'll get one list 180 | of values. This makes sense to use whenever there is only one key being queried. 181 | 182 | Return value: 183 | List of lists of each combination of keys found in the database. 184 | e.g. [("Italy", "course-v1:SomeX+some+2015"), ...] for the course embargo example 185 | """ 186 | flat = kwargs.pop('flat', False) 187 | assert not kwargs, "'flat' is the only kwarg accepted" 188 | key_fields = key_fields or cls.KEY_FIELDS 189 | cache_key = cls.key_values_cache_key_name(*key_fields) 190 | cached_response = TieredCache.get_cached_response(cache_key) 191 | if cached_response.is_found: 192 | return cached_response.value 193 | 194 | values = list(cls.objects.values_list(*key_fields, flat=flat).order_by().distinct()) 195 | TieredCache.set_all_tiers(cache_key, values, cls.cache_timeout) 196 | return values 197 | 198 | def fields_equal(self, instance, fields_to_ignore=("id", "change_date", "changed_by")): 199 | """ 200 | Compares this instance's fields to the supplied instance to test for equality. 201 | This will ignore any fields in `fields_to_ignore`. 202 | 203 | Note that this method ignores many-to-many fields. 204 | 205 | Args: 206 | instance: the model instance to compare 207 | fields_to_ignore: List of fields that should not be compared for equality. By default 208 | includes `id`, `change_date`, and `changed_by`. 209 | 210 | Returns: True if the checked fields are all equivalent, else False 211 | """ 212 | for field in self._meta.get_fields(): 213 | if not field.many_to_many and field.name not in fields_to_ignore: 214 | if getattr(instance, field.name) != getattr(self, field.name): 215 | return False 216 | 217 | return True 218 | 219 | @classmethod 220 | def equal_to_current(cls, json, fields_to_ignore=("id", "change_date", "changed_by")): 221 | """ 222 | Compares for equality this instance to a model instance constructed from the supplied JSON. 223 | This will ignore any fields in `fields_to_ignore`. 224 | 225 | Note that this method cannot handle fields with many-to-many associations, as those can only 226 | be set on a saved model instance (and saving the model instance will create a new entry). 227 | All many-to-many field entries will be removed before the equality comparison is done. 228 | 229 | Args: 230 | json: json representing an entry to compare 231 | fields_to_ignore: List of fields that should not be compared for equality. By default 232 | includes `id`, `change_date`, and `changed_by`. 233 | 234 | Returns: True if the checked fields are all equivalent, else False 235 | """ 236 | 237 | # Remove many-to-many relationships from json. 238 | # They require an instance to be already saved. 239 | info = model_meta.get_field_info(cls) 240 | for field_name, relation_info in info.relations.items(): 241 | if relation_info.to_many and (field_name in json): 242 | json.pop(field_name) 243 | 244 | new_instance = cls(**json) 245 | key_field_args = tuple(getattr(new_instance, key) for key in cls.KEY_FIELDS) 246 | current = cls.current(*key_field_args) 247 | # If current.id is None, no entry actually existed and the "current" method created it. 248 | if current.id is not None: 249 | return current.fields_equal(new_instance, fields_to_ignore) 250 | 251 | return False 252 | -------------------------------------------------------------------------------- /config_models/templates/config_models/base.html: -------------------------------------------------------------------------------- 1 | 2 | {% comment %} 3 | As the developer of this package, don't place anything here if you can help it 4 | since this allows developers to have interoperability between your template 5 | structure and their own. 6 | 7 | Example: Developer melding the 2SoD pattern to fit inside with another pattern:: 8 | 9 | {% extends "base.html" %} 10 | {% load static %} 11 | 12 | 13 | {% block extra_js %} 14 | 15 | 16 | {% block javascript %} 17 | 18 | {% endblock javascript %} 19 | 20 | {% endblock extra_js %} 21 | {% endcomment %} 22 | 23 | -------------------------------------------------------------------------------- /config_models/templatetags.py: -------------------------------------------------------------------------------- 1 | """ 2 | Override the submit_row template tag to remove all save buttons from the 3 | admin dashboard change view if the context has readonly marked in it. 4 | """ 5 | from django.contrib.admin.templatetags.admin_modify import register 6 | from django.contrib.admin.templatetags.admin_modify import submit_row as original_submit_row 7 | 8 | 9 | @register.inclusion_tag('admin/submit_line.html', takes_context=True) 10 | def submit_row(context): 11 | """ 12 | Overrides 'django.contrib.admin.templatetags.admin_modify.submit_row'. 13 | 14 | Manipulates the context going into that function by hiding all of the buttons 15 | in the submit row if the key `readonly` is set in the context. 16 | """ 17 | ctx = original_submit_row(context) 18 | 19 | if context.get('readonly', False): 20 | ctx.update({ 21 | 'show_delete_link': False, 22 | 'show_save_as_new': False, 23 | 'show_save_and_add_another': False, 24 | 'show_save_and_continue': False, 25 | 'show_save': False, 26 | }) 27 | 28 | return ctx 29 | -------------------------------------------------------------------------------- /config_models/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URLs for config_models. 3 | """ 4 | 5 | from django.urls import re_path 6 | from django.views.generic import TemplateView 7 | 8 | urlpatterns = [ 9 | re_path(r'', TemplateView.as_view(template_name="config_models/base.html")), 10 | ] 11 | -------------------------------------------------------------------------------- /config_models/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utilities for working with ConfigurationModels. 3 | """ 4 | 5 | 6 | from django.apps import apps 7 | from django.contrib.auth import get_user_model 8 | from rest_framework.parsers import JSONParser 9 | from rest_framework.serializers import ModelSerializer 10 | 11 | 12 | def get_serializer_class(configuration_model): 13 | """ Returns a ConfigurationModel serializer class for the supplied configuration_model. """ 14 | class AutoConfigModelSerializer(ModelSerializer): 15 | """Serializer class for configuration models.""" 16 | 17 | class Meta: 18 | """Meta information for AutoConfigModelSerializer.""" 19 | model = configuration_model 20 | fields = '__all__' 21 | 22 | def create(self, validated_data): 23 | if "changed_by_username" in self.context: 24 | model = get_user_model() 25 | validated_data['changed_by'] = model.objects.get(username=self.context["changed_by_username"]) 26 | return super().create(validated_data) 27 | 28 | return AutoConfigModelSerializer 29 | 30 | 31 | def deserialize_json(stream, username): 32 | """ 33 | Given a stream containing JSON, deserializers the JSON into ConfigurationModel instances. 34 | 35 | The stream is expected to be in the following format: 36 | { "model": "config_models.ExampleConfigurationModel", 37 | "data": 38 | [ 39 | { "enabled": True, 40 | "color": "black" 41 | ... 42 | }, 43 | { "enabled": False, 44 | "color": "yellow" 45 | ... 46 | }, 47 | ... 48 | ] 49 | } 50 | 51 | If the provided stream does not contain valid JSON for the ConfigurationModel specified, 52 | an Exception will be raised. 53 | 54 | Arguments: 55 | stream: The stream of JSON, as described above. 56 | username: The username of the user making the change. This must match an existing user. 57 | 58 | Returns: the number of created entries 59 | """ 60 | parsed_json = JSONParser().parse(stream) 61 | serializer_class = get_serializer_class(apps.get_model(parsed_json["model"])) 62 | list_serializer = serializer_class(data=parsed_json["data"], context={"changed_by_username": username}, many=True) 63 | if list_serializer.is_valid(): 64 | model_class = serializer_class.Meta.model 65 | for data in reversed(list_serializer.validated_data): 66 | if model_class.equal_to_current(data): 67 | list_serializer.validated_data.remove(data) 68 | 69 | entries_created = len(list_serializer.validated_data) 70 | list_serializer.save() 71 | return entries_created 72 | else: 73 | raise Exception(list_serializer.error_messages) 74 | -------------------------------------------------------------------------------- /config_models/views.py: -------------------------------------------------------------------------------- 1 | """ 2 | API view to allow manipulation of configuration models. 3 | """ 4 | 5 | 6 | from django.db import transaction 7 | from rest_framework.authentication import SessionAuthentication 8 | from rest_framework.generics import CreateAPIView, RetrieveAPIView 9 | from rest_framework.permissions import DjangoModelPermissions 10 | 11 | from config_models.utils import get_serializer_class 12 | 13 | 14 | class ReadableOnlyByAuthors(DjangoModelPermissions): 15 | """Only allow access by users with `add` permissions on the model.""" 16 | perms_map = DjangoModelPermissions.perms_map.copy() 17 | perms_map['GET'] = perms_map['OPTIONS'] = perms_map['HEAD'] = perms_map['POST'] 18 | 19 | 20 | class AtomicMixin: 21 | """Mixin to provide atomic transaction for as_view.""" 22 | @classmethod 23 | def create_atomic_wrapper(cls, wrapped_func): 24 | """Returns a wrapped function.""" 25 | def _create_atomic_wrapper(*args, **kwargs): 26 | """Actual wrapper.""" 27 | # When a view call fails due to a permissions error, it raises an exception. 28 | # An uncaught exception breaks the DB transaction for any following DB operations 29 | # unless it's wrapped in a atomic() decorator or context manager. 30 | with transaction.atomic(): 31 | return wrapped_func(*args, **kwargs) 32 | 33 | return _create_atomic_wrapper 34 | 35 | @classmethod 36 | def as_view(cls, **initkwargs): 37 | """Overrides as_view to add atomic transaction.""" 38 | view = super().as_view(**initkwargs) 39 | return cls.create_atomic_wrapper(view) 40 | 41 | 42 | class ConfigurationModelCurrentAPIView(AtomicMixin, CreateAPIView, RetrieveAPIView): 43 | """ 44 | This view allows an authenticated user with the appropriate model permissions 45 | to read and write the current configuration for the specified `model`. 46 | 47 | Like other APIViews, you can use this by using a url pattern similar to the following:: 48 | 49 | url(r'config/example_config$', ConfigurationModelCurrentAPIView.as_view(model=ExampleConfig)) 50 | """ 51 | authentication_classes = (SessionAuthentication,) 52 | permission_classes = (ReadableOnlyByAuthors,) 53 | model = None 54 | 55 | def get_queryset(self): 56 | return self.model.objects.all() 57 | 58 | def get_object(self): 59 | # Return the currently active configuration 60 | return self.model.current() 61 | 62 | def get_serializer_class(self): 63 | if self.serializer_class is None: 64 | self.serializer_class = get_serializer_class(self.model) 65 | 66 | return self.serializer_class 67 | 68 | def perform_create(self, serializer): 69 | # Set the requesting user as the one who is updating the configuration 70 | serializer.save(changed_by=self.request.user) 71 | -------------------------------------------------------------------------------- /db_keyword_overrides.yml: -------------------------------------------------------------------------------- 1 | # This file is used by the 'check_reserved_keywords' management command to allow specific field names to be overridden 2 | # when checking for conflicts with lists of restricted keywords used in various database/data warehouse tools. 3 | # For more information, see: https://github.com/edx/edx-django-release-util/release_util/management/commands/check_reserved_keywords.py 4 | # 5 | # overrides should be added in the following format: 6 | # - ModelName.field_name 7 | --- 8 | MYSQL: 9 | - ExampleKeyedConfig.left 10 | - ExampleKeyedConfig.right 11 | SNOWFLAKE: 12 | - ExampleKeyedConfig.left 13 | - ExampleKeyedConfig.right 14 | STITCH: 15 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " epub3 to make an epub3" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | @echo " dummy to check syntax errors of document sources" 51 | 52 | .PHONY: clean 53 | clean: 54 | rm -rf $(BUILDDIR)/* 55 | 56 | .PHONY: html 57 | html: 58 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 61 | 62 | .PHONY: dirhtml 63 | dirhtml: 64 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 65 | @echo 66 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 67 | 68 | .PHONY: singlehtml 69 | singlehtml: 70 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 71 | @echo 72 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 73 | 74 | .PHONY: pickle 75 | pickle: 76 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 77 | @echo 78 | @echo "Build finished; now you can process the pickle files." 79 | 80 | .PHONY: json 81 | json: 82 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 83 | @echo 84 | @echo "Build finished; now you can process the JSON files." 85 | 86 | .PHONY: htmlhelp 87 | htmlhelp: 88 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 89 | @echo 90 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 91 | ".hhp project file in $(BUILDDIR)/htmlhelp." 92 | 93 | .PHONY: qthelp 94 | qthelp: 95 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 96 | @echo 97 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 98 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 99 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/config_models.qhcp" 100 | @echo "To view the help file:" 101 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/config_models.qhc" 102 | 103 | .PHONY: applehelp 104 | applehelp: 105 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 106 | @echo 107 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 108 | @echo "N.B. You won't be able to view it unless you put it in" \ 109 | "~/Library/Documentation/Help or install it in your application" \ 110 | "bundle." 111 | 112 | .PHONY: devhelp 113 | devhelp: 114 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 115 | @echo 116 | @echo "Build finished." 117 | @echo "To view the help file:" 118 | @echo "# mkdir -p $$HOME/.local/share/devhelp/config_models" 119 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/config_models" 120 | @echo "# devhelp" 121 | 122 | .PHONY: epub 123 | epub: 124 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 125 | @echo 126 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 127 | 128 | .PHONY: epub3 129 | epub3: 130 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 131 | @echo 132 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 133 | 134 | .PHONY: latex 135 | latex: 136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 137 | @echo 138 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 139 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 140 | "(use \`make latexpdf' here to do that automatically)." 141 | 142 | .PHONY: latexpdf 143 | latexpdf: 144 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 145 | @echo "Running LaTeX files through pdflatex..." 146 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 147 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 148 | 149 | .PHONY: latexpdfja 150 | latexpdfja: 151 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 152 | @echo "Running LaTeX files through platex and dvipdfmx..." 153 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 154 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 155 | 156 | .PHONY: text 157 | text: 158 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 159 | @echo 160 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 161 | 162 | .PHONY: man 163 | man: 164 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 165 | @echo 166 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 167 | 168 | .PHONY: texinfo 169 | texinfo: 170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 171 | @echo 172 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 173 | @echo "Run \`make' in that directory to run these through makeinfo" \ 174 | "(use \`make info' here to do that automatically)." 175 | 176 | .PHONY: info 177 | info: 178 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 179 | @echo "Running Texinfo files through makeinfo..." 180 | make -C $(BUILDDIR)/texinfo info 181 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 182 | 183 | .PHONY: gettext 184 | gettext: 185 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 186 | @echo 187 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 188 | 189 | .PHONY: changes 190 | changes: 191 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 192 | @echo 193 | @echo "The overview file is in $(BUILDDIR)/changes." 194 | 195 | .PHONY: linkcheck 196 | linkcheck: 197 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 198 | @echo 199 | @echo "Link check complete; look for any errors in the above output " \ 200 | "or in $(BUILDDIR)/linkcheck/output.txt." 201 | 202 | .PHONY: doctest 203 | doctest: 204 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 205 | @echo "Testing of doctests in the sources finished, look at the " \ 206 | "results in $(BUILDDIR)/doctest/output.txt." 207 | 208 | .PHONY: coverage 209 | coverage: 210 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 211 | @echo "Testing of coverage in the sources finished, look at the " \ 212 | "results in $(BUILDDIR)/coverage/python.txt." 213 | 214 | .PHONY: xml 215 | xml: 216 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 217 | @echo 218 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 219 | 220 | .PHONY: pseudoxml 221 | pseudoxml: 222 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 223 | @echo 224 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 225 | 226 | .PHONY: dummy 227 | dummy: 228 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 229 | @echo 230 | @echo "Build finished. Dummy builder generates no files." 231 | -------------------------------------------------------------------------------- /docs/_static/theme_overrides.css: -------------------------------------------------------------------------------- 1 | /* override table width restrictions */ 2 | .wy-table-responsive table td, .wy-table-responsive table th { 3 | /* !important prevents the common CSS stylesheets from 4 | overriding this as on RTD they are loaded after this stylesheet */ 5 | white-space: normal !important; 6 | } 7 | 8 | .wy-table-responsive { 9 | overflow: visible !important; 10 | } 11 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGELOG.rst 2 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=invalid-name 2 | """ 3 | django_config_models documentation build configuration file. 4 | 5 | This file is execfile()d with the current directory set to its 6 | containing dir. 7 | 8 | Note that not all possible configuration values are present in this 9 | autogenerated file. 10 | 11 | All configuration values have a default; values that are commented out 12 | serve to show the default. 13 | """ 14 | 15 | import os 16 | import re 17 | import sys 18 | from datetime import datetime 19 | from subprocess import check_call 20 | 21 | # Configure Django for autodoc usage 22 | import django 23 | 24 | 25 | def get_version(*file_paths): 26 | """ 27 | Extract the version string from the file. 28 | Input: 29 | - file_paths: relative path fragments to file with 30 | version string 31 | """ 32 | filename = os.path.join(os.path.dirname(__file__), *file_paths) 33 | version_file = open(filename).read() 34 | version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M) 35 | if version_match: 36 | return version_match.group(1) 37 | raise RuntimeError('Unable to find version string.') 38 | 39 | 40 | # If extensions (or modules to document with autodoc) are in another directory, 41 | # add these directories to sys.path here. If the directory is relative to the 42 | # documentation root, use os.path.abspath to make it absolute, like shown here. 43 | # 44 | # import os 45 | # import sys 46 | # sys.path.insert(0, os.path.abspath('.')) 47 | REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 48 | sys.path.append(REPO_ROOT) 49 | sys.path.append(os.path.join(REPO_ROOT, 'mock_apps')) 50 | 51 | VERSION = get_version('../config_models', '__init__.py') 52 | 53 | if not os.environ.get('DJANGO_SETTINGS_MODULE'): 54 | os.environ['DJANGO_SETTINGS_MODULE'] = 'test_settings' 55 | 56 | django.setup() 57 | 58 | # -- General configuration ------------------------------------------------ 59 | 60 | # If your documentation needs a minimal Sphinx version, state it here. 61 | # 62 | # needs_sphinx = '1.0' 63 | 64 | # Add any Sphinx extension module names here, as strings. They can be 65 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 66 | # ones. 67 | extensions = [ 68 | 'sphinx.ext.autodoc', 69 | 'sphinx.ext.doctest', 70 | 'sphinx.ext.intersphinx', 71 | 'sphinx.ext.ifconfig', 72 | ] 73 | 74 | # A list of warning types to suppress arbitrary warning messages. 75 | suppress_warnings = [ 76 | 'image.nonlocal_uri', 77 | ] 78 | 79 | # Add any paths that contain templates here, relative to this directory. 80 | templates_path = ['_templates'] 81 | 82 | # The suffix(es) of source filenames. 83 | # You can specify multiple suffix as a list of string: 84 | # 85 | # source_suffix = ['.rst', '.md'] 86 | source_suffix = '.rst' 87 | 88 | # The encoding of source files. 89 | # 90 | # source_encoding = 'utf-8-sig' 91 | 92 | # The master toctree document. 93 | master_doc = 'index' 94 | 95 | # General information about the project. 96 | project = 'django_config_models' 97 | copyright = f'{datetime.now().year}, edX Inc.' # pylint: disable=redefined-builtin 98 | author = 'edX Inc.' 99 | 100 | # The version info for the project you're documenting, acts as replacement for 101 | # |version| and |release|, also used in various other places throughout the 102 | # built documents. 103 | # 104 | # The short X.Y version. 105 | version = VERSION 106 | # The full version, including alpha/beta/rc tags. 107 | release = VERSION 108 | 109 | # The language for content autogenerated by Sphinx. Refer to documentation 110 | # for a list of supported languages. 111 | # 112 | # This is also used if you do content translation via gettext catalogs. 113 | # Usually you set "language" from the command line for these cases. 114 | language = None 115 | 116 | # There are two options for replacing |today|: either, you set today to some 117 | # non-false value, then it is used: 118 | # 119 | # today = '' 120 | # 121 | # Else, today_fmt is used as the format for a strftime call. 122 | # 123 | # today_fmt = '%B %d, %Y' 124 | 125 | # List of patterns, relative to source directory, that match files and 126 | # directories to ignore when looking for source files. 127 | # This patterns also effect to html_static_path and html_extra_path 128 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 129 | 130 | # The reST default role (used for this markup: `text`) to use for all 131 | # documents. 132 | # 133 | # default_role = None 134 | 135 | # If true, '()' will be appended to :func: etc. cross-reference text. 136 | # 137 | # add_function_parentheses = True 138 | 139 | # If true, the current module name will be prepended to all description 140 | # unit titles (such as .. function::). 141 | # 142 | # add_module_names = True 143 | 144 | # If true, sectionauthor and moduleauthor directives will be shown in the 145 | # output. They are ignored by default. 146 | # 147 | # show_authors = False 148 | 149 | # The name of the Pygments (syntax highlighting) style to use. 150 | pygments_style = 'sphinx' 151 | 152 | # A list of ignored prefixes for module index sorting. 153 | # modindex_common_prefix = [] 154 | 155 | # If true, keep warnings as "system message" paragraphs in the built documents. 156 | # keep_warnings = False 157 | 158 | # If true, `todo` and `todoList` produce output, else they produce nothing. 159 | todo_include_todos = False 160 | 161 | 162 | # -- Options for HTML output ---------------------------------------------- 163 | 164 | # The theme to use for HTML and HTML Help pages. See the documentation for 165 | # a list of builtin themes. 166 | # 167 | html_theme = 'sphinx_book_theme' 168 | 169 | # Theme options are theme-specific and customize the look and feel of a theme 170 | # further. For a list of options available for each theme, see the 171 | # documentation. 172 | # 173 | html_theme_options = { 174 | "repository_url": "https://github.com/openedx/django-config-models", 175 | "repository_branch": "master", 176 | "path_to_docs": "docs/", 177 | "home_page_in_toc": True, 178 | "use_repository_button": True, 179 | "use_issues_button": True, 180 | "use_edit_page_button": True, 181 | # Please don't change unless you know what you're doing. 182 | "extra_footer": """ 183 | 184 | Creative Commons License 188 | 189 |
190 | These works by 191 | The Center for Reimagining Learning 197 | are licensed under a 198 | Creative Commons Attribution-ShareAlike 4.0 International License. 202 | """ 203 | } 204 | 205 | # Add any paths that contain custom themes here, relative to this directory. 206 | # html_theme_path = [] 207 | 208 | # The name for this set of Sphinx documents. 209 | # " v documentation" by default. 210 | # 211 | # html_title = 'django_config_models v0.1.0' 212 | 213 | # A shorter title for the navigation bar. Default is the same as html_title. 214 | # 215 | # html_short_title = None 216 | 217 | # The name of an image file (relative to this directory) to place at the top 218 | # of the sidebar. 219 | # 220 | html_logo = "https://logos.openedx.org/open-edx-logo-color.png" 221 | 222 | # The name of an image file (relative to this directory) to use as a favicon of 223 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 224 | # pixels large. 225 | # 226 | html_favicon = "https://logos.openedx.org/open-edx-favicon.ico" 227 | 228 | # Add any paths that contain custom static files (such as style sheets) here, 229 | # relative to this directory. They are copied after the builtin static files, 230 | # so a file named "default.css" will overwrite the builtin "default.css". 231 | html_static_path = ['_static'] 232 | 233 | # Add any extra paths that contain custom files (such as robots.txt or 234 | # .htaccess) here, relative to this directory. These files are copied 235 | # directly to the root of the documentation. 236 | # 237 | # html_extra_path = [] 238 | 239 | # If not None, a 'Last updated on:' timestamp is inserted at every page 240 | # bottom, using the given strftime format. 241 | # The empty string is equivalent to '%b %d, %Y'. 242 | # 243 | # html_last_updated_fmt = None 244 | 245 | # If true, SmartyPants will be used to convert quotes and dashes to 246 | # typographically correct entities. 247 | # 248 | # html_use_smartypants = True 249 | 250 | # Custom sidebar templates, maps document names to template names. 251 | # 252 | # html_sidebars = {} 253 | 254 | # Additional templates that should be rendered to pages, maps page names to 255 | # template names. 256 | # 257 | # html_additional_pages = {} 258 | 259 | # If false, no module index is generated. 260 | # 261 | # html_domain_indices = True 262 | 263 | # If false, no index is generated. 264 | # 265 | # html_use_index = True 266 | 267 | # If true, the index is split into individual pages for each letter. 268 | # 269 | # html_split_index = False 270 | 271 | # If true, links to the reST sources are added to the pages. 272 | # 273 | # html_show_sourcelink = True 274 | 275 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 276 | # 277 | # html_show_sphinx = True 278 | 279 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 280 | # 281 | # html_show_copyright = True 282 | 283 | # If true, an OpenSearch description file will be output, and all pages will 284 | # contain a tag referring to it. The value of this option must be the 285 | # base URL from which the finished HTML is served. 286 | # 287 | # html_use_opensearch = '' 288 | 289 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 290 | # html_file_suffix = None 291 | 292 | # Language to be used for generating the HTML full-text search index. 293 | # Sphinx supports the following languages: 294 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 295 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' 296 | # 297 | # html_search_language = 'en' 298 | 299 | # A dictionary with options for the search language support, empty by default. 300 | # 'ja' uses this config value. 301 | # 'zh' user can custom change `jieba` dictionary path. 302 | # 303 | # html_search_options = {'type': 'default'} 304 | 305 | # The name of a javascript file (relative to the configuration directory) that 306 | # implements a search results scorer. If empty, the default will be used. 307 | # 308 | # html_search_scorer = 'scorer.js' 309 | 310 | # Output file base name for HTML help builder. 311 | htmlhelp_basename = 'config_modelsdoc' 312 | 313 | # -- Options for LaTeX output --------------------------------------------- 314 | 315 | latex_elements = { 316 | # The paper size ('letterpaper' or 'a4paper'). 317 | # 318 | # 'papersize': 'letterpaper', 319 | 320 | # The font size ('10pt', '11pt' or '12pt'). 321 | # 322 | # 'pointsize': '10pt', 323 | 324 | # Additional stuff for the LaTeX preamble. 325 | # 326 | # 'preamble': '', 327 | 328 | # Latex figure (float) alignment 329 | # 330 | # 'figure_align': 'htbp', 331 | } 332 | 333 | # Grouping the document tree into LaTeX files. List of tuples 334 | # (source start file, target name, title, 335 | # author, documentclass [howto, manual, or own class]). 336 | latex_documents = [ 337 | (master_doc, 'config_models.tex', 'django_config_models Documentation', 338 | 'edX Doc Team', 'manual'), 339 | ] 340 | 341 | # The name of an image file (relative to this directory) to place at the top of 342 | # the title page. 343 | # 344 | # latex_logo = None 345 | 346 | # For "manual" documents, if this is true, then toplevel headings are parts, 347 | # not chapters. 348 | # 349 | # latex_use_parts = False 350 | 351 | # If true, show page references after internal links. 352 | # 353 | # latex_show_pagerefs = False 354 | 355 | # If true, show URL addresses after external links. 356 | # 357 | # latex_show_urls = False 358 | 359 | # Documents to append as an appendix to all manuals. 360 | # 361 | # latex_appendices = [] 362 | 363 | # It false, will not define \strong, \code, itleref, \crossref ... but only 364 | # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added 365 | # packages. 366 | # 367 | # latex_keep_old_macro_names = True 368 | 369 | # If false, no module index is generated. 370 | # 371 | # latex_domain_indices = True 372 | 373 | 374 | # -- Options for manual page output --------------------------------------- 375 | 376 | # One entry per manual page. List of tuples 377 | # (source start file, name, description, authors, manual section). 378 | man_pages = [ 379 | (master_doc, 'django_config_models', 'django_config_models Documentation', 380 | [author], 1) 381 | ] 382 | 383 | # If true, show URL addresses after external links. 384 | # 385 | # man_show_urls = False 386 | 387 | 388 | # -- Options for Texinfo output ------------------------------------------- 389 | 390 | # Grouping the document tree into Texinfo files. List of tuples 391 | # (source start file, target name, title, author, 392 | # dir menu entry, description, category) 393 | texinfo_documents = [ 394 | (master_doc, 'config_models', 'django_config_models Documentation', 395 | author, 'config_models', 'Configuration models for Django allowing config management with auditing.', 396 | 'Miscellaneous'), 397 | ] 398 | 399 | # Documents to append as an appendix to all manuals. 400 | # 401 | # texinfo_appendices = [] 402 | 403 | # If false, no module index is generated. 404 | # 405 | # texinfo_domain_indices = True 406 | 407 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 408 | # 409 | # texinfo_show_urls = 'footnote' 410 | 411 | # If true, do not generate a @detailmenu in the "Top" node's menu. 412 | # 413 | # texinfo_no_detailmenu = False 414 | 415 | 416 | # -- Options for Epub output ---------------------------------------------- 417 | 418 | # Bibliographic Dublin Core info. 419 | epub_title = project 420 | epub_author = author 421 | epub_publisher = author 422 | epub_copyright = copyright 423 | 424 | # The basename for the epub file. It defaults to the project name. 425 | # epub_basename = project 426 | 427 | # The HTML theme for the epub output. Since the default themes are not 428 | # optimized for small screen space, using the same theme for HTML and epub 429 | # output is usually not wise. This defaults to 'epub', a theme designed to save 430 | # visual space. 431 | # 432 | # epub_theme = 'epub' 433 | 434 | # The language of the text. It defaults to the language option 435 | # or 'en' if the language is not set. 436 | # 437 | # epub_language = '' 438 | 439 | # The scheme of the identifier. Typical schemes are ISBN or URL. 440 | # epub_scheme = '' 441 | 442 | # The unique identifier of the text. This can be a ISBN number 443 | # or the project homepage. 444 | # 445 | # epub_identifier = '' 446 | 447 | # A unique identification for the text. 448 | # 449 | # epub_uid = '' 450 | 451 | # A tuple containing the cover image and cover page html template filenames. 452 | # 453 | # epub_cover = () 454 | 455 | # A sequence of (type, uri, title) tuples for the guide element of content.opf. 456 | # 457 | # epub_guide = () 458 | 459 | # HTML files that should be inserted before the pages created by sphinx. 460 | # The format is a list of tuples containing the path and title. 461 | # 462 | # epub_pre_files = [] 463 | 464 | # HTML files that should be inserted after the pages created by sphinx. 465 | # The format is a list of tuples containing the path and title. 466 | # 467 | # epub_post_files = [] 468 | 469 | # A list of files that should not be packed into the epub file. 470 | epub_exclude_files = ['search.html'] 471 | 472 | # The depth of the table of contents in toc.ncx. 473 | # 474 | # epub_tocdepth = 3 475 | 476 | # Allow duplicate toc entries. 477 | # 478 | # epub_tocdup = True 479 | 480 | # Choose between 'default' and 'includehidden'. 481 | # 482 | # epub_tocscope = 'default' 483 | 484 | # Fix unsupported image types using the Pillow. 485 | # 486 | # epub_fix_images = False 487 | 488 | # Scale large images. 489 | # 490 | # epub_max_image_width = 0 491 | 492 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 493 | # 494 | # epub_show_urls = 'inline' 495 | 496 | # If false, no index is generated. 497 | # 498 | # epub_use_index = True 499 | 500 | 501 | # Example configuration for intersphinx: refer to the Python standard library. 502 | intersphinx_mapping = { 503 | 'python': ('https://docs.python.org/3.11', None), 504 | 'django': ('https://docs.djangoproject.com/en/4.2/', 'https://docs.djangoproject.com/en/4.2/_objects/'), 505 | 'model_utils': ('https://django-model-utils.readthedocs.io/en/latest/', None), 506 | } 507 | 508 | 509 | def on_init(app): # pylint: disable=unused-argument 510 | """ 511 | Run sphinx-apidoc after Sphinx initialization. 512 | Read the Docs won't run tox or custom shell commands, so we need this to 513 | avoid checking in the generated reStructuredText files. 514 | """ 515 | docs_path = os.path.abspath(os.path.dirname(__file__)) 516 | root_path = os.path.abspath(os.path.join(docs_path, '..')) 517 | apidoc_path = 'sphinx-apidoc' 518 | if hasattr(sys, 'real_prefix'): # Check to see if we are in a virtualenv 519 | # If we are, assemble the path manually 520 | bin_path = os.path.abspath(os.path.join(sys.prefix, 'bin')) 521 | apidoc_path = os.path.join(bin_path, apidoc_path) 522 | check_call([apidoc_path, '-o', docs_path, os.path.join(root_path, 'config_models')]) 523 | 524 | 525 | def setup(app): 526 | """Sphinx extension for applying some CSS overrides to the output theme.""" 527 | app.add_css_file('theme_overrides.css') 528 | event = 'builder-inited' 529 | app.connect(event, on_init) 530 | -------------------------------------------------------------------------------- /docs/getting_started.rst: -------------------------------------------------------------------------------- 1 | Getting Started 2 | =============== 3 | 4 | If you have not already done so, create/activate a `virtualenv`_. Unless otherwise stated, assume all terminal code 5 | below is executed within the virtualenv. 6 | 7 | .. _virtualenv: https://virtualenvwrapper.readthedocs.org/en/latest/ 8 | 9 | 10 | Install dependencies 11 | -------------------- 12 | Dependencies can be installed via the command below. 13 | 14 | .. code-block:: bash 15 | 16 | $ make requirements 17 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. django_config_models documentation master file, created by 2 | sphinx-quickstart on Thu Oct 06 13:40:01 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | django_config_models 7 | =============================== 8 | Configuration models for Django allowing config management with auditing. 9 | 10 | Contents: 11 | 12 | .. toctree:: 13 | :maxdepth: 2 14 | 15 | readme 16 | getting_started 17 | modules 18 | testing 19 | internationalization 20 | changelog 21 | 22 | 23 | Indices and tables 24 | ================== 25 | 26 | * :ref:`genindex` 27 | * :ref:`modindex` 28 | * :ref:`search` 29 | 30 | -------------------------------------------------------------------------------- /docs/internationalization.rst: -------------------------------------------------------------------------------- 1 | Internationalization 2 | ==================== 3 | All user-facing text content should be marked for translation. Even if this application is only run in English, our 4 | open source users may choose to use another language. Marking content for translation ensures our users have 5 | this choice. 6 | 7 | Follow the `internationalization coding guidelines`_ in the Open edX Developer's Guide when developing new features. 8 | 9 | .. _internationalization coding guidelines: https://docs.openedx.org/en/latest/developers/references/developer_guide/internationalization/i18n.html 10 | 11 | Updating Translations 12 | ~~~~~~~~~~~~~~~~~~~~~ 13 | This project uses `Transifex`_ to translate content. After new features are developed the translation source files 14 | should be pushed to Transifex. Our translation community will translate the content, after which we can retrieve the 15 | translations. 16 | 17 | .. _Transifex: https://www.transifex.com/ 18 | 19 | Pushing source translation files to Transifex requires access to the edx-platform. Request access from the Open Source 20 | Team if you will be pushing translation files. You should also `configure the Transifex client`_ if you have not done so 21 | already. 22 | 23 | .. _configure the Transifex client: http://docs.transifex.com/client/config/ 24 | 25 | The `make` targets listed below can be used to push or pull translations. 26 | 27 | .. list-table:: 28 | :widths: 25 75 29 | :header-rows: 1 30 | 31 | * - Target 32 | - Description 33 | * - pull_translations 34 | - Pull translations from Transifex 35 | * - push_translations 36 | - Push source translation files to Transifex 37 | 38 | Fake Translations 39 | ~~~~~~~~~~~~~~~~~ 40 | As you develop features it may be helpful to know which strings have been marked for translation, and which are not. 41 | Use the `fake_translations` make target for this purpose. This target will extract all strings marked for translation, 42 | generate fake translations in the Esperanto (eo) language directory, and compile the translations. 43 | 44 | You can trigger the display of the translations by setting your browser's language to Esperanto (eo), and navigating to 45 | a page on the site. Instead of plain English strings, you should see specially-accented English strings that look like this: 46 | 47 | Thé Fütüré øf Ønlïné Édüçätïøn Ⱡσяєм ι# Før änýøné, änýwhéré, änýtïmé Ⱡσяєм # 48 | 49 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. epub3 to make an epub3 31 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 32 | echo. text to make text files 33 | echo. man to make manual pages 34 | echo. texinfo to make Texinfo files 35 | echo. gettext to make PO message catalogs 36 | echo. changes to make an overview over all changed/added/deprecated items 37 | echo. xml to make Docutils-native XML files 38 | echo. pseudoxml to make pseudoxml-XML files for display purposes 39 | echo. linkcheck to check all external links for integrity 40 | echo. doctest to run all doctests embedded in the documentation if enabled 41 | echo. coverage to run coverage check of the documentation if enabled 42 | echo. dummy to check syntax errors of document sources 43 | goto end 44 | ) 45 | 46 | if "%1" == "clean" ( 47 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 48 | del /q /s %BUILDDIR%\* 49 | goto end 50 | ) 51 | 52 | 53 | REM Check if sphinx-build is available and fallback to Python version if any 54 | %SPHINXBUILD% 1>NUL 2>NUL 55 | if errorlevel 9009 goto sphinx_python 56 | goto sphinx_ok 57 | 58 | :sphinx_python 59 | 60 | set SPHINXBUILD=python -m sphinx.__init__ 61 | %SPHINXBUILD% 2> nul 62 | if errorlevel 9009 ( 63 | echo. 64 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 65 | echo.installed, then set the SPHINXBUILD environment variable to point 66 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 67 | echo.may add the Sphinx directory to PATH. 68 | echo. 69 | echo.If you don't have Sphinx installed, grab it from 70 | echo.http://sphinx-doc.org/ 71 | exit /b 1 72 | ) 73 | 74 | :sphinx_ok 75 | 76 | 77 | if "%1" == "html" ( 78 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 79 | if errorlevel 1 exit /b 1 80 | echo. 81 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 82 | goto end 83 | ) 84 | 85 | if "%1" == "dirhtml" ( 86 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 87 | if errorlevel 1 exit /b 1 88 | echo. 89 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 90 | goto end 91 | ) 92 | 93 | if "%1" == "singlehtml" ( 94 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 95 | if errorlevel 1 exit /b 1 96 | echo. 97 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 98 | goto end 99 | ) 100 | 101 | if "%1" == "pickle" ( 102 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 103 | if errorlevel 1 exit /b 1 104 | echo. 105 | echo.Build finished; now you can process the pickle files. 106 | goto end 107 | ) 108 | 109 | if "%1" == "json" ( 110 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 111 | if errorlevel 1 exit /b 1 112 | echo. 113 | echo.Build finished; now you can process the JSON files. 114 | goto end 115 | ) 116 | 117 | if "%1" == "htmlhelp" ( 118 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 119 | if errorlevel 1 exit /b 1 120 | echo. 121 | echo.Build finished; now you can run HTML Help Workshop with the ^ 122 | .hhp project file in %BUILDDIR%/htmlhelp. 123 | goto end 124 | ) 125 | 126 | if "%1" == "qthelp" ( 127 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 128 | if errorlevel 1 exit /b 1 129 | echo. 130 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 131 | .qhcp project file in %BUILDDIR%/qthelp, like this: 132 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\config_models.qhcp 133 | echo.To view the help file: 134 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\config_models.ghc 135 | goto end 136 | ) 137 | 138 | if "%1" == "devhelp" ( 139 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 140 | if errorlevel 1 exit /b 1 141 | echo. 142 | echo.Build finished. 143 | goto end 144 | ) 145 | 146 | if "%1" == "epub" ( 147 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 148 | if errorlevel 1 exit /b 1 149 | echo. 150 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 151 | goto end 152 | ) 153 | 154 | if "%1" == "epub3" ( 155 | %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 156 | if errorlevel 1 exit /b 1 157 | echo. 158 | echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. 159 | goto end 160 | ) 161 | 162 | if "%1" == "latex" ( 163 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 164 | if errorlevel 1 exit /b 1 165 | echo. 166 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdf" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "latexpdfja" ( 181 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 182 | cd %BUILDDIR%/latex 183 | make all-pdf-ja 184 | cd %~dp0 185 | echo. 186 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 187 | goto end 188 | ) 189 | 190 | if "%1" == "text" ( 191 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 192 | if errorlevel 1 exit /b 1 193 | echo. 194 | echo.Build finished. The text files are in %BUILDDIR%/text. 195 | goto end 196 | ) 197 | 198 | if "%1" == "man" ( 199 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 200 | if errorlevel 1 exit /b 1 201 | echo. 202 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 203 | goto end 204 | ) 205 | 206 | if "%1" == "texinfo" ( 207 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 208 | if errorlevel 1 exit /b 1 209 | echo. 210 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 211 | goto end 212 | ) 213 | 214 | if "%1" == "gettext" ( 215 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 216 | if errorlevel 1 exit /b 1 217 | echo. 218 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 219 | goto end 220 | ) 221 | 222 | if "%1" == "changes" ( 223 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 224 | if errorlevel 1 exit /b 1 225 | echo. 226 | echo.The overview file is in %BUILDDIR%/changes. 227 | goto end 228 | ) 229 | 230 | if "%1" == "linkcheck" ( 231 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 232 | if errorlevel 1 exit /b 1 233 | echo. 234 | echo.Link check complete; look for any errors in the above output ^ 235 | or in %BUILDDIR%/linkcheck/output.txt. 236 | goto end 237 | ) 238 | 239 | if "%1" == "doctest" ( 240 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 241 | if errorlevel 1 exit /b 1 242 | echo. 243 | echo.Testing of doctests in the sources finished, look at the ^ 244 | results in %BUILDDIR%/doctest/output.txt. 245 | goto end 246 | ) 247 | 248 | if "%1" == "coverage" ( 249 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 250 | if errorlevel 1 exit /b 1 251 | echo. 252 | echo.Testing of coverage in the sources finished, look at the ^ 253 | results in %BUILDDIR%/coverage/python.txt. 254 | goto end 255 | ) 256 | 257 | if "%1" == "xml" ( 258 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 259 | if errorlevel 1 exit /b 1 260 | echo. 261 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 262 | goto end 263 | ) 264 | 265 | if "%1" == "pseudoxml" ( 266 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 267 | if errorlevel 1 exit /b 1 268 | echo. 269 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 270 | goto end 271 | ) 272 | 273 | if "%1" == "dummy" ( 274 | %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy 275 | if errorlevel 1 exit /b 1 276 | echo. 277 | echo.Build finished. Dummy builder generates no files. 278 | goto end 279 | ) 280 | 281 | :end 282 | -------------------------------------------------------------------------------- /docs/readme.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | -------------------------------------------------------------------------------- /docs/testing.rst: -------------------------------------------------------------------------------- 1 | Testing 2 | ======= 3 | 4 | django_config_models has an assortment of test cases and code quality 5 | checks to catch potential problems during development. To run them all in the 6 | version of Python you chose for your virtualenv: 7 | 8 | .. code-block:: bash 9 | 10 | $ make validate 11 | 12 | To run just the unit tests: 13 | 14 | .. code-block:: bash 15 | 16 | $ make test 17 | 18 | To run just the code quality checks: 19 | 20 | .. code-block:: bash 21 | 22 | $ make quality 23 | 24 | To run the unit tests under every supported Python version and the code 25 | quality checks: 26 | 27 | .. code-block:: bash 28 | 29 | $ make test-all 30 | 31 | To generate and open an HTML report of how much of the code is covered by 32 | test cases: 33 | 34 | .. code-block:: bash 35 | 36 | $ make coverage -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Django administration utility. 4 | """ 5 | 6 | 7 | 8 | import os 9 | import sys 10 | 11 | PWD = os.path.abspath(os.path.dirname(__file__)) 12 | 13 | if __name__ == '__main__': 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'test_settings') 15 | sys.path.append('./mock_apps') 16 | sys.path.append(PWD) 17 | try: 18 | from django.core.management import \ 19 | execute_from_command_line # pylint: disable=wrong-import-position 20 | except ImportError: 21 | # The above import may fail for some other reason. Ensure that the 22 | # issue is really that Django is missing to avoid masking other 23 | # exceptions on Python 2. 24 | try: 25 | import django # pylint: disable=unused-import, wrong-import-position 26 | except ImportError: 27 | raise ImportError( 28 | "Couldn't import Django. Are you sure it's installed and " 29 | "available on your PYTHONPATH environment variable? Did you " 30 | "forget to activate a virtual environment?" 31 | ) 32 | raise 33 | execute_from_command_line(sys.argv) 34 | -------------------------------------------------------------------------------- /mock_apps/example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openedx/django-config-models/9097f36b0c54fef87ff9ae35f426e44ded6437ed/mock_apps/example/__init__.py -------------------------------------------------------------------------------- /mock_apps/example/apps.py: -------------------------------------------------------------------------------- 1 | """ 2 | config_models Django application initialization. 3 | """ 4 | 5 | 6 | 7 | from django.apps import AppConfig 8 | 9 | 10 | class ExampleConfig(AppConfig): 11 | """ 12 | Configuration for the config_models Django application. 13 | """ 14 | 15 | name = 'example' 16 | -------------------------------------------------------------------------------- /mock_apps/example/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.9 on 2020-01-07 09:27 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='ManyToManyExampleConfig', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')), 22 | ('enabled', models.BooleanField(default=False, verbose_name='Enabled')), 23 | ('string_field', models.TextField()), 24 | ('changed_by', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='Changed by')), 25 | ('many_user_field', models.ManyToManyField(related_name='topic_many_user_field', to=settings.AUTH_USER_MODEL)), 26 | ], 27 | options={ 28 | 'ordering': ('-change_date',), 29 | 'abstract': False, 30 | }, 31 | ), 32 | migrations.CreateModel( 33 | name='ExampleKeyedConfig', 34 | fields=[ 35 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 36 | ('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')), 37 | ('enabled', models.BooleanField(default=False, verbose_name='Enabled')), 38 | ('left', models.CharField(max_length=30)), 39 | ('right', models.CharField(max_length=30)), 40 | ('string_field', models.TextField()), 41 | ('int_field', models.IntegerField(default=10)), 42 | ('changed_by', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='Changed by')), 43 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='test_user', to=settings.AUTH_USER_MODEL)), 44 | ], 45 | options={ 46 | 'ordering': ('-change_date',), 47 | 'abstract': False, 48 | }, 49 | ), 50 | migrations.CreateModel( 51 | name='ExampleConfig', 52 | fields=[ 53 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 54 | ('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')), 55 | ('enabled', models.BooleanField(default=False, verbose_name='Enabled')), 56 | ('string_field', models.TextField()), 57 | ('int_field', models.IntegerField(default=10)), 58 | ('changed_by', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='Changed by')), 59 | ], 60 | options={ 61 | 'ordering': ('-change_date',), 62 | 'abstract': False, 63 | }, 64 | ), 65 | ] 66 | -------------------------------------------------------------------------------- /mock_apps/example/migrations/0002_exampledecoratorconfig_exampledeserializeconfig.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.9 on 2020-01-07 09:42 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('example', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='ExampleDeserializeConfig', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')), 21 | ('enabled', models.BooleanField(default=False, verbose_name='Enabled')), 22 | ('name', models.TextField()), 23 | ('int_field', models.IntegerField(default=10)), 24 | ('changed_by', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='Changed by')), 25 | ], 26 | options={ 27 | 'ordering': ('-change_date',), 28 | 'abstract': False, 29 | }, 30 | ), 31 | migrations.CreateModel( 32 | name='ExampleDecoratorConfig', 33 | fields=[ 34 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 35 | ('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')), 36 | ('enabled', models.BooleanField(default=False, verbose_name='Enabled')), 37 | ('changed_by', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='Changed by')), 38 | ], 39 | options={ 40 | 'ordering': ('-change_date',), 41 | 'abstract': False, 42 | }, 43 | ), 44 | ] 45 | -------------------------------------------------------------------------------- /mock_apps/example/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openedx/django-config-models/9097f36b0c54fef87ff9ae35f426e44ded6437ed/mock_apps/example/migrations/__init__.py -------------------------------------------------------------------------------- /mock_apps/example/models.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.auth.models import User 3 | from django.db import models 4 | 5 | from config_models.models import ConfigurationModel 6 | 7 | 8 | class ExampleConfig(ConfigurationModel): 9 | """ 10 | Test model for testing ``ConfigurationModels``. 11 | """ 12 | cache_timeout = 300 13 | 14 | string_field = models.TextField() 15 | int_field = models.IntegerField(default=10) 16 | 17 | def __str__(self): 18 | return "ExampleConfig(enabled={}, string_field={}, int_field={})".format( 19 | self.enabled, self.string_field, self.int_field 20 | ) 21 | 22 | 23 | class ManyToManyExampleConfig(ConfigurationModel): 24 | """ 25 | Test model configuration with a many-to-many field. 26 | """ 27 | cache_timeout = 300 28 | 29 | string_field = models.TextField() 30 | many_user_field = models.ManyToManyField(User, related_name='topic_many_user_field') 31 | 32 | def __str__(self): 33 | return f"ManyToManyExampleConfig(enabled={self.enabled}, string_field={self.string_field})" 34 | 35 | class ExampleKeyedConfig(ConfigurationModel): 36 | """ 37 | Test model for testing ``ConfigurationModels`` with keyed configuration. 38 | 39 | Does not inherit from ExampleConfig due to how Django handles model inheritance. 40 | """ 41 | cache_timeout = 300 42 | 43 | KEY_FIELDS = ('left', 'right', 'user') 44 | 45 | left = models.CharField(max_length=30) 46 | right = models.CharField(max_length=30) 47 | user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='test_user') 48 | 49 | string_field = models.TextField() 50 | int_field = models.IntegerField(default=10) 51 | 52 | def __str__(self): 53 | return "ExampleKeyedConfig(enabled={}, left={}, right={}, user={}, string_field={}, int_field={})".format( 54 | self.enabled, self.left, self.right, self.user, self.string_field, self.int_field 55 | ) 56 | 57 | 58 | class ExampleDecoratorConfig(ConfigurationModel): 59 | """ 60 | Test model for testing the require_config decorator 61 | """ 62 | def __str__(self): 63 | return f"ExampleDecoratorConfig(enabled={self.enabled})" 64 | 65 | 66 | class ExampleDeserializeConfig(ConfigurationModel): 67 | """ 68 | Test model for testing deserialization of ``ConfigurationModels`` with keyed configuration. 69 | """ 70 | KEY_FIELDS = ('name',) 71 | 72 | name = models.TextField() 73 | int_field = models.IntegerField(default=10) 74 | 75 | def __str__(self): 76 | return "ExampleDeserializeConfig(enabled={}, name={}, int_field={})".format( 77 | self.enabled, self.name, self.int_field 78 | ) 79 | -------------------------------------------------------------------------------- /openedx.yaml: -------------------------------------------------------------------------------- 1 | nick: config_models 2 | tags: 3 | - tools 4 | - library 5 | oeps: 6 | oep-2: true 7 | oep-3: 8 | state: false 9 | reason: TODO - Implement for this application if appropriate 10 | oep-5: 11 | state: false 12 | reason: TODO - Implement for this application if appropriate 13 | oep-7: true 14 | oep-18: true 15 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | # *************************** 2 | # ** DO NOT EDIT THIS FILE ** 3 | # *************************** 4 | # 5 | # This file was generated by edx-lint: https://github.com/openedx/edx-lint 6 | # 7 | # If you want to change this file, you have two choices, depending on whether 8 | # you want to make a local change that applies only to this repo, or whether 9 | # you want to make a central change that applies to all repos using edx-lint. 10 | # 11 | # Note: If your pylintrc file is simply out-of-date relative to the latest 12 | # pylintrc in edx-lint, ensure you have the latest edx-lint installed 13 | # and then follow the steps for a "LOCAL CHANGE". 14 | # 15 | # LOCAL CHANGE: 16 | # 17 | # 1. Edit the local pylintrc_tweaks file to add changes just to this 18 | # repo's file. 19 | # 20 | # 2. Run: 21 | # 22 | # $ edx_lint write pylintrc 23 | # 24 | # 3. This will modify the local file. Submit a pull request to get it 25 | # checked in so that others will benefit. 26 | # 27 | # 28 | # CENTRAL CHANGE: 29 | # 30 | # 1. Edit the pylintrc file in the edx-lint repo at 31 | # https://github.com/openedx/edx-lint/blob/master/edx_lint/files/pylintrc 32 | # 33 | # 2. install the updated version of edx-lint (in edx-lint): 34 | # 35 | # $ pip install . 36 | # 37 | # 3. Run (in edx-lint): 38 | # 39 | # $ edx_lint write pylintrc 40 | # 41 | # 4. Make a new version of edx_lint, submit and review a pull request with the 42 | # pylintrc update, and after merging, update the edx-lint version and 43 | # publish the new version. 44 | # 45 | # 5. In your local repo, install the newer version of edx-lint. 46 | # 47 | # 6. Run: 48 | # 49 | # $ edx_lint write pylintrc 50 | # 51 | # 7. This will modify the local file. Submit a pull request to get it 52 | # checked in so that others will benefit. 53 | # 54 | # 55 | # 56 | # 57 | # 58 | # STAY AWAY FROM THIS FILE! 59 | # 60 | # 61 | # 62 | # 63 | # 64 | # SERIOUSLY. 65 | # 66 | # ------------------------------ 67 | # Generated by edx-lint version: 5.3.1 68 | # ------------------------------ 69 | [MASTER] 70 | ignore = migrations 71 | persistent = yes 72 | load-plugins = edx_lint.pylint,pylint_django,pylint_celery 73 | 74 | [MESSAGES CONTROL] 75 | enable = 76 | blacklisted-name, 77 | line-too-long, 78 | 79 | abstract-class-instantiated, 80 | abstract-method, 81 | access-member-before-definition, 82 | anomalous-backslash-in-string, 83 | anomalous-unicode-escape-in-string, 84 | arguments-differ, 85 | assert-on-tuple, 86 | assigning-non-slot, 87 | assignment-from-no-return, 88 | assignment-from-none, 89 | attribute-defined-outside-init, 90 | bad-except-order, 91 | bad-format-character, 92 | bad-format-string-key, 93 | bad-format-string, 94 | bad-open-mode, 95 | bad-reversed-sequence, 96 | bad-staticmethod-argument, 97 | bad-str-strip-call, 98 | bad-super-call, 99 | binary-op-exception, 100 | boolean-datetime, 101 | catching-non-exception, 102 | cell-var-from-loop, 103 | confusing-with-statement, 104 | continue-in-finally, 105 | dangerous-default-value, 106 | duplicate-argument-name, 107 | duplicate-bases, 108 | duplicate-except, 109 | duplicate-key, 110 | expression-not-assigned, 111 | format-combined-specification, 112 | format-needs-mapping, 113 | function-redefined, 114 | global-variable-undefined, 115 | import-error, 116 | import-self, 117 | inconsistent-mro, 118 | inherit-non-class, 119 | init-is-generator, 120 | invalid-all-object, 121 | invalid-format-index, 122 | invalid-length-returned, 123 | invalid-sequence-index, 124 | invalid-slice-index, 125 | invalid-slots-object, 126 | invalid-slots, 127 | invalid-unary-operand-type, 128 | logging-too-few-args, 129 | logging-too-many-args, 130 | logging-unsupported-format, 131 | lost-exception, 132 | method-hidden, 133 | misplaced-bare-raise, 134 | misplaced-future, 135 | missing-format-argument-key, 136 | missing-format-attribute, 137 | missing-format-string-key, 138 | no-member, 139 | no-method-argument, 140 | no-name-in-module, 141 | no-self-argument, 142 | no-value-for-parameter, 143 | non-iterator-returned, 144 | non-parent-method-called, 145 | nonexistent-operator, 146 | not-a-mapping, 147 | not-an-iterable, 148 | not-callable, 149 | not-context-manager, 150 | not-in-loop, 151 | pointless-statement, 152 | pointless-string-statement, 153 | raising-bad-type, 154 | raising-non-exception, 155 | redefined-builtin, 156 | redefined-outer-name, 157 | redundant-keyword-arg, 158 | repeated-keyword, 159 | return-arg-in-generator, 160 | return-in-init, 161 | return-outside-function, 162 | signature-differs, 163 | super-init-not-called, 164 | super-method-not-called, 165 | syntax-error, 166 | test-inherits-tests, 167 | too-few-format-args, 168 | too-many-format-args, 169 | too-many-function-args, 170 | translation-of-non-string, 171 | truncated-format-string, 172 | undefined-all-variable, 173 | undefined-loop-variable, 174 | undefined-variable, 175 | unexpected-keyword-arg, 176 | unexpected-special-method-signature, 177 | unpacking-non-sequence, 178 | unreachable, 179 | unsubscriptable-object, 180 | unsupported-binary-operation, 181 | unsupported-membership-test, 182 | unused-format-string-argument, 183 | unused-format-string-key, 184 | used-before-assignment, 185 | using-constant-test, 186 | yield-outside-function, 187 | 188 | astroid-error, 189 | fatal, 190 | method-check-failed, 191 | parse-error, 192 | raw-checker-failed, 193 | 194 | empty-docstring, 195 | invalid-characters-in-docstring, 196 | missing-docstring, 197 | wrong-spelling-in-comment, 198 | wrong-spelling-in-docstring, 199 | 200 | unused-argument, 201 | unused-import, 202 | unused-variable, 203 | 204 | eval-used, 205 | exec-used, 206 | 207 | bad-classmethod-argument, 208 | bad-mcs-classmethod-argument, 209 | bad-mcs-method-argument, 210 | bare-except, 211 | broad-except, 212 | consider-iterating-dictionary, 213 | consider-using-enumerate, 214 | global-at-module-level, 215 | global-variable-not-assigned, 216 | literal-used-as-attribute, 217 | logging-format-interpolation, 218 | logging-not-lazy, 219 | multiple-imports, 220 | multiple-statements, 221 | no-classmethod-decorator, 222 | no-staticmethod-decorator, 223 | protected-access, 224 | redundant-unittest-assert, 225 | reimported, 226 | simplifiable-if-statement, 227 | simplifiable-range, 228 | singleton-comparison, 229 | superfluous-parens, 230 | unidiomatic-typecheck, 231 | unnecessary-lambda, 232 | unnecessary-pass, 233 | unnecessary-semicolon, 234 | unneeded-not, 235 | useless-else-on-loop, 236 | wrong-assert-type, 237 | 238 | deprecated-method, 239 | deprecated-module, 240 | 241 | too-many-boolean-expressions, 242 | too-many-nested-blocks, 243 | too-many-statements, 244 | 245 | wildcard-import, 246 | wrong-import-order, 247 | wrong-import-position, 248 | 249 | missing-final-newline, 250 | mixed-line-endings, 251 | trailing-newlines, 252 | trailing-whitespace, 253 | unexpected-line-ending-format, 254 | 255 | bad-inline-option, 256 | bad-option-value, 257 | deprecated-pragma, 258 | unrecognized-inline-option, 259 | useless-suppression, 260 | disable = 261 | bad-indentation, 262 | broad-exception-raised, 263 | consider-using-f-string, 264 | duplicate-code, 265 | file-ignored, 266 | fixme, 267 | global-statement, 268 | invalid-name, 269 | locally-disabled, 270 | no-else-return, 271 | suppressed-message, 272 | too-few-public-methods, 273 | too-many-ancestors, 274 | too-many-arguments, 275 | too-many-branches, 276 | too-many-instance-attributes, 277 | too-many-lines, 278 | too-many-locals, 279 | too-many-public-methods, 280 | too-many-return-statements, 281 | ungrouped-imports, 282 | unspecified-encoding, 283 | unused-wildcard-import, 284 | use-maxsplit-arg, 285 | 286 | feature-toggle-needs-doc, 287 | illegal-waffle-usage, 288 | 289 | logging-fstring-interpolation, 290 | missing-timeout, 291 | django-not-configured 292 | 293 | [REPORTS] 294 | output-format = text 295 | reports = no 296 | score = no 297 | 298 | [BASIC] 299 | module-rgx = (([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 300 | const-rgx = (([A-Z_][A-Z0-9_]*)|(__.*__)|log|urlpatterns)$ 301 | class-rgx = [A-Z_][a-zA-Z0-9]+$ 302 | function-rgx = ([a-z_][a-z0-9_]{2,40}|test_[a-z0-9_]+)$ 303 | method-rgx = ([a-z_][a-z0-9_]{2,40}|setUp|set[Uu]pClass|tearDown|tear[Dd]ownClass|assert[A-Z]\w*|maxDiff|test_[a-z0-9_]+)$ 304 | attr-rgx = [a-z_][a-z0-9_]{2,30}$ 305 | argument-rgx = [a-z_][a-z0-9_]{2,30}$ 306 | variable-rgx = [a-z_][a-z0-9_]{2,30}$ 307 | class-attribute-rgx = ([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 308 | inlinevar-rgx = [A-Za-z_][A-Za-z0-9_]*$ 309 | good-names = f,i,j,k,db,ex,Run,_,__ 310 | bad-names = foo,bar,baz,toto,tutu,tata 311 | no-docstring-rgx = __.*__$|test_.+|setUp$|setUpClass$|tearDown$|tearDownClass$|Meta$ 312 | docstring-min-length = 5 313 | 314 | [FORMAT] 315 | max-line-length = 120 316 | ignore-long-lines = ^\s*(# )?((?)|(\.\. \w+: .*))$ 317 | single-line-if-stmt = no 318 | max-module-lines = 1000 319 | indent-string = ' ' 320 | 321 | [MISCELLANEOUS] 322 | notes = FIXME,XXX,TODO 323 | 324 | [SIMILARITIES] 325 | min-similarity-lines = 4 326 | ignore-comments = yes 327 | ignore-docstrings = yes 328 | ignore-imports = no 329 | 330 | [TYPECHECK] 331 | ignore-mixin-members = yes 332 | ignored-classes = SQLObject 333 | unsafe-load-any-extension = yes 334 | generated-members = 335 | REQUEST, 336 | acl_users, 337 | aq_parent, 338 | objects, 339 | DoesNotExist, 340 | can_read, 341 | can_write, 342 | get_url, 343 | size, 344 | content, 345 | status_code, 346 | create, 347 | build, 348 | fields, 349 | tag, 350 | org, 351 | course, 352 | category, 353 | name, 354 | revision, 355 | _meta, 356 | 357 | [VARIABLES] 358 | init-import = no 359 | dummy-variables-rgx = _|dummy|unused|.*_unused 360 | additional-builtins = 361 | 362 | [CLASSES] 363 | defining-attr-methods = __init__,__new__,setUp 364 | valid-classmethod-first-arg = cls 365 | valid-metaclass-classmethod-first-arg = mcs 366 | 367 | [DESIGN] 368 | max-args = 5 369 | ignored-argument-names = _.* 370 | max-locals = 15 371 | max-returns = 6 372 | max-branches = 12 373 | max-statements = 50 374 | max-parents = 7 375 | max-attributes = 7 376 | min-public-methods = 2 377 | max-public-methods = 20 378 | 379 | [IMPORTS] 380 | deprecated-modules = regsub,TERMIOS,Bastion,rexec 381 | import-graph = 382 | ext-import-graph = 383 | int-import-graph = 384 | 385 | [EXCEPTIONS] 386 | overgeneral-exceptions = builtins.Exception 387 | 388 | # eeb3a09ffe4ad7b225e28401d7b7a458d6d54b77 389 | -------------------------------------------------------------------------------- /pylintrc_tweaks: -------------------------------------------------------------------------------- 1 | # pylintrc tweaks for use with edx_lint. 2 | [MASTER] 3 | ignore = migrations 4 | load-plugins = edx_lint.pylint,pylint_django,pylint_celery 5 | 6 | [MESSAGES CONTROL] 7 | disable+ = 8 | missing-timeout, 9 | django-not-configured 10 | -------------------------------------------------------------------------------- /requirements/base.in: -------------------------------------------------------------------------------- 1 | # Core requirements for using this application 2 | -c constraints.txt 3 | 4 | Django # Web application framework 5 | djangorestframework>=3.6 6 | edx-django-utils 7 | -------------------------------------------------------------------------------- /requirements/base.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # make upgrade 6 | # 7 | asgiref==3.8.1 8 | # via django 9 | cffi==1.17.1 10 | # via pynacl 11 | click==8.2.1 12 | # via edx-django-utils 13 | django==4.2.21 14 | # via 15 | # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt 16 | # -r requirements/base.in 17 | # django-crum 18 | # django-waffle 19 | # djangorestframework 20 | # edx-django-utils 21 | django-crum==0.7.9 22 | # via edx-django-utils 23 | django-waffle==4.2.0 24 | # via edx-django-utils 25 | djangorestframework==3.16.0 26 | # via -r requirements/base.in 27 | edx-django-utils==8.0.0 28 | # via -r requirements/base.in 29 | pbr==6.1.1 30 | # via stevedore 31 | psutil==7.0.0 32 | # via edx-django-utils 33 | pycparser==2.22 34 | # via cffi 35 | pynacl==1.5.0 36 | # via edx-django-utils 37 | sqlparse==0.5.3 38 | # via django 39 | stevedore==5.4.1 40 | # via edx-django-utils 41 | 42 | # The following packages are considered to be unsafe in a requirements file: 43 | # setuptools 44 | -------------------------------------------------------------------------------- /requirements/ci.in: -------------------------------------------------------------------------------- 1 | # Requirements for running tests in Travis 2 | -c constraints.txt 3 | 4 | tox # Virtualenv management for tests 5 | -------------------------------------------------------------------------------- /requirements/ci.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # make upgrade 6 | # 7 | cachetools==6.0.0 8 | # via tox 9 | chardet==5.2.0 10 | # via tox 11 | colorama==0.4.6 12 | # via tox 13 | distlib==0.3.9 14 | # via virtualenv 15 | filelock==3.18.0 16 | # via 17 | # tox 18 | # virtualenv 19 | packaging==25.0 20 | # via 21 | # pyproject-api 22 | # tox 23 | platformdirs==4.3.8 24 | # via 25 | # tox 26 | # virtualenv 27 | pluggy==1.6.0 28 | # via tox 29 | pyproject-api==1.9.1 30 | # via tox 31 | tox==4.26.0 32 | # via -r requirements/ci.in 33 | virtualenv==20.31.2 34 | # via tox 35 | -------------------------------------------------------------------------------- /requirements/constraints.txt: -------------------------------------------------------------------------------- 1 | # Version constraints for pip installation. 2 | # 3 | # This file doesn't install any packages. It specifies version constraints 4 | # that will be applied if a package is needed. 5 | # 6 | # When pinning something here, please provide an explanation of why. Ideally, 7 | # link to other information that will help people in the future to remove the 8 | # pin when possible. Writing an issue against the offending project and 9 | # linking to it here is good. 10 | 11 | # This file contains all common constraints for edx-repos 12 | -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt 13 | 14 | # Temporary to Support the python 3.11 Upgrade 15 | backports.zoneinfo;python_version<"3.9" # Newer versions have zoneinfo available in the standard library 16 | -------------------------------------------------------------------------------- /requirements/dev.in: -------------------------------------------------------------------------------- 1 | # Additional requirements for development of this application 2 | -c constraints.txt 3 | 4 | -r pip-tools.txt # Dependencies for requirements file management 5 | -r quality.txt # All dependencies needed to run and test django-config-models 6 | -r ci.txt # tox and related dependencies 7 | 8 | edx-i18n-tools # For i18n_tool dummy 9 | -------------------------------------------------------------------------------- /requirements/dev.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # make upgrade 6 | # 7 | asgiref==3.8.1 8 | # via 9 | # -r requirements/quality.txt 10 | # django 11 | astroid==3.3.10 12 | # via 13 | # -r requirements/quality.txt 14 | # pylint 15 | # pylint-celery 16 | build==1.2.2.post1 17 | # via 18 | # -r requirements/pip-tools.txt 19 | # pip-tools 20 | cachetools==6.0.0 21 | # via 22 | # -r requirements/ci.txt 23 | # tox 24 | cffi==1.17.1 25 | # via 26 | # -r requirements/quality.txt 27 | # pynacl 28 | chardet==5.2.0 29 | # via 30 | # -r requirements/ci.txt 31 | # tox 32 | click==8.2.1 33 | # via 34 | # -r requirements/pip-tools.txt 35 | # -r requirements/quality.txt 36 | # click-log 37 | # code-annotations 38 | # edx-django-utils 39 | # edx-lint 40 | # pip-tools 41 | click-log==0.4.0 42 | # via 43 | # -r requirements/quality.txt 44 | # edx-lint 45 | code-annotations==2.3.0 46 | # via 47 | # -r requirements/quality.txt 48 | # edx-lint 49 | colorama==0.4.6 50 | # via 51 | # -r requirements/ci.txt 52 | # tox 53 | coverage[toml]==7.8.2 54 | # via 55 | # -r requirements/quality.txt 56 | # pytest-cov 57 | ddt==1.7.2 58 | # via -r requirements/quality.txt 59 | dill==0.4.0 60 | # via 61 | # -r requirements/quality.txt 62 | # pylint 63 | distlib==0.3.9 64 | # via 65 | # -r requirements/ci.txt 66 | # virtualenv 67 | django==4.2.21 68 | # via 69 | # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt 70 | # -r requirements/quality.txt 71 | # django-crum 72 | # django-waffle 73 | # djangorestframework 74 | # edx-django-release-util 75 | # edx-django-utils 76 | # edx-i18n-tools 77 | django-crum==0.7.9 78 | # via 79 | # -r requirements/quality.txt 80 | # edx-django-utils 81 | django-waffle==4.2.0 82 | # via 83 | # -r requirements/quality.txt 84 | # edx-django-utils 85 | djangorestframework==3.16.0 86 | # via -r requirements/quality.txt 87 | edx-django-release-util==1.5.0 88 | # via -r requirements/quality.txt 89 | edx-django-utils==8.0.0 90 | # via -r requirements/quality.txt 91 | edx-i18n-tools==1.9.0 92 | # via -r requirements/dev.in 93 | edx-lint==5.6.0 94 | # via -r requirements/quality.txt 95 | filelock==3.18.0 96 | # via 97 | # -r requirements/ci.txt 98 | # tox 99 | # virtualenv 100 | freezegun==1.5.2 101 | # via -r requirements/quality.txt 102 | iniconfig==2.1.0 103 | # via 104 | # -r requirements/quality.txt 105 | # pytest 106 | isort==6.0.1 107 | # via 108 | # -r requirements/quality.txt 109 | # pylint 110 | jinja2==3.1.6 111 | # via 112 | # -r requirements/quality.txt 113 | # code-annotations 114 | lxml[html-clean,html_clean]==5.4.0 115 | # via 116 | # edx-i18n-tools 117 | # lxml-html-clean 118 | lxml-html-clean==0.4.2 119 | # via lxml 120 | markupsafe==3.0.2 121 | # via 122 | # -r requirements/quality.txt 123 | # jinja2 124 | mccabe==0.7.0 125 | # via 126 | # -r requirements/quality.txt 127 | # pylint 128 | packaging==25.0 129 | # via 130 | # -r requirements/ci.txt 131 | # -r requirements/pip-tools.txt 132 | # -r requirements/quality.txt 133 | # build 134 | # pyproject-api 135 | # pytest 136 | # tox 137 | path==16.16.0 138 | # via edx-i18n-tools 139 | pbr==6.1.1 140 | # via 141 | # -r requirements/quality.txt 142 | # stevedore 143 | pip-tools==7.4.1 144 | # via -r requirements/pip-tools.txt 145 | platformdirs==4.3.8 146 | # via 147 | # -r requirements/ci.txt 148 | # -r requirements/quality.txt 149 | # pylint 150 | # tox 151 | # virtualenv 152 | pluggy==1.6.0 153 | # via 154 | # -r requirements/ci.txt 155 | # -r requirements/quality.txt 156 | # pytest 157 | # tox 158 | polib==1.2.0 159 | # via edx-i18n-tools 160 | psutil==7.0.0 161 | # via 162 | # -r requirements/quality.txt 163 | # edx-django-utils 164 | pycodestyle==2.13.0 165 | # via -r requirements/quality.txt 166 | pycparser==2.22 167 | # via 168 | # -r requirements/quality.txt 169 | # cffi 170 | pydocstyle==6.3.0 171 | # via -r requirements/quality.txt 172 | pylint==3.3.7 173 | # via 174 | # -r requirements/quality.txt 175 | # edx-lint 176 | # pylint-celery 177 | # pylint-django 178 | # pylint-plugin-utils 179 | pylint-celery==0.3 180 | # via 181 | # -r requirements/quality.txt 182 | # edx-lint 183 | pylint-django==2.6.1 184 | # via 185 | # -r requirements/quality.txt 186 | # edx-lint 187 | pylint-plugin-utils==0.8.2 188 | # via 189 | # -r requirements/quality.txt 190 | # pylint-celery 191 | # pylint-django 192 | pynacl==1.5.0 193 | # via 194 | # -r requirements/quality.txt 195 | # edx-django-utils 196 | pyproject-api==1.9.1 197 | # via 198 | # -r requirements/ci.txt 199 | # tox 200 | pyproject-hooks==1.2.0 201 | # via 202 | # -r requirements/pip-tools.txt 203 | # build 204 | # pip-tools 205 | pytest==8.3.5 206 | # via 207 | # -r requirements/quality.txt 208 | # pytest-cov 209 | # pytest-django 210 | pytest-cov==6.1.1 211 | # via -r requirements/quality.txt 212 | pytest-django==4.11.1 213 | # via -r requirements/quality.txt 214 | python-dateutil==2.9.0.post0 215 | # via 216 | # -r requirements/quality.txt 217 | # freezegun 218 | python-slugify==8.0.4 219 | # via 220 | # -r requirements/quality.txt 221 | # code-annotations 222 | pyyaml==6.0.2 223 | # via 224 | # -r requirements/quality.txt 225 | # code-annotations 226 | # edx-django-release-util 227 | # edx-i18n-tools 228 | six==1.17.0 229 | # via 230 | # -r requirements/quality.txt 231 | # edx-django-release-util 232 | # edx-lint 233 | # python-dateutil 234 | snowballstemmer==3.0.1 235 | # via 236 | # -r requirements/quality.txt 237 | # pydocstyle 238 | sqlparse==0.5.3 239 | # via 240 | # -r requirements/quality.txt 241 | # django 242 | stevedore==5.4.1 243 | # via 244 | # -r requirements/quality.txt 245 | # code-annotations 246 | # edx-django-utils 247 | text-unidecode==1.3 248 | # via 249 | # -r requirements/quality.txt 250 | # python-slugify 251 | tomlkit==0.13.2 252 | # via 253 | # -r requirements/quality.txt 254 | # pylint 255 | tox==4.26.0 256 | # via -r requirements/ci.txt 257 | virtualenv==20.31.2 258 | # via 259 | # -r requirements/ci.txt 260 | # tox 261 | wheel==0.45.1 262 | # via 263 | # -r requirements/pip-tools.txt 264 | # pip-tools 265 | 266 | # The following packages are considered to be unsafe in a requirements file: 267 | # pip 268 | # setuptools 269 | -------------------------------------------------------------------------------- /requirements/doc.in: -------------------------------------------------------------------------------- 1 | # Requirements for documentation validation 2 | -c constraints.txt 3 | 4 | -r test.txt # Core and test dependencies so sphinx-apidoc can import the code to document 5 | 6 | sphinx-book-theme # Common theme for all Open edX projects 7 | readme_renderer # Validates README.rst for usage on PyPI 8 | Sphinx # Documentation builder 9 | twine 10 | -------------------------------------------------------------------------------- /requirements/doc.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # make upgrade 6 | # 7 | accessible-pygments==0.0.5 8 | # via pydata-sphinx-theme 9 | alabaster==1.0.0 10 | # via sphinx 11 | asgiref==3.8.1 12 | # via 13 | # -r requirements/test.txt 14 | # django 15 | babel==2.17.0 16 | # via 17 | # pydata-sphinx-theme 18 | # sphinx 19 | backports-tarfile==1.2.0 20 | # via jaraco-context 21 | beautifulsoup4==4.13.4 22 | # via pydata-sphinx-theme 23 | certifi==2025.4.26 24 | # via requests 25 | cffi==1.17.1 26 | # via 27 | # -r requirements/test.txt 28 | # cryptography 29 | # pynacl 30 | charset-normalizer==3.4.2 31 | # via requests 32 | click==8.2.1 33 | # via 34 | # -r requirements/test.txt 35 | # edx-django-utils 36 | coverage[toml]==7.8.2 37 | # via 38 | # -r requirements/test.txt 39 | # pytest-cov 40 | cryptography==45.0.3 41 | # via secretstorage 42 | ddt==1.7.2 43 | # via -r requirements/test.txt 44 | django==4.2.21 45 | # via 46 | # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt 47 | # -r requirements/test.txt 48 | # django-crum 49 | # django-waffle 50 | # djangorestframework 51 | # edx-django-release-util 52 | # edx-django-utils 53 | django-crum==0.7.9 54 | # via 55 | # -r requirements/test.txt 56 | # edx-django-utils 57 | django-waffle==4.2.0 58 | # via 59 | # -r requirements/test.txt 60 | # edx-django-utils 61 | djangorestframework==3.16.0 62 | # via -r requirements/test.txt 63 | docutils==0.21.2 64 | # via 65 | # pydata-sphinx-theme 66 | # readme-renderer 67 | # sphinx 68 | edx-django-release-util==1.5.0 69 | # via -r requirements/test.txt 70 | edx-django-utils==8.0.0 71 | # via -r requirements/test.txt 72 | freezegun==1.5.2 73 | # via -r requirements/test.txt 74 | id==1.5.0 75 | # via twine 76 | idna==3.10 77 | # via requests 78 | imagesize==1.4.1 79 | # via sphinx 80 | importlib-metadata==8.7.0 81 | # via keyring 82 | iniconfig==2.1.0 83 | # via 84 | # -r requirements/test.txt 85 | # pytest 86 | jaraco-classes==3.4.0 87 | # via keyring 88 | jaraco-context==6.0.1 89 | # via keyring 90 | jaraco-functools==4.1.0 91 | # via keyring 92 | jeepney==0.9.0 93 | # via 94 | # keyring 95 | # secretstorage 96 | jinja2==3.1.6 97 | # via sphinx 98 | keyring==25.6.0 99 | # via twine 100 | markdown-it-py==3.0.0 101 | # via rich 102 | markupsafe==3.0.2 103 | # via jinja2 104 | mdurl==0.1.2 105 | # via markdown-it-py 106 | more-itertools==10.7.0 107 | # via 108 | # jaraco-classes 109 | # jaraco-functools 110 | nh3==0.2.21 111 | # via readme-renderer 112 | packaging==25.0 113 | # via 114 | # -r requirements/test.txt 115 | # pydata-sphinx-theme 116 | # pytest 117 | # sphinx 118 | # twine 119 | pbr==6.1.1 120 | # via 121 | # -r requirements/test.txt 122 | # stevedore 123 | pluggy==1.6.0 124 | # via 125 | # -r requirements/test.txt 126 | # pytest 127 | psutil==7.0.0 128 | # via 129 | # -r requirements/test.txt 130 | # edx-django-utils 131 | pycparser==2.22 132 | # via 133 | # -r requirements/test.txt 134 | # cffi 135 | pydata-sphinx-theme==0.15.4 136 | # via sphinx-book-theme 137 | pygments==2.19.1 138 | # via 139 | # accessible-pygments 140 | # pydata-sphinx-theme 141 | # readme-renderer 142 | # rich 143 | # sphinx 144 | pynacl==1.5.0 145 | # via 146 | # -r requirements/test.txt 147 | # edx-django-utils 148 | pytest==8.3.5 149 | # via 150 | # -r requirements/test.txt 151 | # pytest-cov 152 | # pytest-django 153 | pytest-cov==6.1.1 154 | # via -r requirements/test.txt 155 | pytest-django==4.11.1 156 | # via -r requirements/test.txt 157 | python-dateutil==2.9.0.post0 158 | # via 159 | # -r requirements/test.txt 160 | # freezegun 161 | pyyaml==6.0.2 162 | # via 163 | # -r requirements/test.txt 164 | # edx-django-release-util 165 | readme-renderer==44.0 166 | # via 167 | # -r requirements/doc.in 168 | # twine 169 | requests==2.32.3 170 | # via 171 | # id 172 | # requests-toolbelt 173 | # sphinx 174 | # twine 175 | requests-toolbelt==1.0.0 176 | # via twine 177 | rfc3986==2.0.0 178 | # via twine 179 | rich==14.0.0 180 | # via twine 181 | roman-numerals-py==3.1.0 182 | # via sphinx 183 | secretstorage==3.3.3 184 | # via keyring 185 | six==1.17.0 186 | # via 187 | # -r requirements/test.txt 188 | # edx-django-release-util 189 | # python-dateutil 190 | snowballstemmer==3.0.1 191 | # via sphinx 192 | soupsieve==2.7 193 | # via beautifulsoup4 194 | sphinx==8.2.3 195 | # via 196 | # -r requirements/doc.in 197 | # pydata-sphinx-theme 198 | # sphinx-book-theme 199 | sphinx-book-theme==1.1.4 200 | # via -r requirements/doc.in 201 | sphinxcontrib-applehelp==2.0.0 202 | # via sphinx 203 | sphinxcontrib-devhelp==2.0.0 204 | # via sphinx 205 | sphinxcontrib-htmlhelp==2.1.0 206 | # via sphinx 207 | sphinxcontrib-jsmath==1.0.1 208 | # via sphinx 209 | sphinxcontrib-qthelp==2.0.0 210 | # via sphinx 211 | sphinxcontrib-serializinghtml==2.0.0 212 | # via sphinx 213 | sqlparse==0.5.3 214 | # via 215 | # -r requirements/test.txt 216 | # django 217 | stevedore==5.4.1 218 | # via 219 | # -r requirements/test.txt 220 | # edx-django-utils 221 | twine==6.1.0 222 | # via -r requirements/doc.in 223 | typing-extensions==4.13.2 224 | # via 225 | # beautifulsoup4 226 | # pydata-sphinx-theme 227 | urllib3==2.2.3 228 | # via 229 | # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt 230 | # requests 231 | # twine 232 | zipp==3.22.0 233 | # via importlib-metadata 234 | 235 | # The following packages are considered to be unsafe in a requirements file: 236 | # setuptools 237 | -------------------------------------------------------------------------------- /requirements/pip-tools.in: -------------------------------------------------------------------------------- 1 | # Just the dependencies to run pip-tools, mainly for the "upgrade" make target 2 | -c constraints.txt 3 | 4 | pip-tools # Contains pip-compile, used to generate pip requirements files 5 | -------------------------------------------------------------------------------- /requirements/pip-tools.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # make upgrade 6 | # 7 | build==1.2.2.post1 8 | # via pip-tools 9 | click==8.2.1 10 | # via pip-tools 11 | packaging==25.0 12 | # via build 13 | pip-tools==7.4.1 14 | # via -r requirements/pip-tools.in 15 | pyproject-hooks==1.2.0 16 | # via 17 | # build 18 | # pip-tools 19 | wheel==0.45.1 20 | # via pip-tools 21 | 22 | # The following packages are considered to be unsafe in a requirements file: 23 | # pip 24 | # setuptools 25 | -------------------------------------------------------------------------------- /requirements/pip.in: -------------------------------------------------------------------------------- 1 | -c constraints.txt 2 | 3 | # Core dependencies for installing other packages 4 | 5 | pip 6 | setuptools 7 | wheel 8 | -------------------------------------------------------------------------------- /requirements/pip.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # make upgrade 6 | # 7 | wheel==0.45.1 8 | # via -r requirements/pip.in 9 | 10 | # The following packages are considered to be unsafe in a requirements file: 11 | pip==24.2 12 | # via 13 | # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt 14 | # -r requirements/pip.in 15 | setuptools==80.9.0 16 | # via -r requirements/pip.in 17 | -------------------------------------------------------------------------------- /requirements/private.readme: -------------------------------------------------------------------------------- 1 | # If there are any Python packages you want to keep in your virtualenv beyond 2 | # those listed in the official requirements files, create a "private.in" file 3 | # and list them there. Generate the corresponding "private.txt" file pinning 4 | # all of their indirect dependencies to specific versions as follows: 5 | 6 | # pip-compile private.in 7 | 8 | # This allows you to use "pip-sync" without removing these packages: 9 | 10 | # pip-sync requirements/*.txt 11 | 12 | # "private.in" and "private.txt" aren't checked into git to avoid merge 13 | # conflicts, and the presence of this file allows "private.*" to be 14 | # included in scripted pip-sync usage without requiring that those files be 15 | # created first. 16 | -------------------------------------------------------------------------------- /requirements/quality.in: -------------------------------------------------------------------------------- 1 | # Requirements for code quality checks 2 | -c constraints.txt 3 | 4 | -r test.txt # Core and test dependencies, for pylint analysis 5 | 6 | edx-lint # edX pylint rules and plugins 7 | pycodestyle # PEP 8 compliance validation 8 | pydocstyle # PEP 257 compliance validation 9 | -------------------------------------------------------------------------------- /requirements/quality.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # make upgrade 6 | # 7 | asgiref==3.8.1 8 | # via 9 | # -r requirements/test.txt 10 | # django 11 | astroid==3.3.10 12 | # via 13 | # pylint 14 | # pylint-celery 15 | cffi==1.17.1 16 | # via 17 | # -r requirements/test.txt 18 | # pynacl 19 | click==8.2.1 20 | # via 21 | # -r requirements/test.txt 22 | # click-log 23 | # code-annotations 24 | # edx-django-utils 25 | # edx-lint 26 | click-log==0.4.0 27 | # via edx-lint 28 | code-annotations==2.3.0 29 | # via edx-lint 30 | coverage[toml]==7.8.2 31 | # via 32 | # -r requirements/test.txt 33 | # pytest-cov 34 | ddt==1.7.2 35 | # via -r requirements/test.txt 36 | dill==0.4.0 37 | # via pylint 38 | django==4.2.21 39 | # via 40 | # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt 41 | # -r requirements/test.txt 42 | # django-crum 43 | # django-waffle 44 | # djangorestframework 45 | # edx-django-release-util 46 | # edx-django-utils 47 | django-crum==0.7.9 48 | # via 49 | # -r requirements/test.txt 50 | # edx-django-utils 51 | django-waffle==4.2.0 52 | # via 53 | # -r requirements/test.txt 54 | # edx-django-utils 55 | djangorestframework==3.16.0 56 | # via -r requirements/test.txt 57 | edx-django-release-util==1.5.0 58 | # via -r requirements/test.txt 59 | edx-django-utils==8.0.0 60 | # via -r requirements/test.txt 61 | edx-lint==5.6.0 62 | # via -r requirements/quality.in 63 | freezegun==1.5.2 64 | # via -r requirements/test.txt 65 | iniconfig==2.1.0 66 | # via 67 | # -r requirements/test.txt 68 | # pytest 69 | isort==6.0.1 70 | # via pylint 71 | jinja2==3.1.6 72 | # via code-annotations 73 | markupsafe==3.0.2 74 | # via jinja2 75 | mccabe==0.7.0 76 | # via pylint 77 | packaging==25.0 78 | # via 79 | # -r requirements/test.txt 80 | # pytest 81 | pbr==6.1.1 82 | # via 83 | # -r requirements/test.txt 84 | # stevedore 85 | platformdirs==4.3.8 86 | # via pylint 87 | pluggy==1.6.0 88 | # via 89 | # -r requirements/test.txt 90 | # pytest 91 | psutil==7.0.0 92 | # via 93 | # -r requirements/test.txt 94 | # edx-django-utils 95 | pycodestyle==2.13.0 96 | # via -r requirements/quality.in 97 | pycparser==2.22 98 | # via 99 | # -r requirements/test.txt 100 | # cffi 101 | pydocstyle==6.3.0 102 | # via -r requirements/quality.in 103 | pylint==3.3.7 104 | # via 105 | # edx-lint 106 | # pylint-celery 107 | # pylint-django 108 | # pylint-plugin-utils 109 | pylint-celery==0.3 110 | # via edx-lint 111 | pylint-django==2.6.1 112 | # via edx-lint 113 | pylint-plugin-utils==0.8.2 114 | # via 115 | # pylint-celery 116 | # pylint-django 117 | pynacl==1.5.0 118 | # via 119 | # -r requirements/test.txt 120 | # edx-django-utils 121 | pytest==8.3.5 122 | # via 123 | # -r requirements/test.txt 124 | # pytest-cov 125 | # pytest-django 126 | pytest-cov==6.1.1 127 | # via -r requirements/test.txt 128 | pytest-django==4.11.1 129 | # via -r requirements/test.txt 130 | python-dateutil==2.9.0.post0 131 | # via 132 | # -r requirements/test.txt 133 | # freezegun 134 | python-slugify==8.0.4 135 | # via code-annotations 136 | pyyaml==6.0.2 137 | # via 138 | # -r requirements/test.txt 139 | # code-annotations 140 | # edx-django-release-util 141 | six==1.17.0 142 | # via 143 | # -r requirements/test.txt 144 | # edx-django-release-util 145 | # edx-lint 146 | # python-dateutil 147 | snowballstemmer==3.0.1 148 | # via pydocstyle 149 | sqlparse==0.5.3 150 | # via 151 | # -r requirements/test.txt 152 | # django 153 | stevedore==5.4.1 154 | # via 155 | # -r requirements/test.txt 156 | # code-annotations 157 | # edx-django-utils 158 | text-unidecode==1.3 159 | # via python-slugify 160 | tomlkit==0.13.2 161 | # via pylint 162 | 163 | # The following packages are considered to be unsafe in a requirements file: 164 | # setuptools 165 | -------------------------------------------------------------------------------- /requirements/test.in: -------------------------------------------------------------------------------- 1 | # Requirements for test runs. 2 | -c constraints.txt 3 | 4 | -r base.txt # Core dependencies of django-config-models 5 | 6 | pytest-cov # pytest extension for code coverage statistics 7 | pytest-django # pytest extension for better Django support 8 | ddt # Run a test case multiple times with different input 9 | freezegun # Allows tests to mock the output of assorted datetime module functions 10 | edx-django-release-util # Contains the reserved keyword check 11 | -------------------------------------------------------------------------------- /requirements/test.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # make upgrade 6 | # 7 | asgiref==3.8.1 8 | # via 9 | # -r requirements/base.txt 10 | # django 11 | cffi==1.17.1 12 | # via 13 | # -r requirements/base.txt 14 | # pynacl 15 | click==8.2.1 16 | # via 17 | # -r requirements/base.txt 18 | # edx-django-utils 19 | coverage[toml]==7.8.2 20 | # via pytest-cov 21 | ddt==1.7.2 22 | # via -r requirements/test.in 23 | # via 24 | # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt 25 | # -r requirements/base.txt 26 | # django-crum 27 | # django-waffle 28 | # djangorestframework 29 | # edx-django-release-util 30 | # edx-django-utils 31 | django-crum==0.7.9 32 | # via 33 | # -r requirements/base.txt 34 | # edx-django-utils 35 | django-waffle==4.2.0 36 | # via 37 | # -r requirements/base.txt 38 | # edx-django-utils 39 | # via -r requirements/base.txt 40 | edx-django-release-util==1.5.0 41 | # via -r requirements/test.in 42 | edx-django-utils==8.0.0 43 | # via -r requirements/base.txt 44 | freezegun==1.5.2 45 | # via -r requirements/test.in 46 | iniconfig==2.1.0 47 | # via pytest 48 | packaging==25.0 49 | # via pytest 50 | pbr==6.1.1 51 | # via 52 | # -r requirements/base.txt 53 | # stevedore 54 | pluggy==1.6.0 55 | # via pytest 56 | psutil==7.0.0 57 | # via 58 | # -r requirements/base.txt 59 | # edx-django-utils 60 | pycparser==2.22 61 | # via 62 | # -r requirements/base.txt 63 | # cffi 64 | pynacl==1.5.0 65 | # via 66 | # -r requirements/base.txt 67 | # edx-django-utils 68 | pytest==8.3.5 69 | # via 70 | # pytest-cov 71 | # pytest-django 72 | pytest-cov==6.1.1 73 | # via -r requirements/test.in 74 | pytest-django==4.11.1 75 | # via -r requirements/test.in 76 | python-dateutil==2.9.0.post0 77 | # via freezegun 78 | pyyaml==6.0.2 79 | # via edx-django-release-util 80 | six==1.17.0 81 | # via 82 | # edx-django-release-util 83 | # python-dateutil 84 | sqlparse==0.5.3 85 | # via 86 | # -r requirements/base.txt 87 | # django 88 | stevedore==5.4.1 89 | # via 90 | # -r requirements/base.txt 91 | # edx-django-utils 92 | 93 | # The following packages are considered to be unsafe in a requirements file: 94 | # setuptools 95 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # pylint: disable=C0111,W6005,W6100 3 | 4 | import os 5 | import re 6 | import sys 7 | 8 | from setuptools import setup 9 | 10 | 11 | def get_version(*file_paths): 12 | """ 13 | Extract the version string from the file at the given relative path fragments. 14 | """ 15 | filename = os.path.join(os.path.dirname(__file__), *file_paths) 16 | version_file = open(filename).read() 17 | version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", 18 | version_file, re.M) 19 | if version_match: 20 | return version_match.group(1) 21 | raise RuntimeError('Unable to find version string.') 22 | 23 | 24 | def load_requirements(*requirements_paths): 25 | """ 26 | Load all requirements from the specified requirements files. 27 | 28 | Requirements will include any constraints from files specified 29 | with -c in the requirements files. 30 | Returns a list of requirement strings. 31 | """ 32 | # UPDATED VIA SEMGREP - if you need to remove/modify this method remove this line and add a comment specifying why. 33 | 34 | # e.g. {"django": "Django", "confluent-kafka": "confluent_kafka[avro]"} 35 | by_canonical_name = {} 36 | 37 | def check_name_consistent(package): 38 | """ 39 | Raise exception if package is named different ways. 40 | 41 | This ensures that packages are named consistently so we can match 42 | constraints to packages. It also ensures that if we require a package 43 | with extras we don't constrain it without mentioning the extras (since 44 | that too would interfere with matching constraints.) 45 | """ 46 | canonical = package.lower().replace('_', '-').split('[')[0] 47 | seen_spelling = by_canonical_name.get(canonical) 48 | if seen_spelling is None: 49 | by_canonical_name[canonical] = package 50 | elif seen_spelling != package: 51 | raise Exception( 52 | f'Encountered both "{seen_spelling}" and "{package}" in requirements ' 53 | 'and constraints files; please use just one or the other.' 54 | ) 55 | 56 | requirements = {} 57 | constraint_files = set() 58 | 59 | # groups "pkg<=x.y.z,..." into ("pkg", "<=x.y.z,...") 60 | re_package_name_base_chars = r"a-zA-Z0-9\-_." # chars allowed in base package name 61 | # Two groups: name[maybe,extras], and optionally a constraint 62 | requirement_line_regex = re.compile( 63 | r"([%s]+(?:\[[%s,\s]+\])?)([<>=][^#\s]+)?" 64 | % (re_package_name_base_chars, re_package_name_base_chars) 65 | ) 66 | 67 | def add_version_constraint_or_raise(current_line, current_requirements, add_if_not_present): 68 | regex_match = requirement_line_regex.match(current_line) 69 | if regex_match: 70 | package = regex_match.group(1) 71 | version_constraints = regex_match.group(2) 72 | check_name_consistent(package) 73 | existing_version_constraints = current_requirements.get(package, None) 74 | # It's fine to add constraints to an unconstrained package, 75 | # but raise an error if there are already constraints in place. 76 | if existing_version_constraints and existing_version_constraints != version_constraints: 77 | raise BaseException(f'Multiple constraint definitions found for {package}:' 78 | f' "{existing_version_constraints}" and "{version_constraints}".' 79 | f'Combine constraints into one location with {package}' 80 | f'{existing_version_constraints},{version_constraints}.') 81 | if add_if_not_present or package in current_requirements: 82 | current_requirements[package] = version_constraints 83 | 84 | # Read requirements from .in files and store the path to any 85 | # constraint files that are pulled in. 86 | for path in requirements_paths: 87 | with open(path) as reqs: 88 | for line in reqs: 89 | if is_requirement(line): 90 | add_version_constraint_or_raise(line, requirements, True) 91 | if line and line.startswith('-c') and not line.startswith('-c http'): 92 | constraint_files.add(os.path.dirname(path) + '/' + line.split('#')[0].replace('-c', '').strip()) 93 | 94 | # process constraint files: add constraints to existing requirements 95 | for constraint_file in constraint_files: 96 | with open(constraint_file) as reader: 97 | for line in reader: 98 | if is_requirement(line): 99 | add_version_constraint_or_raise(line, requirements, False) 100 | 101 | # process back into list of pkg><=constraints strings 102 | constrained_requirements = [f'{pkg}{version or ""}' for (pkg, version) in sorted(requirements.items())] 103 | return constrained_requirements 104 | 105 | 106 | def is_requirement(line): 107 | """ 108 | Return True if the requirement line is a package requirement. 109 | 110 | Returns: 111 | bool: True if the line is not blank, a comment, 112 | a URL, or an included file 113 | """ 114 | # UPDATED VIA SEMGREP - if you need to remove/modify this method remove this line and add a comment specifying why 115 | 116 | return line and line.strip() and not line.startswith(('-r', '#', '-e', 'git+', '-c')) 117 | 118 | 119 | VERSION = get_version('config_models', '__init__.py') 120 | 121 | if sys.argv[-1] == 'tag': 122 | print("Tagging the version on github:") 123 | os.system("git tag -a %s -m 'version %s'" % (VERSION, VERSION)) 124 | os.system("git push --tags") 125 | sys.exit() 126 | 127 | README = open(os.path.join(os.path.dirname(__file__), 'README.rst')).read() 128 | CHANGELOG = open(os.path.join(os.path.dirname(__file__), 'CHANGELOG.rst')).read() 129 | 130 | setup( 131 | name='django-config-models', 132 | version=VERSION, 133 | description="""Configuration models for Django allowing config management with auditing.""", 134 | long_description=README + '\n\n' + CHANGELOG, 135 | author='edX', 136 | author_email='oscm@edx.org', 137 | url='https://github.com/openedx/django-config-models', 138 | packages=[ 139 | 'config_models', 140 | ], 141 | include_package_data=True, 142 | install_requires=load_requirements('requirements/base.in'), 143 | license="AGPL 3.0", 144 | zip_safe=False, 145 | keywords='Django edx', 146 | classifiers=[ 147 | 'Development Status :: 3 - Alpha', 148 | 'Framework :: Django', 149 | 'Framework :: Django :: 4.2', 150 | 'Framework :: Django :: 5.2', 151 | 'Intended Audience :: Developers', 152 | 'License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)', 153 | 'Natural Language :: English', 154 | 'Programming Language :: Python :: 3', 155 | 'Programming Language :: Python :: 3.11', 156 | 'Programming Language :: Python :: 3.12', 157 | ], 158 | ) 159 | -------------------------------------------------------------------------------- /test_settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | These settings are here to use during tests, because django requires them. 3 | 4 | In a real-world use case, apps in this project are installed into other 5 | Django applications, so these settings will not be used. 6 | """ 7 | 8 | 9 | 10 | from os.path import abspath, dirname, join 11 | 12 | 13 | def root(*args): 14 | """ 15 | Get the absolute path of the given path relative to the project root. 16 | """ 17 | return join(abspath(dirname(__file__)), *args) 18 | 19 | 20 | DATABASES = { 21 | 'default': { 22 | 'ENGINE': 'django.db.backends.sqlite3', 23 | 'NAME': 'default.db', 24 | 'USER': '', 25 | 'PASSWORD': '', 26 | 'HOST': '', 27 | 'PORT': '', 28 | } 29 | } 30 | 31 | INSTALLED_APPS = ( 32 | 'django.contrib.auth', 33 | 'django.contrib.contenttypes', 34 | 'example.apps.ExampleConfig', 35 | 'config_models', 36 | 'tests', 37 | 'release_util' 38 | ) 39 | 40 | LOCALE_PATHS = [ 41 | root('config_models', 'conf', 'locale'), 42 | ] 43 | 44 | ROOT_URLCONF = 'config_models.urls' 45 | 46 | SECRET_KEY = 'insecure-secret-key' 47 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openedx/django-config-models/9097f36b0c54fef87ff9ae35f426e44ded6437ed/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """ 2 | Define pytest hooks. 3 | """ 4 | 5 | 6 | from django import db 7 | 8 | 9 | def pytest_runtest_setup(item): # pylint: disable=unused-argument 10 | """ 11 | Before any tests start, reset all django database connections. 12 | Used to make sure that tests running in multi processes aren't sharing 13 | a database connection. 14 | """ 15 | for db_ in db.connections.all(): 16 | db_.close() 17 | -------------------------------------------------------------------------------- /tests/data/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "model": "example.ExampleDeserializeConfig", 3 | "data": [ 4 | { 5 | "name": "betty", 6 | "enabled": true, 7 | "int_field": 5 8 | }, 9 | { 10 | "name": "fred", 11 | "enabled": false 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tests/test_admin.py: -------------------------------------------------------------------------------- 1 | """ 2 | ConfigurationModel Admin Module Test Cases 3 | """ 4 | from unittest.mock import patch 5 | 6 | from django.contrib.admin.sites import AdminSite 7 | from django.contrib.auth import get_user_model 8 | from django.contrib.messages.storage.fallback import FallbackStorage 9 | from django.http import HttpRequest 10 | from django.test import TestCase 11 | from example.models import ExampleKeyedConfig 12 | 13 | from config_models import admin 14 | from config_models.models import ConfigurationModel 15 | 16 | User = get_user_model() 17 | 18 | 19 | class AdminTestCaseMixin: 20 | """ 21 | Provide a request factory method. 22 | """ 23 | 24 | def get_request(self): 25 | request = HttpRequest() 26 | request.session = "session" 27 | request._messages = FallbackStorage(request) # pylint: disable=protected-access 28 | return request 29 | 30 | 31 | class AdminTestCase(TestCase, AdminTestCaseMixin): 32 | """ 33 | Test Case module for ConfigurationModel Admin 34 | """ 35 | 36 | def setUp(self): 37 | super().setUp() 38 | self.conf_admin = admin.ConfigurationModelAdmin(ConfigurationModel, AdminSite()) 39 | 40 | def test_default_fields(self): 41 | """ 42 | Test: checking fields 43 | """ 44 | request = self.get_request() 45 | self.assertEqual( 46 | list(self.conf_admin.get_form(request).base_fields), ["enabled"] 47 | ) 48 | 49 | 50 | class KeyedAdminTestCase(TestCase, AdminTestCaseMixin): 51 | """ 52 | Test case module for KeyedConfigurationModelAdmin. 53 | """ 54 | 55 | def get_edit_link(self): 56 | """ 57 | Return an edit link from a KeyedConfigurationModelAdmin. Patch the `reverse` 58 | and `_` methods to modify the return value. 59 | """ 60 | conf_admin = admin.KeyedConfigurationModelAdmin(ExampleKeyedConfig, AdminSite()) 61 | request = self.get_request() 62 | ExampleKeyedConfig.objects.create(user=User.objects.create()) 63 | config = conf_admin.get_queryset(request)[0] 64 | return conf_admin.edit_link(config) 65 | 66 | def test_edit_link(self): 67 | with patch.object(admin, "reverse", return_value="http://google.com"): 68 | self.assertEqual( 69 | 'Update', self.get_edit_link(), 70 | ) 71 | 72 | def test_edit_link_xss_url(self): 73 | with patch.object( 74 | admin, "reverse", return_value='">' 75 | ): 76 | edit_link = self.get_edit_link() 77 | 78 | self.assertNotIn( 79 | "' 89 | ): 90 | edit_link = self.get_edit_link() 91 | 92 | self.assertNotIn( 93 | "