././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736552720.6985018 django_dynamic_preferences-1.17.0/0000775000175000017500000000000014740330421016113 5ustar00agateagate././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/AUTHORS.rst0000644000175000017500000000166114413261622017777 0ustar00agateagate======= Credits ======= Development Lead ---------------- * Agate Blue Contributors ------------ * Ryan Anguiano, via `prefs-n-perms package `_ * [@willseward](https://github.com/willseward) * [@haroon-sheikh](https://github.com/haroon-sheikh) * [@yurtaev](https://github.com/yurtaev) * [@pomerama](https://github.com/pomerama) * [@philipbelesky](https://github.com/philipbelesky) * [@what-digital](https://github.com/what-digital) * [@czlee](https://github.com/czlee) * [@ricard33](https://github.com/ricard33) * [@JetUni](https://github.com/JetUni) * [@pip182](https://github.com/pip182) * [@JanMalte](https://github.com/JanMalte) * [@macolo](https://github.com/macolo) * [@fabrixxm](https://github.com/fabrixxm) * [@swalladge](https://github.com/swalladge) * [@rvignesh89](https://github.com/rvignesh89) * [@okolimar](https://github.com/okolimar) * [@hansegucker](https://github.com/hansegucker)././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736550780.0 django_dynamic_preferences-1.17.0/CONTRIBUTING.rst0000664000175000017500000000740714740324574020600 0ustar00agateagate============ Contributing ============ **Important**: We are using git-flow workflow here, so please submit your pull requests against develop branch (and not master). Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. You can contribute in many ways: Types of Contributions ---------------------- Report Bugs ~~~~~~~~~~~ Report bugs at https://github.com/agateblue/django-dynamic-preferences/issues. If you are reporting a bug, please include: * Your operating system name and version. * Any details about your local setup that might be helpful in troubleshooting. * Detailed steps to reproduce the bug. * Include whole stacktraces and error reports when necessary, directly in your issue body. Do not use external services such as pastebin. Contributing ------------ Fix Bugs ~~~~~~~~ Look through the GitHub issues for bugs. Anything tagged with "bug" is open to whoever wants to implement it. Implement Features ~~~~~~~~~~~~~~~~~~ Look through the GitHub issues for features. Anything tagged with "feature" is open to whoever wants to implement it. Write Documentation ~~~~~~~~~~~~~~~~~~~ django-dynamic-preferences could always use more documentation, whether as part of the official django-dynamic-preferences docs, in docstrings, or even on the web in blog posts, articles, and such. Submit Feedback ~~~~~~~~~~~~~~~ The best way to send feedback is to file an issue at https://github.com/agateblue/django-dynamic-preferences/issues. If you are proposing a feature: * Explain in detail how it would work. * Keep the scope as narrow as possible, to make it easier to implement. * Remember that this is a volunteer-driven project, and that contributions are welcome :) Get Started! ------------ Ready to contribute? Here's how to set up `django-dynamic-preferences` for local development. 1. Fork the `django-dynamic-preferences` repo on GitHub. 2. Clone your fork locally:: $ git clone git@github.com:your_name_here/django-dynamic-preferences.git 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: $ mkvirtualenv django-dynamic-preferences $ cd django-dynamic-preferences/ $ python setup.py develop 4. Create a branch for local development:: $ git checkout -b name-of-your-bugfix-or-feature Now you can make your changes locally. 5. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:: $ flake8 dynamic_preferences tests $ python setup.py test $ tox To get flake8 and tox, just pip install them into your virtualenv. 6. Commit your changes and push your branch to GitHub:: $ git add . $ git commit -m "Your detailed description of your changes." $ git push origin name-of-your-bugfix-or-feature 7. Submit a pull request through the GitHub website. Pull Request Guidelines ----------------------- Before you submit a pull request, check that it meets these guidelines: 1. The pull request should include tests. 2. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.rst. 3. The pull request should work for Python 3.8, 3.9, 3.10 and 3.11. Check https://travis-ci.org/agateblue/django-dynamic-preferences/pull_requests and make sure that the tests pass for all supported Python versions. 4. The pull request must target the `develop` branch, since the project relies on `git-flow branching model`_ .. _git-flow branching model: http://nvie.com/posts/a-successful-git-branching-model/ Tips ---- To run a subset of tests:: $ python -m unittest tests.test_dynamic_preferences ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736552639.0 django_dynamic_preferences-1.17.0/HISTORY.rst0000644000175000017500000005044414740330277020024 0ustar00agateagate.. :changelog: Changelog ========= 1.17.0 (2025-01-11) ******************* This version includes update to our django/python compatibility table. Tests now run against python 3.9, 3.10, 3.11, 3.12, 3.13, Django 4.2 and Django 5.1. These versions are officially supported. - Drop support for python 3.7, 3.8 and django 3.2. Also run tests on python 3.12, 3.13 and django 5.1 (#312) - `preference_updated` is now triggered when the preference is updated by the REST API (#310) - Add the preference model instance to the parameters of the `preference_updated` signal's. (#310) - Fix conversion of non-integer pk value in ModelSerializer (#307) - remove six usage (#306) - fix(docs): Fix quote in quickstart guide (#304) - fix(docs): Update Python and django versions in docs (#305) - return 400 when calling bulk update with an empty payload (#302) 1.16.0 (2023-10-15) ******************* - add raw prefix to invalid regex (#299) - Add Indonesian Translations (#298) - Added ability to pickle FilePreference (#297) - Fixed invalid year for 1.15 release 1.15.0 (2023-04-05) ******************* - Run tests against django 4.2 and main - Fixed broken PATCH with ModelMultipleSerializer (#291) - Fix MultipleObjectsReturned in preference admin (#285) - Batch cache updates to improve performances (#292) 1.14.0 (2022-08-24) ******************* - Drop official support for python 3.6 and django < 3.2. Previous versions may continue to work but aren't tested anymore - OFficial support for Django 4.1 and Python 3.10 - Fix #281: ensure preference_updated signal is trigerred when updating preference through admin UI - Include signal in example project (#281) - Fixed broken documentation build 1.13.0 (2022-06-12) ******************* - Add autofield to avoid unecessary migrations - Bumped dependencies - updated readme with supported python / django versions - Flake, autoflake and various cleanups for tests file - Blackified - Remove unittest TestCases, switch to pytest style - Handle serialization for non interger primary key in ModelMultipleSerializer )#268 - Updated readme badges - Fixed invalid workflow action - Ensure tests are run for incoming PRs (#271) - Added config option to specify the cache to use (#270) - Fix CI pipeline (#269) - Setup CI with Gitlab actions (#267) - FloatPreference can be initialized with either int or float (#266) 1.12.0 (2022-02-26) ******************* - Add ENABLE_GLOBAL_MODEL_AUTO_REGISTRATION setting (#259) - fix: checkpreferences command failed if MANAGER_ATTRIBUTE is changed (#258) - Allow to skip preference creation when checkpreferences is invoked (#257) - Use default django cache timeoout (#253) - Fix signal handler doc (#250) - Added htmlcov in .gitignore (#251) - Use stdout in checkpreferences (#252) - MAINT:dynamic_preferences serializers.py - - Exception handling for class ModelMultipleSerializer; types.py - handle queryset in api_reprs of ModelChoicePreference (#243) 1.11.0 (2021-10-09) ******************* - Update quickstart.rst (#240) - Fix model multiple choice preference to react correctly to deletion handler (#244) - fix #234 (#235) - Fix compatibility issues with python 3.10 and django 4.0 (#236) - Fixed a typo in documentation (#229) - Add polish translation (#227) - Fix the typos in the comments and documents (#225) - Update forms docs (#224) 1.10.1 (2020-08-21) ******************* - Fix django 3.0 and 3.1 compat (#218) - Generated missing user migrations (#221) - Dropped support for python 2 and Django 1.11 - Updated test matrix Contributors: - @Natureshadow - @agateblue 1.10 (2020-07-03) ***************** - Add MultipleChoicePreference (#21) Contributors: - @Natureshadow 1.9 (2020-05-06) **************** - Emit signal when a preference is updated (#207) - Pass instance provided in form builder to manager (#212) - Use PreferencesManager for saving preferences in forms (#211) - Fixed wrong filename when using FilePreference and saving multiple times (#198) - Fixed broken compat with restframework 3.11 (#200) - Fixed typo in documentation (#204) Contributors: - @agateblue - @hansegucker - @Natureshadow - @saemideluxe - @timgates42 1.8.1 (2019-12-29) ****************** - Django 3.0 and Python 3.8 compatibility (#194) Contributors: - @dadoeyad 1.8 (2019-11-06) ****************** - Add time preference type (#187) - Fix dependency conflict for issue (#183) - fix(migrations): add missing `verbose_name` (#184) - Fix crash: 'NoneType' object has no attribute 'name' (#190) - Test under Django 2.2 and Python 3.7 Contributors: - @capaci - @exequiel09 - @NeolithEra - @nourwolf - @treemo 1.7.1 (2019-07-30) ****************** - Added djangorestframework 3.10.x compatibility (#180) - Fixed direct access to ChoicePreference.choice (#177) - German and missing translations (#175) - Run makemigrations to add missing migrations file (#161) Contributors: - @JITdev - @izimobil - @jwaschkau - @exequiel09 1.7 (2018-11-19) **************** - Fix string format arguments in get_by_name error (#157) - Fix UserPreferenceRegistry and its 'section_url_namespace' attribute (#152) - Handle 'required' attribute for all inherited BasePreferenceType class (#153) - add section filter in query string for DRF list endpoint (#154) - Fix ModelChoicePreference when using with model attribute and not queryset (#151) - Update outdated context_processors documentation (#149) - Update README.rst (#147) - Fixed ModelMultipleSerializer.to_python() (#146) - Added ModelMultipleChoicePreference Contributors: - @eriktelepovsky - @monkeywithacupcake - @ptrstn - @jordiromera - @calvin620707 - @czlee - @ElManaa 1.6 (2018-06-17) **************** - Fixed #141 and #141: migrations issues (see below) - Dropped support for django < 1.11 - Dropped support for Python 3.4 - Better namespaces for urls Better namespaces for urls -------------------------- Historically, the package included multiple urls. To ensure compatibility with django 2 and better namespacing, you should update any references to those urls as described below: +-------------------------------------+-------------------------------------+ | Old url | New url | +=====================================+=====================================+ | dynamic_preferences.global | dynamic_preferences:global | +-------------------------------------+-------------------------------------+ | dynamic_preferences.global.section | dynamic_preferences:global.section | +-------------------------------------+-------------------------------------+ | dynamic_preferences.user | dynamic_preferences:user | +-------------------------------------+-------------------------------------+ | dynamic_preferences.user.section | dynamic_preferences:user.section | +-------------------------------------+-------------------------------------+ Migration cleanup ----------------- This version includes a proper fix for migration issues. Full background is available at https://github.com/agateblue/django-dynamic-preferences/pull/142, but here is the gist of it: 1. Early versions of dynamic_preferences included the user and global preferences models in the same app 2. The community requested a way to disable user preferences. The only way to do that was to move the user preference model in a dedicated app (dynamic_preferences_user 3. A migration was written to handle that transparently, but this was not actually possible to have something that worked for both existing and new installations 4. Thus, we ended up with issues such as #140 or #141, inconsistent db state, tables lying around in the database, etc. I'd like to apologize to everyone impacted. By trying to make 3. completely transparent to everyone and avoid a manual migration step for new installations, I actually made things worse. This release should fix all that: any remains of the user app was removed from the main app migrations. For any new user, it will be like nothing happened. For existing installations with user preferences disabled, there is nothing to do, apart from deleting the `dynamic_preferences_users_userpreferencemodel` table in your database. For existing installations with user preferences enabled, there is nothing to do. You should have ``'dynamic_preferences.users.apps.UserPreferencesConfig'`` in your installed apps. If ``python manage.py migrate`` fails with ``django.db.utils.ProgrammingError: relation "dynamic_preferences_users_userpreferencemodel" already exists``, this probably means you are upgrading for a really old release. In such event, simply skip the initial migration for the ``dynamic_preferences_user`` app by running ``python manage.py migrate dynamic_preferences_users 0001 --fake``. Many thanks to all people who helped clearing this mess, especially @czlee. 1.5.1 (06-03-2018) ****************** This is a minor bugfix release: * Get proper PreferenceModelsRegistry when preference is proxy model (#137) * Add missing `format()` to IntegerSerializer exception text (#138) * Add some attributes to PerInstancePreferenceAdmin (#135) Contributors: * @czlee * @danie1k 1.5 (16-12-2017) ****************** From now on, django-dynamic-preferences should fully support Django 2.0. This release should be fully backward-compatible with previous versions of the module. You will still have to upgrade your own code to work with Django 2, like adding on_delete option to your ForeignKey fields. * removed typo in API code that could cause a crash (#127) * added on_dete=models.CASCADE to migrations for Django 2.0 compatibility (#129 and #131) * Duration, date and datetime serialization issue in rest framework (#115) Contributors: * @rvignesh89 * @zamai 1.4.2 (06-11-2017) ****************** * Fix #121: reverted Section import missing from dynamic_preferences.types Contributors: * @okolimar * @swalladge 1.4.1 (03-11-2017) ****************** * Section verbose name and filter in django admin (#114) * Fixed wrong import in Quickstart documentation (#113) * Fix #111: use path as returned by storage save method (#112) Contributors: * @okolimar * @swalladge 1.4 (15-10-2017) ****************** * Fix #8: we now have date, datetime and duration preferences * Fix #108: Dropped tests and guaranteed compatibility with django 1.8 and 1.9, though * Fix #103: bugged filtering of user preferences via REST API * Fix #78: removed ``create_default_per_instance_preferences``. This is *not* considered a backward-incompatible change as this method did nothing at all and was not documented Contributors: * @rvignesh89 * @haroon-sheikh 1.3.3 (25-09-2017) ****************** * Fix #97 where the API serializer could crash during preference update because of incomplete parsing Contributors: * @rvignesh89 1.3.2 (11-09-2017) ****************** * Should fix Python 3.3 complaints in CI, also add tests on Python 3.6 (#94) * Fixed #75: Fix checkpreferences command that was not deleting obsolete preferences anymore (#93) * Retrieve existing preferences in bulk (#92) * Cache values when queried in all() (#91) Contributors: * @czlee 1.3.1 (30-07-2017) ****************** - Fix #84: serialization error for preferences with None value (@swalladge) - More documentation about preferences form fields 1.3 (03-07-2017) ******************* This release fix a critical bug in 1.2 that can result in data loss. Please upgrade to 1.3 as soon as possible and never use 1.2 in production. See `#81 `_ for more details. 1.2 (06-07-2017) ******************* .. warning:: There is a critical bug in this that can result in dataloss. Please upgrade to 1.3 as soon as possible and never use 1.2 in production. See `#81 `_ for more details. - important performance improvements (less database and cache queries) - A brand new `REST API `_ based on Django REST Framework, to interact with preferences (this is an optionnal, opt-in feature) - A new `FilePreference `_ [original work by @macolo] 1.1.1 (11-05-2017) ******************* Bugfix release to restore disabled user preferences admin (#77). 1.1 (06-03-2017) ***************** * Fixed #49 and #71 by passing full section objects in templates (and not just the section identifiers). This means it's easier to write template that use sections, for example if you want have i18n in your project and want to display the translated section's name. URL reversing for sections is also more reliable in templates. If you subclassed `PreferenceRegistry` to implement your own preference class and use the built-in templates, you need to add a ``section_url_namespace`` attribute to your registry class to benefit from the new URL reversing. [Major release] 1.0 (21-02-2017) *********************************** Dynamic-preferences was release more than two years ago, and since then, more than 20 feature and bugfixe releases have been published. But even after two years the project was still advertised as in Alpha-state on PyPi, and the tags used for the releases, were implicitly saying that the project was not production-ready. Today, we're changing that by releasing the first major version of dynamic-preferences, the ``1.0`` release. We will stick to semantic versioning and keep backward compatibility until the next major version. Dynamic-preferences is already used in various production applications .The implemented features are stable, working, and address many of the uses cases the project was designed for: - painless and efficient global configuration for your project - painless and efficient per-user (or any other model) settings - ease-of-use, both for end-user (via the admin interface) and developpers (settings are easy to create and to manage) - more than decent performance, thanks to caching By making a major release, we want to show that the project is trustworthy and, in the end, to attract new users and develop the community around it. Development will goes on as before, with an increased focus on stability and backward compatibility. **Because of the major version switch, some dirt was removed from the code, and manual intervention is required for the upgrade. Please have a for the detailed instructions:** https://django-dynamic-preferences.readthedocs.io/en/latest/upgrade.html Thanks to all the people who contributed over the years by reporting bugs, asking for new features, working on the documentation or on implementing solutions! 0.8.4 (10-01-2017) ****************** This version is an emergency release to restore backward compatibility that was broken in 0.8.3, as described in issue #67. Please upgrade as soon as possible if you use 0.8.3. Special thanks to [czlee](https://github.com/czlee) for reporting this! 0.8.3 (06-01-2017) (**DO NOT USE: BACKWARD INCOMPATIBLE**) ********************************************************** **This release introduced by mistake a backward incompatible change (commit 723f2e).** **Please upgrade to 0.8.4 or higher to restore backward compatibility with earlier versions** This is a small bugfix release. Happy new year everyone! * Now fetch model default value using the get_default method * Fixed #50: now use real apps path for autodiscovering, should fix some strange error when using AppConfig and explicit AppConfig path in INSTALLED_APPS * Fix #63: Added initial doc to explain how to bind preferences to arbitrary models (#65) * Added test to ensure form submission works when no section filter is applied, see #53 * Example project now works with latest django versions * Added missing max_length on example model * Fixed a few typos in example project 0.8.2 (23-08-2016) ****************** * Added django 1.10 compatibility [ricard33] * Fixed tests for django 1.7 * Fix issue #57: PreferenceManager.get() returns value [ricard33] * Fixed missing coma in boolean serializer [czlee] * Added some documentations and example [JetUni] 0.8.1 (25-02-2016) ****************** * Fixed still inconsistend preference order in form builder (#44) [czlee] 0.8 (23-02-2016) **************** **Warning**: there is a backward incompatbile change in this release. To address #45 and #46, an import statement was removed from __init__.py. Please refer to the documentation for upgrade instructions: http://django-dynamic-preferences.readthedocs.org/en/stable/upgrade.html 0.7.2 (23-02-2016) ****************** * Fix #45: importerrror on pip install, and removed useless import * Replaced built-in registries by persisting_theory, this will maintain a consistent order for preferences, see #44 0.7.1 (12-02-2016) ****************** * Removed useless sections and fixed typos/structure in documentation, fix #39 * Added setting to disable user preferences admin, see #33 * Added setting to disable preference caching, fix #7 * Added validation agains sections and preferences names, fix #28, it could raise backward incompatible behaviour, since invalid names will stop execution by default 0.7 (12-01-2016) **************** * Added by_name and get_by_name methods on manager to retrieve preferences without using sections, fix #34 * Added float preference, fix #31 [philipbelesky] * Made name, section read-only in django admin, fix #36 [what-digital] * Fixed typos in documentation [philipbelesky] 0.6.6 (23-12-2015) ****************** * Fixed #23 (again bis repetita): Fixed second migration to create section and name columns with correct length 0.6.5 (23-12-2015) ****************** * Fixed #23 (again): Fixed initial migration to create section and name columns with correct length 0.6.4 (23-12-2015) ****************** * Fixed #23: Added migration for shorter names and sections 0.6.3 (09-12-2015) ****************** * Fixed #27: AttributeError: 'unicode' object has no attribute 'name' in preference `__repr__` [pomerama] 0.6.2 (24-11-2015) ****************** * Added support for django 1.9, [yurtaev] * Better travic CI conf (which run tests against two version of Python and three versions of django up to 1.9), fix #22 [yurtaev] 0.6.1 (6-11-2015) ***************** * Added decimal field and serializer 0.6 (24-10-2015) **************** * Fixed #10 : added model choice preference * Fixed #19 : Sections are now plain python objects, the string notation is now deprecated 0.5.4 (06-09-2015) ****************** * Merged PR #16 that fix a typo in the code 0.5.3 (24-08-2015) ****************** * Added switch for list_editable in admin and warning in documentation, fix #14 * Now use Textarea for LongStringPreference, fix #15 0.5.2 (22-07-2015) ****************** * Fixed models not loaded error 0.5.1 (17-07-2015) ****************** * Fixed pip install (#3), thanks @willseward * It's now easier to override preference form field attributes on a preference (please refer to `Preferences attributes `_ for more information) * Cleaner serializer api 0.5 (12-07-2015) **************** This release may involves some specific upgrade steps, please refer to the ``Upgrade`` section of the documentation. 0.5 (12-07-2015) **************** This release may involves some specific upgrade steps, please refer to the ``Upgrade`` section of the documentation. * Migration to CharField for section and name fields. This fix MySQL compatibility issue #2 * Updated example project to the 0.4 API 0.4.2 (05-07-2015) ****************** * Minor changes to README / docs 0.4.1 (05-07-2015) ****************** * The cookiecutter part was not fully merged 0.4 (05-07-2015) **************** * Implemented cache to avoid database queries when possible, which should result in huge performance improvements * Whole API cleanup, we now use dict-like objects to get preferences values, which simplifies the code a lot (Thanks to Ryan Anguiano) * Migrated the whole app to cookiecutter-djangopackage layout * Docs update to reflect the new API 0.3.1 (10-06-2015) ****************** * Improved test setup * More precise data in setup.py classifiers 0.2.4 (14-10-2014) ****************** * Added Python 3.4 compatibility 0.2.3 (22-08-2014) ****************** * Added LongStringPreference 0.2.2 (21-08-2014) ****************** * Removed view that added global and user preferences to context. They are now replaced by template context processors 0.2.1 (09-07-2014) ****************** * Switched from GPLv3 to BSD license ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/LICENSE0000644000175000017500000000271314413261622017124 0ustar00agateagateCopyright (c) 2014, Agate Blue All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of django-dynamic-preferences nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1593767263.0 django_dynamic_preferences-1.17.0/MANIFEST.in0000644000175000017500000000037413677572537017703 0ustar00agateagateinclude AUTHORS.rst include CONTRIBUTING.rst include HISTORY.rst include LICENSE include README.rst recursive-include dynamic_preferences *.html *.png *.gif *js *.css *jpg *jpeg *svg *py recursive-include dynamic_preferences/locale django.mo django.po ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736552720.6985018 django_dynamic_preferences-1.17.0/PKG-INFO0000644000175000017500000001027714740330421017215 0ustar00agateagateMetadata-Version: 2.2 Name: django-dynamic-preferences Version: 1.17.0 Summary: Dynamic global and instance settings for your django project Home-page: https://github.com/agateblue/django-dynamic-preferences Author: Agate Blue Author-email: me+github@agate.blue License: BSD Keywords: django-dynamic-preferences Classifier: Development Status :: 5 - Production/Stable Classifier: Framework :: Django Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Natural Language :: English Classifier: Programming Language :: Python :: 3 License-File: LICENSE License-File: AUTHORS.rst Requires-Dist: django>=4.2 Requires-Dist: persisting_theory==1.0 Dynamic: author Dynamic: author-email Dynamic: classifier Dynamic: description Dynamic: home-page Dynamic: keywords Dynamic: license Dynamic: requires-dist Dynamic: summary ============================= django-dynamic-preferences ============================= .. image:: https://badge.fury.io/py/django-dynamic-preferences.png :target: https://badge.fury.io/py/django-dynamic-preferences .. image:: https://readthedocs.org/projects/django-dynamic-preferences/badge/?version=latest :target: http://django-dynamic-preferences.readthedocs.org/en/latest/ .. image:: https://github.com/agateblue/django-dynamic-preferences/actions/workflows/tests.yml/badge.svg :target: https://github.com/agateblue/django-dynamic-preferences/actions/workflows/tests.yml .. image:: https://opencollective.com/django-dynamic-preferences/backers/badge.svg :alt: Backers on Open Collective :target: #backers .. image:: https://opencollective.com/django-dynamic-preferences/sponsors/badge.svg :alt: Sponsors on Open Collective :target: #sponsors Dynamic-preferences is a Django app, BSD-licensed, designed to help you manage your project settings. While most of the time, a ``settings.py`` file is sufficient, there are some situations where you need something more flexible such as: * per-user settings (or, generally speaking, per instance settings) * settings change without server restart For per-instance settings, you could actually store them in some kind of profile model. However, it means that every time you want to add a new setting, you need to add a new column to the profile DB table. Not very efficient. Dynamic-preferences allow you to register settings (a.k.a. preferences) in a declarative way. Preferences values are serialized before storage in database, and automatically deserialized when you need them. With dynamic-preferences, you can update settings on the fly, through django's admin or custom forms, without restarting your application. The project is tested and work under Python 3.7, 3.8, 3.9, 3.10 and 3.11 and with django 3.2 and 4.2. Features -------- * Simple to setup * Admin integration * Forms integration * Bundled with global and per-user preferences * Can be extended to other models if need (e.g. per-site preferences) * Integrates with django caching mechanisms to improve performance Documentation ------------- The full documentation is at https://django-dynamic-preferences.readthedocs.org. Changelog --------- See https://django-dynamic-preferences.readthedocs.io/en/latest/history.html Contributing ------------ See https://django-dynamic-preferences.readthedocs.org/en/latest/contributing.html Credits +++++++ Contributors ------------ This project exists thanks to all the people who contribute! .. image:: https://opencollective.com/django-dynamic-preferences/contributors.svg?width=890&button=false Backers ------- Thank you to all our backers! `Become a backer`__. .. image:: https://opencollective.com/django-dynamic-preferences/backers.svg?width=890 :target: https://opencollective.com/django-dynamic-preferences#backers __ Backer_ .. _Backer: https://opencollective.com/django-dynamic-preferences#backer Sponsors -------- Support us by becoming a sponsor. Your logo will show up here with a link to your website. `Become a sponsor`__. .. image:: https://opencollective.com/django-dynamic-preferences/sponsor/0/avatar.svg :target: https://opencollective.com/django-dynamic-preferences/sponsor/0/website __ Sponsor_ .. _Sponsor: https://opencollective.com/django-dynamic-preferences#sponsor ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696228.0 django_dynamic_preferences-1.17.0/README.rst0000644000175000017500000000653114413261644017614 0ustar00agateagate============================= django-dynamic-preferences ============================= .. image:: https://badge.fury.io/py/django-dynamic-preferences.png :target: https://badge.fury.io/py/django-dynamic-preferences .. image:: https://readthedocs.org/projects/django-dynamic-preferences/badge/?version=latest :target: http://django-dynamic-preferences.readthedocs.org/en/latest/ .. image:: https://github.com/agateblue/django-dynamic-preferences/actions/workflows/tests.yml/badge.svg :target: https://github.com/agateblue/django-dynamic-preferences/actions/workflows/tests.yml .. image:: https://opencollective.com/django-dynamic-preferences/backers/badge.svg :alt: Backers on Open Collective :target: #backers .. image:: https://opencollective.com/django-dynamic-preferences/sponsors/badge.svg :alt: Sponsors on Open Collective :target: #sponsors Dynamic-preferences is a Django app, BSD-licensed, designed to help you manage your project settings. While most of the time, a ``settings.py`` file is sufficient, there are some situations where you need something more flexible such as: * per-user settings (or, generally speaking, per instance settings) * settings change without server restart For per-instance settings, you could actually store them in some kind of profile model. However, it means that every time you want to add a new setting, you need to add a new column to the profile DB table. Not very efficient. Dynamic-preferences allow you to register settings (a.k.a. preferences) in a declarative way. Preferences values are serialized before storage in database, and automatically deserialized when you need them. With dynamic-preferences, you can update settings on the fly, through django's admin or custom forms, without restarting your application. The project is tested and work under Python 3.7, 3.8, 3.9, 3.10 and 3.11 and with django 3.2 and 4.2. Features -------- * Simple to setup * Admin integration * Forms integration * Bundled with global and per-user preferences * Can be extended to other models if need (e.g. per-site preferences) * Integrates with django caching mechanisms to improve performance Documentation ------------- The full documentation is at https://django-dynamic-preferences.readthedocs.org. Changelog --------- See https://django-dynamic-preferences.readthedocs.io/en/latest/history.html Contributing ------------ See https://django-dynamic-preferences.readthedocs.org/en/latest/contributing.html Credits +++++++ Contributors ------------ This project exists thanks to all the people who contribute! .. image:: https://opencollective.com/django-dynamic-preferences/contributors.svg?width=890&button=false Backers ------- Thank you to all our backers! `Become a backer`__. .. image:: https://opencollective.com/django-dynamic-preferences/backers.svg?width=890 :target: https://opencollective.com/django-dynamic-preferences#backers __ Backer_ .. _Backer: https://opencollective.com/django-dynamic-preferences#backer Sponsors -------- Support us by becoming a sponsor. Your logo will show up here with a link to your website. `Become a sponsor`__. .. image:: https://opencollective.com/django-dynamic-preferences/sponsor/0/avatar.svg :target: https://opencollective.com/django-dynamic-preferences/sponsor/0/website __ Sponsor_ .. _Sponsor: https://opencollective.com/django-dynamic-preferences#sponsor ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736552720.6985018 django_dynamic_preferences-1.17.0/django_dynamic_preferences.egg-info/0000775000175000017500000000000014740330421025134 5ustar00agateagate././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736552720.0 django_dynamic_preferences-1.17.0/django_dynamic_preferences.egg-info/PKG-INFO0000644000175000017500000001027714740330420026235 0ustar00agateagateMetadata-Version: 2.2 Name: django-dynamic-preferences Version: 1.17.0 Summary: Dynamic global and instance settings for your django project Home-page: https://github.com/agateblue/django-dynamic-preferences Author: Agate Blue Author-email: me+github@agate.blue License: BSD Keywords: django-dynamic-preferences Classifier: Development Status :: 5 - Production/Stable Classifier: Framework :: Django Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Natural Language :: English Classifier: Programming Language :: Python :: 3 License-File: LICENSE License-File: AUTHORS.rst Requires-Dist: django>=4.2 Requires-Dist: persisting_theory==1.0 Dynamic: author Dynamic: author-email Dynamic: classifier Dynamic: description Dynamic: home-page Dynamic: keywords Dynamic: license Dynamic: requires-dist Dynamic: summary ============================= django-dynamic-preferences ============================= .. image:: https://badge.fury.io/py/django-dynamic-preferences.png :target: https://badge.fury.io/py/django-dynamic-preferences .. image:: https://readthedocs.org/projects/django-dynamic-preferences/badge/?version=latest :target: http://django-dynamic-preferences.readthedocs.org/en/latest/ .. image:: https://github.com/agateblue/django-dynamic-preferences/actions/workflows/tests.yml/badge.svg :target: https://github.com/agateblue/django-dynamic-preferences/actions/workflows/tests.yml .. image:: https://opencollective.com/django-dynamic-preferences/backers/badge.svg :alt: Backers on Open Collective :target: #backers .. image:: https://opencollective.com/django-dynamic-preferences/sponsors/badge.svg :alt: Sponsors on Open Collective :target: #sponsors Dynamic-preferences is a Django app, BSD-licensed, designed to help you manage your project settings. While most of the time, a ``settings.py`` file is sufficient, there are some situations where you need something more flexible such as: * per-user settings (or, generally speaking, per instance settings) * settings change without server restart For per-instance settings, you could actually store them in some kind of profile model. However, it means that every time you want to add a new setting, you need to add a new column to the profile DB table. Not very efficient. Dynamic-preferences allow you to register settings (a.k.a. preferences) in a declarative way. Preferences values are serialized before storage in database, and automatically deserialized when you need them. With dynamic-preferences, you can update settings on the fly, through django's admin or custom forms, without restarting your application. The project is tested and work under Python 3.7, 3.8, 3.9, 3.10 and 3.11 and with django 3.2 and 4.2. Features -------- * Simple to setup * Admin integration * Forms integration * Bundled with global and per-user preferences * Can be extended to other models if need (e.g. per-site preferences) * Integrates with django caching mechanisms to improve performance Documentation ------------- The full documentation is at https://django-dynamic-preferences.readthedocs.org. Changelog --------- See https://django-dynamic-preferences.readthedocs.io/en/latest/history.html Contributing ------------ See https://django-dynamic-preferences.readthedocs.org/en/latest/contributing.html Credits +++++++ Contributors ------------ This project exists thanks to all the people who contribute! .. image:: https://opencollective.com/django-dynamic-preferences/contributors.svg?width=890&button=false Backers ------- Thank you to all our backers! `Become a backer`__. .. image:: https://opencollective.com/django-dynamic-preferences/backers.svg?width=890 :target: https://opencollective.com/django-dynamic-preferences#backers __ Backer_ .. _Backer: https://opencollective.com/django-dynamic-preferences#backer Sponsors -------- Support us by becoming a sponsor. Your logo will show up here with a link to your website. `Become a sponsor`__. .. image:: https://opencollective.com/django-dynamic-preferences/sponsor/0/avatar.svg :target: https://opencollective.com/django-dynamic-preferences/sponsor/0/website __ Sponsor_ .. _Sponsor: https://opencollective.com/django-dynamic-preferences#sponsor ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736552720.0 django_dynamic_preferences-1.17.0/django_dynamic_preferences.egg-info/SOURCES.txt0000644000175000017500000000605114740330420027017 0ustar00agateagateAUTHORS.rst CONTRIBUTING.rst HISTORY.rst LICENSE MANIFEST.in README.rst setup.cfg setup.py django_dynamic_preferences.egg-info/PKG-INFO django_dynamic_preferences.egg-info/SOURCES.txt django_dynamic_preferences.egg-info/dependency_links.txt django_dynamic_preferences.egg-info/not-zip-safe django_dynamic_preferences.egg-info/requires.txt django_dynamic_preferences.egg-info/top_level.txt dynamic_preferences/__init__.py dynamic_preferences/admin.py dynamic_preferences/apps.py dynamic_preferences/exceptions.py dynamic_preferences/forms.py dynamic_preferences/managers.py dynamic_preferences/models.py dynamic_preferences/preferences.py dynamic_preferences/processors.py dynamic_preferences/registries.py dynamic_preferences/serializers.py dynamic_preferences/settings.py dynamic_preferences/signals.py dynamic_preferences/types.py dynamic_preferences/urls.py dynamic_preferences/utils.py dynamic_preferences/views.py dynamic_preferences/api/__init__.py dynamic_preferences/api/serializers.py dynamic_preferences/api/viewsets.py dynamic_preferences/locale/ar/LC_MESSAGES/django.mo dynamic_preferences/locale/ar/LC_MESSAGES/django.po dynamic_preferences/locale/de/LC_MESSAGES/django.mo dynamic_preferences/locale/de/LC_MESSAGES/django.po dynamic_preferences/locale/fr/LC_MESSAGES/django.mo dynamic_preferences/locale/fr/LC_MESSAGES/django.po dynamic_preferences/locale/id/LC_MESSAGES/django.mo dynamic_preferences/locale/id/LC_MESSAGES/django.po dynamic_preferences/locale/pl/LC_MESSAGES/django.mo dynamic_preferences/locale/pl/LC_MESSAGES/django.po dynamic_preferences/management/__init__.py dynamic_preferences/management/commands/__init__.py dynamic_preferences/management/commands/checkpreferences.py dynamic_preferences/migrations/0001_initial.py dynamic_preferences/migrations/0002_auto_20150712_0332.py dynamic_preferences/migrations/0003_auto_20151223_1407.py dynamic_preferences/migrations/0004_move_user_model.py dynamic_preferences/migrations/0005_auto_20181120_0848.py dynamic_preferences/migrations/0006_auto_20191001_2236.py dynamic_preferences/migrations/__init__.py dynamic_preferences/templates/dynamic_preferences/base.html dynamic_preferences/templates/dynamic_preferences/form.html dynamic_preferences/templates/dynamic_preferences/sections.html dynamic_preferences/templates/dynamic_preferences/testcontext.html dynamic_preferences/users/__init__.py dynamic_preferences/users/admin.py dynamic_preferences/users/apps.py dynamic_preferences/users/forms.py dynamic_preferences/users/models.py dynamic_preferences/users/registries.py dynamic_preferences/users/serializers.py dynamic_preferences/users/urls.py dynamic_preferences/users/views.py dynamic_preferences/users/viewsets.py dynamic_preferences/users/migrations/0001_initial.py dynamic_preferences/users/migrations/0002_auto_20200821_0837.py dynamic_preferences/users/migrations/__init__.py tests/test_checkpreferences_command.py tests/test_global_preferences.py tests/test_manager.py tests/test_preferences.py tests/test_rest_framework.py tests/test_serializers.py tests/test_tutorial.py tests/test_types.py tests/test_user_preferences.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736552720.0 django_dynamic_preferences-1.17.0/django_dynamic_preferences.egg-info/dependency_links.txt0000644000175000017500000000000114740330420031177 0ustar00agateagate ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1593767263.0 django_dynamic_preferences-1.17.0/django_dynamic_preferences.egg-info/not-zip-safe0000644000175000017500000000000113677572537027410 0ustar00agateagate ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736552720.0 django_dynamic_preferences-1.17.0/django_dynamic_preferences.egg-info/requires.txt0000644000175000017500000000004314740330420027526 0ustar00agateagatedjango>=4.2 persisting_theory==1.0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736552720.0 django_dynamic_preferences-1.17.0/django_dynamic_preferences.egg-info/top_level.txt0000644000175000017500000000002414740330420027657 0ustar00agateagatedynamic_preferences ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736552720.6915016 django_dynamic_preferences-1.17.0/dynamic_preferences/0000775000175000017500000000000014740330421022120 5ustar00agateagate././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736552656.0 django_dynamic_preferences-1.17.0/dynamic_preferences/__init__.py0000644000175000017500000000014014740330320024220 0ustar00agateagate__version__ = "1.17.0" default_app_config = "dynamic_preferences.apps.DynamicPreferencesConfig" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696213.0 django_dynamic_preferences-1.17.0/dynamic_preferences/admin.py0000644000175000017500000000724014413261625023571 0ustar00agateagatefrom django.contrib import admin from django import forms from .settings import preferences_settings from .registries import global_preferences_registry from .models import GlobalPreferenceModel from .forms import GlobalSinglePreferenceForm, SinglePerInstancePreferenceForm from django.utils.translation import gettext_lazy as _ class SectionFilter(admin.AllValuesFieldListFilter): def __init__(self, field, request, params, model, model_admin, field_path): super(SectionFilter, self).__init__( field, request, params, model, model_admin, field_path ) parent_model, reverse_path = admin.utils.reverse_field_path(model, field_path) if model == parent_model: queryset = model_admin.get_queryset(request) else: queryset = parent_model._default_manager.all() self.registries = [] registry_name_set = set() for preferenceModel in queryset.distinct(): l = len(registry_name_set) registry_name_set.add(preferenceModel.registry.__class__.__name__) if len(registry_name_set) != l: self.registries.append(preferenceModel.registry) def choices(self, changelist): choices = super(SectionFilter, self).choices(changelist) for choice in choices: display = choice["display"] try: for registry in self.registries: display = registry.section_objects[display].verbose_name choice["display"] = display except (KeyError): pass yield choice class DynamicPreferenceAdmin(admin.ModelAdmin): list_display = ( "verbose_name", "name", "section_name", "help_text", "raw_value", "default_value", ) fields = ("raw_value", "default_value", "name", "section_name") readonly_fields = ("name", "section_name", "default_value") if preferences_settings.ADMIN_ENABLE_CHANGELIST_FORM: list_editable = ("raw_value",) search_fields = ["name", "section", "raw_value"] list_filter = (("section", SectionFilter),) if preferences_settings.ADMIN_ENABLE_CHANGELIST_FORM: def get_changelist_form(self, request, **kwargs): return self.changelist_form def default_value(self, obj): return obj.preference.default default_value.short_description = _("Default Value") def section_name(self, obj): try: return obj.registry.section_objects[obj.section].verbose_name except KeyError: pass return obj.section section_name.short_description = _("Section Name") def save_model(self, request, obj, form, change): pref = form.instance manager = pref.registry.manager(instance=getattr(obj, "instance", None)) manager.update_db_pref(pref.section, pref.name, form.cleaned_data["raw_value"]) class GlobalPreferenceAdmin(DynamicPreferenceAdmin): form = GlobalSinglePreferenceForm changelist_form = GlobalSinglePreferenceForm def get_queryset(self, *args, **kwargs): # Instanciate default prefs manager = global_preferences_registry.manager() manager.all() return super(GlobalPreferenceAdmin, self).get_queryset(*args, **kwargs) admin.site.register(GlobalPreferenceModel, GlobalPreferenceAdmin) class PerInstancePreferenceAdmin(DynamicPreferenceAdmin): list_display = ("instance",) + DynamicPreferenceAdmin.list_display fields = ("instance",) + DynamicPreferenceAdmin.fields raw_id_fields = ("instance",) form = SinglePerInstancePreferenceForm changelist_form = SinglePerInstancePreferenceForm list_select_related = True ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736552720.6925018 django_dynamic_preferences-1.17.0/dynamic_preferences/api/0000775000175000017500000000000014740330421022671 5ustar00agateagate././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1593767263.0 django_dynamic_preferences-1.17.0/dynamic_preferences/api/__init__.py0000644000175000017500000000000013677572537025016 0ustar00agateagate././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736550780.0 django_dynamic_preferences-1.17.0/dynamic_preferences/api/serializers.py0000664000175000017500000000437614740324574025625 0ustar00agateagatefrom rest_framework import serializers from dynamic_preferences.signals import preference_updated class PreferenceValueField(serializers.Field): def get_attribute(self, o): return o def to_representation(self, o): return o.preference.api_repr(o.value) def to_internal_value(self, data): return data class PreferenceSerializer(serializers.Serializer): section = serializers.CharField(read_only=True) name = serializers.CharField(read_only=True) identifier = serializers.SerializerMethodField() default = serializers.SerializerMethodField() value = PreferenceValueField() verbose_name = serializers.SerializerMethodField() help_text = serializers.SerializerMethodField() additional_data = serializers.SerializerMethodField() field = serializers.SerializerMethodField() class Meta: fields = [ "default", "value", "verbose_name", "help_text", ] def get_default(self, o): return o.preference.api_repr(o.preference.get("default")) def get_verbose_name(self, o): return o.preference.get("verbose_name") def get_identifier(self, o): return o.preference.identifier() def get_help_text(self, o): return o.preference.get("help_text") def get_additional_data(self, o): return o.preference.get_api_additional_data() def get_field(self, o): return o.preference.get_api_field_data() def validate_value(self, value): """ We call validation from the underlying form field """ field = self.instance.preference.setup_field() value = field.to_python(value) field.validate(value) field.run_validators(value) return value def update(self, instance, validated_data): old_value = instance.value instance.value = validated_data["value"] instance.save() preference_updated.send( sender=self.__class__, section=instance.section, name=instance.name, old_value=old_value, new_value=validated_data["value"], instance=instance ) return instance class GlobalPreferenceSerializer(PreferenceSerializer): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736550780.0 django_dynamic_preferences-1.17.0/dynamic_preferences/api/viewsets.py0000664000175000017500000001337114740324574025135 0ustar00agateagatefrom django.db import transaction from django.db.models import Q from rest_framework import mixins from rest_framework import viewsets from rest_framework import permissions from rest_framework.response import Response from rest_framework.decorators import action from rest_framework.generics import get_object_or_404 from dynamic_preferences import models from dynamic_preferences import exceptions from dynamic_preferences.settings import preferences_settings from . import serializers class PreferenceViewSet( mixins.UpdateModelMixin, mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet, ): """ - list preferences - detail given preference - batch update preferences - update a single preference """ def get_queryset(self): """ We just ensure preferences are actually populated before fetching from db """ self.init_preferences() queryset = super(PreferenceViewSet, self).get_queryset() section = self.request.query_params.get("section") if section: queryset = queryset.filter(section=section) return queryset def get_manager(self): return self.queryset.model.registry.manager() def init_preferences(self): manager = self.get_manager() manager.all() def get_object(self): """ Returns the object the view is displaying. You may want to override this if you need to provide non-standard queryset lookups. Eg if objects are referenced using multiple keyword arguments in the url conf. """ queryset = self.filter_queryset(self.get_queryset()) lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field identifier = self.kwargs[lookup_url_kwarg] section, name = self.get_section_and_name(identifier) filter_kwargs = {"section": section, "name": name} obj = get_object_or_404(queryset, **filter_kwargs) # May raise a permission denied self.check_object_permissions(self.request, obj) return obj def get_section_and_name(self, identifier): try: section, name = identifier.split(preferences_settings.SECTION_KEY_SEPARATOR) except ValueError: # no section given section, name = None, identifier return section, name @action(detail=False, methods=["post"]) @transaction.atomic def bulk(self, request, *args, **kwargs): """ Update multiple preferences at once this is a long method because we ensure everything is valid before actually persisting the changes """ manager = self.get_manager() errors = {} preferences = [] payload = request.data # first, we check updated preferences actually exists in the registry try: for identifier, value in payload.items(): try: preferences.append(self.queryset.model.registry.get(identifier)) except exceptions.NotFoundInRegistry: errors[identifier] = "invalid preference" except (TypeError, AttributeError): return Response("invalid payload", status=400) if errors: return Response(errors, status=400) # now, we generate an optimized Q objects to retrieve all matching # preferences at once from database queries = [Q(section=p.section.name, name=p.name) for p in preferences] try: query = queries[0] except IndexError: return Response("empty payload", status=400) for q in queries[1:]: query |= q preferences_qs = self.get_queryset().filter(query) # next, we generate a serializer for each database preference serializer_objects = [] for p in preferences_qs: s = self.get_serializer_class()( p, data={"value": payload[p.preference.identifier()]} ) serializer_objects.append(s) validation_errors = {} # we check if any serializer is invalid for s in serializer_objects: if s.is_valid(): continue validation_errors[s.instance.preference.identifier()] = s.errors if validation_errors: return Response(validation_errors, status=400) for s in serializer_objects: s.save() return Response( [s.data for s in serializer_objects], status=200, ) class GlobalPreferencePermission(permissions.DjangoModelPermissions): perms_map = { "GET": ["%(app_label)s.change_%(model_name)s"], "OPTIONS": ["%(app_label)s.change_%(model_name)s"], "HEAD": ["%(app_label)s.change_%(model_name)s"], "POST": ["%(app_label)s.change_%(model_name)s"], "PUT": ["%(app_label)s.change_%(model_name)s"], "PATCH": ["%(app_label)s.change_%(model_name)s"], "DELETE": ["%(app_label)s.change_%(model_name)s"], } class GlobalPreferencesViewSet(PreferenceViewSet): queryset = models.GlobalPreferenceModel.objects.all() serializer_class = serializers.GlobalPreferenceSerializer permission_classes = [GlobalPreferencePermission] class PerInstancePreferenceViewSet(PreferenceViewSet): def get_manager(self): return self.queryset.model.registry.manager( instance=self.get_related_instance() ) def get_queryset(self): return ( super(PerInstancePreferenceViewSet, self) .get_queryset() .filter(instance=self.get_related_instance()) ) def get_related_instance(self): """ Override this to the instance bound to the preferences """ raise NotImplementedError ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/dynamic_preferences/apps.py0000644000175000017500000000165014413261622023440 0ustar00agateagatefrom django.apps import AppConfig, apps from django.conf import settings from django.utils.translation import gettext_lazy as _ from .registries import preference_models, global_preferences_registry from .settings import preferences_settings class DynamicPreferencesConfig(AppConfig): name = "dynamic_preferences" verbose_name = _("Dynamic Preferences") default_auto_field = "django.db.models.AutoField" def ready(self): if preferences_settings.ENABLE_GLOBAL_MODEL_AUTO_REGISTRATION: GlobalPreferenceModel = self.get_model("GlobalPreferenceModel") preference_models.register( GlobalPreferenceModel, global_preferences_registry ) # This will load all dynamic_preferences_registry.py files under # installed apps app_names = [app.name for app in apps.app_configs.values()] global_preferences_registry.autodiscover(app_names) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/dynamic_preferences/exceptions.py0000644000175000017500000000205714413261622024660 0ustar00agateagateclass DynamicPreferencesException(Exception): detail_default = "An exception occurred with django-dynamic-preferences" def __init__(self, detail=None): if detail is not None: self.detail = str(detail) else: self.detail = str(self.detail_default) def __str__(self): return self.detail class MissingDefault(DynamicPreferencesException): detail_default = "You must provide a default value for all preferences" class NotFoundInRegistry(DynamicPreferencesException, KeyError): detail_default = "Preference with this name/section not found in registry" class DoesNotExist(DynamicPreferencesException): detail_default = "Cannot retrieve preference value, ensure the preference is correctly registered and database is synced" class CachedValueNotFound(DynamicPreferencesException): detail_default = "Cached value not found" class MissingModel(DynamicPreferencesException): detail_default = 'You must define a model choice through "model" \ or "queryset" attribute' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736550780.0 django_dynamic_preferences-1.17.0/dynamic_preferences/forms.py0000664000175000017500000001204014740324574023631 0ustar00agateagatefrom django import forms from django.core.exceptions import ValidationError from collections import OrderedDict from .registries import global_preferences_registry from .models import GlobalPreferenceModel from .exceptions import NotFoundInRegistry class AbstractSinglePreferenceForm(forms.ModelForm): class Meta: fields = ("section", "name", "raw_value") def __init__(self, *args, **kwargs): self.instance = kwargs.get("instance") initial = {} if self.instance: initial["raw_value"] = self.instance.value kwargs["initial"] = initial super(AbstractSinglePreferenceForm, self).__init__(*args, **kwargs) if self.instance.name: self.fields["raw_value"] = self.instance.preference.setup_field() def clean(self): cleaned_data = super(AbstractSinglePreferenceForm, self).clean() try: self.instance.name, self.instance.section = ( cleaned_data["name"], cleaned_data["section"], ) except KeyError: # changelist form pass try: self.instance.preference except NotFoundInRegistry: raise ValidationError(NotFoundInRegistry.detail_default) return self.cleaned_data def save(self, *args, **kwargs): self.instance.value = self.cleaned_data["raw_value"] return super(AbstractSinglePreferenceForm, self).save(*args, **kwargs) class SinglePerInstancePreferenceForm(AbstractSinglePreferenceForm): class Meta: fields = ("instance",) + AbstractSinglePreferenceForm.Meta.fields def clean(self): cleaned_data = super(AbstractSinglePreferenceForm, self).clean() try: self.instance.name, self.instance.section = ( cleaned_data["name"], cleaned_data["section"], ) except KeyError: # changelist form pass i = cleaned_data.get("instance") if i: self.instance.instance = i try: self.instance.preference except NotFoundInRegistry: raise ValidationError(NotFoundInRegistry.detail_default) return self.cleaned_data class GlobalSinglePreferenceForm(AbstractSinglePreferenceForm): class Meta: model = GlobalPreferenceModel fields = AbstractSinglePreferenceForm.Meta.fields def preference_form_builder(form_base_class, preferences=[], **kwargs): """ Return a form class for updating preferences :param form_base_class: a Form class used as the base. Must have a ``registry` attribute :param preferences: a list of :py:class: :param section: a section where the form builder will load preferences """ registry = form_base_class.registry preferences_obj = [] if len(preferences) > 0: # Preferences have been selected explicitly for pref in preferences: if isinstance(pref, str): preferences_obj.append(registry.get(name=pref)) elif type(pref) == tuple: preferences_obj.append(registry.get(name=pref[0], section=pref[1])) else: raise NotImplementedError( "The data you provide can't be converted to a Preference object" ) elif kwargs.get("section", None): # Try to use section param preferences_obj = registry.preferences(section=kwargs.get("section", None)) else: # display all preferences in the form preferences_obj = registry.preferences() fields = OrderedDict() instances = [] if "model" in kwargs: # backward compat, see #212 manager_kwargs = kwargs.get("model") else: manager_kwargs = {"instance": kwargs.get("instance", None)} manager = registry.manager(**manager_kwargs) for preference in preferences_obj: f = preference.field instance = manager.get_db_pref( section=preference.section.name, name=preference.name ) f.initial = instance.value fields[preference.identifier()] = f instances.append(instance) form_class = type("Custom" + form_base_class.__name__, (form_base_class,), {}) form_class.base_fields = fields form_class.preferences = preferences_obj form_class.instances = instances form_class.manager = manager return form_class def global_preference_form_builder(preferences=[], **kwargs): """ A shortcut :py:func:`preference_form_builder(GlobalPreferenceForm, preferences, **kwargs)` """ return preference_form_builder(GlobalPreferenceForm, preferences, **kwargs) class PreferenceForm(forms.Form): registry = None def update_preferences(self, **kwargs): for instance in self.instances: self.manager.update_db_pref( instance.preference.section.name, instance.preference.name, self.cleaned_data[instance.preference.identifier()], ) class GlobalPreferenceForm(PreferenceForm): registry = global_preferences_registry ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736552720.6865017 django_dynamic_preferences-1.17.0/dynamic_preferences/locale/0000775000175000017500000000000014740330421023357 5ustar00agateagate././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736552720.6855016 django_dynamic_preferences-1.17.0/dynamic_preferences/locale/ar/0000775000175000017500000000000014740330421023761 5ustar00agateagate././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736552720.6925018 django_dynamic_preferences-1.17.0/dynamic_preferences/locale/ar/LC_MESSAGES/0000775000175000017500000000000014740330421025546 5ustar00agateagate././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1593767263.0 django_dynamic_preferences-1.17.0/dynamic_preferences/locale/ar/LC_MESSAGES/django.mo0000644000175000017500000000203413677572537027372 0ustar00agateagateÞ• tÌ 3E Xb g q~ …ž’!1)S}—± ÇÒî     Default ValueDynamic PreferencesGlobal preferenceGlobal preferencesHelp TextNameRaw ValueSection NameSubmitVerbose NameProject-Id-Version: Report-Msgid-Bugs-To: POT-Creation-Date: 2018-11-08 10:37+0100 PO-Revision-Date: 2018-11-09 17:15+0100 Last-Translator: Language-Team: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5; Language: ar X-Generator: Poedit 2.1.1 القيمة Ø§Ù„Ø§ÙØªØ±Ø§Ø¶ÙŠØ©Ø§Ù„ØªÙØ¶ÙŠÙ„ات Ø§Ù„Ø¯ÙŠÙ†Ø§Ù…ÙŠÙƒÙŠØ©Ø§Ù„ØªÙØ¶ÙŠÙ„ Ø§Ù„Ø¹Ø§Ù…Ø§Ù„ØªÙØ¶ÙŠÙ„ العامنص المساعدةالاسمالقيمة الأوليةإسم القسمإرسالاسم مطول././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1593767263.0 django_dynamic_preferences-1.17.0/dynamic_preferences/locale/ar/LC_MESSAGES/django.po0000644000175000017500000000262113677572537027377 0ustar00agateagate# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-11-08 10:37+0100\n" "PO-Revision-Date: 2018-11-09 17:15+0100\n" "Last-Translator: \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " "&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" "Language: ar\n" "X-Generator: Poedit 2.1.1\n" #: .\admin.py:56 msgid "Default Value" msgstr "القيمة Ø§Ù„Ø§ÙØªØ±Ø§Ø¶ÙŠØ©" #: .\admin.py:65 msgid "Section Name" msgstr "إسم القسم" #: .\apps.py:9 msgid "Dynamic Preferences" msgstr "Ø§Ù„ØªÙØ¶ÙŠÙ„ات الديناميكية" #: .\models.py:25 msgid "Name" msgstr "الاسم" #: .\models.py:28 msgid "Raw Value" msgstr "القيمة الأولية" #: .\models.py:42 msgid "Verbose Name" msgstr "اسم مطول" #: .\models.py:47 msgid "Help Text" msgstr "نص المساعدة" #: .\models.py:84 msgid "Global preference" msgstr "Ø§Ù„ØªÙØ¶ÙŠÙ„ العام" #: .\models.py:85 msgid "Global preferences" msgstr "Ø§Ù„ØªÙØ¶ÙŠÙ„ العام" #: .\templates\dynamic_preferences\form.html:11 msgid "Submit" msgstr "إرسال" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736552720.6855016 django_dynamic_preferences-1.17.0/dynamic_preferences/locale/de/0000775000175000017500000000000014740330421023747 5ustar00agateagate././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736552720.6925018 django_dynamic_preferences-1.17.0/dynamic_preferences/locale/de/LC_MESSAGES/0000775000175000017500000000000014740330421025534 5ustar00agateagate././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1593767263.0 django_dynamic_preferences-1.17.0/dynamic_preferences/locale/de/LC_MESSAGES/django.mo0000644000175000017500000000202213677572537027355 0ustar00agateagateÞ•ŒüH IWk} šŸ ³ ½Ê ÑÞîJÿ JWp„ š¤©Â ÇÑ Úæû  Default ValueDynamic PreferencesGlobal preferenceGlobal preferencesHelp TextNamePreferences - UsersRaw ValueSection NameSubmitVerbose Nameuser preferenceuser preferencesProject-Id-Version: Report-Msgid-Bugs-To: POT-Creation-Date: 2019-04-15 13:22+0200 PO-Revision-Date: 2018-11-09 17:14+0100 Last-Translator: Language-Team: Language: fr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n > 1); X-Generator: Poedit 2.1.1 StandardwertDynamische EinstellungenGlobale EinstellungGlobale EinstellungenHilfetextNameEinstellungen - BenutzerWertAbschnittAbsendenBezeichnungBenutzer EinstellungBenutzer Einstellungen././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1593767263.0 django_dynamic_preferences-1.17.0/dynamic_preferences/locale/de/LC_MESSAGES/django.po0000644000175000017500000000272713677572537027374 0ustar00agateagate# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-04-15 13:48+0200\n" "PO-Revision-Date: 2018-11-09 17:14+0100\n" "Last-Translator: \n" "Language-Team: \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" "X-Generator: Poedit 2.1.1\n" #: .\admin.py:56 msgid "Default Value" msgstr "Standardwert" #: .\admin.py:65 .\models.py:22 msgid "Section Name" msgstr "Abschnitt" #: .\apps.py:9 msgid "Dynamic Preferences" msgstr "Dynamische Einstellungen" #: .\models.py:25 msgid "Name" msgstr "Name" #: .\models.py:28 msgid "Raw Value" msgstr "Wert" #: .\models.py:42 msgid "Verbose Name" msgstr "Bezeichnung" #: .\models.py:47 msgid "Help Text" msgstr "Hilfetext" #: .\models.py:84 msgid "Global preference" msgstr "Globale Einstellung" #: .\models.py:85 msgid "Global preferences" msgstr "Globale Einstellungen" #: .\templates\dynamic_preferences\form.html:11 msgid "Submit" msgstr "Absenden" #: .\users\apps.py:11 msgid "Preferences - Users" msgstr "Einstellungen - Benutzer" #: .\users\models.py:15 msgid "user preference" msgstr "Benutzer Einstellung" #: .\users\models.py:16 msgid "user preferences" msgstr "Benutzer Einstellungen" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736552720.6855016 django_dynamic_preferences-1.17.0/dynamic_preferences/locale/fr/0000775000175000017500000000000014740330421023766 5ustar00agateagate././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736552720.6925018 django_dynamic_preferences-1.17.0/dynamic_preferences/locale/fr/LC_MESSAGES/0000775000175000017500000000000014740330421025553 5ustar00agateagate././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1593767263.0 django_dynamic_preferences-1.17.0/dynamic_preferences/locale/fr/LC_MESSAGES/django.mo0000644000175000017500000000157213677572537027405 0ustar00agateagateÞ• tÌ 3E Xb g q~ …J’Ýð  5B FQck   Default ValueDynamic PreferencesGlobal preferenceGlobal preferencesHelp TextNameRaw ValueSection NameSubmitVerbose NameProject-Id-Version: Report-Msgid-Bugs-To: POT-Creation-Date: 2018-11-08 10:37+0100 PO-Revision-Date: 2018-11-09 17:14+0100 Last-Translator: Language-Team: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n > 1); Language: fr X-Generator: Poedit 2.1.1 Valeur par défautPréférences dynamiquesPréférence globalePréférences globalesTexte d'AideNomValeur RAWNom de la SectionValiderNom détaillé././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1593767263.0 django_dynamic_preferences-1.17.0/dynamic_preferences/locale/fr/LC_MESSAGES/django.po0000644000175000017500000000235413677572537027407 0ustar00agateagate# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-11-08 10:37+0100\n" "PO-Revision-Date: 2018-11-09 17:14+0100\n" "Last-Translator: \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" "Language: fr\n" "X-Generator: Poedit 2.1.1\n" #: .\admin.py:56 msgid "Default Value" msgstr "Valeur par défaut" #: .\admin.py:65 msgid "Section Name" msgstr "Nom de la Section" #: .\apps.py:9 msgid "Dynamic Preferences" msgstr "Préférences dynamiques" #: .\models.py:25 msgid "Name" msgstr "Nom" #: .\models.py:28 msgid "Raw Value" msgstr "Valeur RAW" #: .\models.py:42 msgid "Verbose Name" msgstr "Nom détaillé" #: .\models.py:47 msgid "Help Text" msgstr "Texte d'Aide" #: .\models.py:84 msgid "Global preference" msgstr "Préférence globale" #: .\models.py:85 msgid "Global preferences" msgstr "Préférences globales" #: .\templates\dynamic_preferences\form.html:11 msgid "Submit" msgstr "Valider" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736552720.6865017 django_dynamic_preferences-1.17.0/dynamic_preferences/locale/id/0000775000175000017500000000000014740330421023753 5ustar00agateagate././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736552720.6935017 django_dynamic_preferences-1.17.0/dynamic_preferences/locale/id/LC_MESSAGES/0000775000175000017500000000000014740330421025540 5ustar00agateagate././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1697360004.0 django_dynamic_preferences-1.17.0/dynamic_preferences/locale/id/LC_MESSAGES/django.mo0000644000175000017500000000202414512724204027336 0ustar00agateagateÞ•ŒüH IWk} šŸ ³ ½Ê ÑÞîSÿ Sat† ˜¥ª À ÍÙ ßì  Default ValueDynamic PreferencesGlobal preferenceGlobal preferencesHelp TextNamePreferences - UsersRaw ValueSection NameSubmitVerbose Nameuser preferenceuser preferencesProject-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: POT-Creation-Date: 2023-07-19 20:44+0800 PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE Last-Translator: Kira Language-Team: Language: id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=1; plural=0; Nilai DefaultPreferensi DinamisPreferensi globalPreferensi globalTeks BantuanNamaPreferensi - PenggunaNilai MentahNama BagianKirimNama Verbosepreferensi penggunapreferensi pengguna././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1697360004.0 django_dynamic_preferences-1.17.0/dynamic_preferences/locale/id/LC_MESSAGES/django.po0000644000175000017500000000336514512724204027352 0ustar00agateagate# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # Kira , 2023. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-07-19 20:44+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Kira \n" "Language-Team: \n" "Language: id\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" #: .\dynamic_preferences\admin.py:66 msgid "Default Value" msgstr "Nilai Default" #: .\dynamic_preferences\admin.py:75 .\dynamic_preferences\models.py:30 msgid "Section Name" msgstr "Nama Bagian" #: .\dynamic_preferences\apps.py:10 msgid "Dynamic Preferences" msgstr "Preferensi Dinamis" #: .\dynamic_preferences\models.py:34 msgid "Name" msgstr "Nama" #: .\dynamic_preferences\models.py:37 msgid "Raw Value" msgstr "Nilai Mentah" #: .\dynamic_preferences\models.py:51 msgid "Verbose Name" msgstr "Nama Verbose" #: .\dynamic_preferences\models.py:57 msgid "Help Text" msgstr "Teks Bantuan" #: .\dynamic_preferences\models.py:94 msgid "Global preference" msgstr "Preferensi global" #: .\dynamic_preferences\models.py:95 msgid "Global preferences" msgstr "Preferensi global" #: .\dynamic_preferences\templates\dynamic_preferences\form.html:11 msgid "Submit" msgstr "Kirim" #: .\dynamic_preferences\users\apps.py:11 msgid "Preferences - Users" msgstr "Preferensi - Pengguna" #: .\dynamic_preferences\users\models.py:14 msgid "user preference" msgstr "preferensi pengguna" #: .\dynamic_preferences\users\models.py:15 msgid "user preferences" msgstr "preferensi pengguna" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736552720.6865017 django_dynamic_preferences-1.17.0/dynamic_preferences/locale/pl/0000775000175000017500000000000014740330421023772 5ustar00agateagate././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736552720.6935017 django_dynamic_preferences-1.17.0/dynamic_preferences/locale/pl/LC_MESSAGES/0000775000175000017500000000000014740330421025557 5ustar00agateagate././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/dynamic_preferences/locale/pl/LC_MESSAGES/django.mo0000644000175000017500000000202014413261622027351 0ustar00agateagateÞ•ŒüH IWk} šŸ ³ ½Ê ÑÞî!ÿ!5La vƒ‰¤ µÂÊÞ÷  Default ValueDynamic PreferencesGlobal preferenceGlobal preferencesHelp TextNamePreferences - UsersRaw ValueSection NameSubmitVerbose Nameuser preferenceuser preferencesProject-Id-Version: Report-Msgid-Bugs-To: PO-Revision-Date: 2020-09-23 23:59+0200 Last-Translator: Language-Team: Language: pl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n > 1); X-Generator: Poedit 2.4.1 Wartość domyÅ›lnaDynamiczne PreferencjeGlobalna preferencjaGlobalne PreferencjeTekst pomocyNazwaPreferencje - UżytkownicySurowa wartośćNazwa sekcjiWyÅ›lijNazwa Szczegółowapreferencja użytkownikapreferencje użytkownika././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/dynamic_preferences/locale/pl/LC_MESSAGES/django.po0000644000175000017500000000277614413261622027376 0ustar00agateagate# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-04-15 13:48+0200\n" "PO-Revision-Date: 2020-09-23 23:59+0200\n" "Last-Translator: \n" "Language-Team: \n" "Language: pl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" "X-Generator: Poedit 2.4.1\n" #: .\admin.py:56 msgid "Default Value" msgstr "Wartość domyÅ›lna" #: .\admin.py:65 .\models.py:22 msgid "Section Name" msgstr "Nazwa sekcji" #: .\apps.py:9 msgid "Dynamic Preferences" msgstr "Dynamiczne Preferencje" #: .\models.py:25 msgid "Name" msgstr "Nazwa" #: .\models.py:28 msgid "Raw Value" msgstr "Surowa wartość" #: .\models.py:42 msgid "Verbose Name" msgstr "Nazwa Szczegółowa" #: .\models.py:47 msgid "Help Text" msgstr "Tekst pomocy" #: .\models.py:84 msgid "Global preference" msgstr "Globalna preferencja" #: .\models.py:85 msgid "Global preferences" msgstr "Globalne Preferencje" #: .\templates\dynamic_preferences\form.html:11 msgid "Submit" msgstr "WyÅ›lij" #: .\users\apps.py:11 msgid "Preferences - Users" msgstr "Preferencje - Użytkownicy" #: .\users\models.py:15 msgid "user preference" msgstr "preferencja użytkownika" #: .\users\models.py:16 msgid "user preferences" msgstr "preferencje użytkownika" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736552720.6935017 django_dynamic_preferences-1.17.0/dynamic_preferences/management/0000775000175000017500000000000014740330421024234 5ustar00agateagate././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/dynamic_preferences/management/__init__.py0000644000175000017500000000003114413261622026340 0ustar00agateagate__author__ = "agateblue" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736552720.6935017 django_dynamic_preferences-1.17.0/dynamic_preferences/management/commands/0000775000175000017500000000000014740330421026035 5ustar00agateagate././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/dynamic_preferences/management/commands/__init__.py0000644000175000017500000000003114413261622030141 0ustar00agateagate__author__ = "agateblue" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/dynamic_preferences/management/commands/checkpreferences.py0000644000175000017500000000521614413261622031713 0ustar00agateagatefrom django.core.management.base import BaseCommand from dynamic_preferences.exceptions import NotFoundInRegistry from dynamic_preferences.models import GlobalPreferenceModel from dynamic_preferences.registries import ( global_preferences_registry, preference_models, ) from dynamic_preferences.settings import preferences_settings def delete_preferences(queryset): """ Delete preferences objects if they are not present in registry. Return a list of deleted objects """ deleted = [] # Iterate through preferences. If an error is raised when accessing # preference object, just delete it for p in queryset: try: p.registry.get(section=p.section, name=p.name, fallback=False) except NotFoundInRegistry: p.delete() deleted.append(p) return deleted class Command(BaseCommand): help = ( "Find and delete preferences from database if they don't exist in " "registries. Create preferences that are not present in database" "(except when invoked with --skip_create)." ) def add_arguments(self, parser): parser.add_argument( "--skip_create", action="store_true", help="Forces to skip the creation step for missing preferences", ) def handle(self, *args, **options): skip_create = options["skip_create"] # Create needed preferences # Global if not skip_create: self.stdout.write("Creating missing global preferences...") manager = global_preferences_registry.manager() manager.all() deleted = delete_preferences(GlobalPreferenceModel.objects.all()) message = "Deleted {deleted} global preferences".format(deleted=len(deleted)) self.stdout.write(message) for preference_model, registry in preference_models.items(): deleted = delete_preferences(preference_model.objects.all()) message = "Deleted {deleted} {model} preferences".format( deleted=len(deleted), model=preference_model.__name__, ) self.stdout.write(message) if not hasattr(preference_model, "get_instance_model"): continue if skip_create: continue message = "Creating missing preferences for {model} model...".format( model=preference_model.get_instance_model().__name__, ) self.stdout.write(message) for instance in preference_model.get_instance_model().objects.all(): getattr(instance, preferences_settings.MANAGER_ATTRIBUTE).all() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736550780.0 django_dynamic_preferences-1.17.0/dynamic_preferences/managers.py0000664000175000017500000002044414740324574024307 0ustar00agateagatetry: from collections.abc import Mapping except ImportError: from collections import Mapping from .settings import preferences_settings from .exceptions import CachedValueNotFound, DoesNotExist from .signals import preference_updated class PreferencesManager(Mapping): """Handle retrieving / caching of preferences""" def __init__(self, model, registry, **kwargs): self.model = model self.registry = registry self.instance = kwargs.get("instance") @property def queryset(self): qs = self.model.objects.all() if self.instance: qs = qs.filter(instance=self.instance) return qs @property def cache(self): from django.core.cache import caches return caches[preferences_settings.CACHE_NAME] def __getitem__(self, key): return self.get(key) def __setitem__(self, key, value): section, name = self.parse_lookup(key) preference = self.registry.get(section=section, name=name, fallback=False) preference.validate(value) self.update_db_pref(section=section, name=name, value=value) def __repr__(self): return repr(self.all()) def __iter__(self): return self.all().__iter__() def __len__(self): return len(self.all()) def by_name(self): """Return a dictionary with preferences identifiers and values, but without the section name in the identifier""" return { key.split(preferences_settings.SECTION_KEY_SEPARATOR)[-1]: value for key, value in self.all().items() } def get_by_name(self, name): return self.get(self.registry.get_by_name(name).identifier()) def get_cache_key(self, section, name): """Return the cache key corresponding to a given preference""" if not self.instance: return "dynamic_preferences_{0}_{1}_{2}".format( self.model.__name__, section, name ) return "dynamic_preferences_{0}_{1}_{2}_{3}".format( self.model.__name__, self.instance.pk, section, name, self.instance.pk ) def from_cache(self, section, name): """Return a preference raw_value from cache""" cached_value = self.cache.get( self.get_cache_key(section, name), CachedValueNotFound ) if cached_value is CachedValueNotFound: raise CachedValueNotFound if cached_value == preferences_settings.CACHE_NONE_VALUE: cached_value = None return self.registry.get(section=section, name=name).serializer.deserialize( cached_value ) def many_from_cache(self, preferences): """ Return cached value for given preferences missing preferences will be skipped """ keys = {p: self.get_cache_key(p.section.name, p.name) for p in preferences} cached = self.cache.get_many(list(keys.values())) for k, v in cached.items(): # we replace dummy cached values by None here, if needed if v == preferences_settings.CACHE_NONE_VALUE: cached[k] = None # we have to remap returned value since the underlying cached keys # are not usable for an end user return { p.identifier(): p.serializer.deserialize(cached[k]) for p, k in keys.items() if k in cached } def to_cache(self, *prefs): """ Update/create the cache value for the given preference model instances """ update_dict = {} for pref in prefs: key = self.get_cache_key(pref.section, pref.name) value = pref.raw_value if value is None or value == "": # some cache backends refuse to cache None or empty values # resulting in more DB queries, so we cache an arbitrary value # to ensure the cache is hot (even with empty values) value = preferences_settings.CACHE_NONE_VALUE update_dict[key] = value self.cache.set_many(update_dict) def pref_obj(self, section, name): return self.registry.get(section=section, name=name) def parse_lookup(self, lookup): try: section, name = lookup.split(preferences_settings.SECTION_KEY_SEPARATOR) except ValueError: name = lookup section = None return section, name def get(self, key, no_cache=False): """Return the value of a single preference using a dotted path key :arg no_cache: if true, the cache is bypassed """ section, name = self.parse_lookup(key) preference = self.registry.get(section=section, name=name, fallback=False) if no_cache or not preferences_settings.ENABLE_CACHE: return self.get_db_pref(section=section, name=name).value try: return self.from_cache(section, name) except CachedValueNotFound: pass db_pref = self.get_db_pref(section=section, name=name) self.to_cache(db_pref) return db_pref.value def get_db_pref(self, section, name): try: pref = self.queryset.get(section=section, name=name) except self.model.DoesNotExist: pref_obj = self.pref_obj(section=section, name=name) pref = self.create_db_pref( section=section, name=name, value=pref_obj.get("default") ) return pref def update_db_pref(self, section, name, value): try: db_pref = self.queryset.get(section=section, name=name) old_value = db_pref.value db_pref.value = value db_pref.save() preference_updated.send( sender=self.__class__, section=section, name=name, old_value=old_value, new_value=value, instance=db_pref ) except self.model.DoesNotExist: return self.create_db_pref(section, name, value) return db_pref def create_db_pref(self, section, name, value): kwargs = { "section": section, "name": name, } if self.instance: kwargs["instance"] = self.instance # this is a just a shortcut to get the raw, serialized value # so we can pass it to get_or_create m = self.model(**kwargs) m.value = value raw_value = m.raw_value db_pref, created = self.model.objects.get_or_create(**kwargs) if created and db_pref.raw_value != raw_value: db_pref.raw_value = raw_value db_pref.save() return db_pref def all(self): """Return a dictionary containing all preferences by section Loaded from cache or from db in case of cold cache """ if not preferences_settings.ENABLE_CACHE: return self.load_from_db() preferences = self.registry.preferences() # first we hit the cache once for all existing preferences a = self.many_from_cache(preferences) if len(a) == len(preferences): return a # avoid database hit if not necessary # then we fill those that miss, but exist in the database # (just hit the database for all of them, filtering is complicated, and # in most cases you'd need to grab the majority of them anyway) a.update(self.load_from_db(cache=True)) return a def load_from_db(self, cache=False): """Return a dictionary of preferences by section directly from DB""" a = {} db_prefs = {p.preference.identifier(): p for p in self.queryset} cache_prefs = [] for preference in self.registry.preferences(): try: db_pref = db_prefs[preference.identifier()] except KeyError: db_pref = self.create_db_pref( section=preference.section.name, name=preference.name, value=preference.get("default"), ) else: # cache if create_db_pref() hasn't already done so if cache: cache_prefs.append(db_pref) a[preference.identifier()] = db_pref.value if cache_prefs: self.to_cache(*cache_prefs) return a ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736552720.6945016 django_dynamic_preferences-1.17.0/dynamic_preferences/migrations/0000775000175000017500000000000014740330421024274 5ustar00agateagate././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/dynamic_preferences/migrations/0001_initial.py0000644000175000017500000000275214413261622026746 0ustar00agateagate# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations from django.conf import settings class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( name="GlobalPreferenceModel", fields=[ ( "id", models.AutoField( primary_key=True, serialize=False, verbose_name="ID", auto_created=True, ), ), ( "section", models.CharField( blank=True, default=None, null=True, max_length=150, db_index=True, ), ), ("name", models.CharField(max_length=150, db_index=True)), ("raw_value", models.TextField(blank=True, null=True)), ], options={ "verbose_name_plural": "global preferences", "verbose_name": "global preference", }, bases=(models.Model,), ), migrations.AlterUniqueTogether( name="globalpreferencemodel", unique_together=set([("section", "name")]), ), ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/dynamic_preferences/migrations/0002_auto_20150712_0332.py0000644000175000017500000000131514413261622027710 0ustar00agateagate# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations from django.conf import settings class Migration(migrations.Migration): dependencies = [ ("dynamic_preferences", "0001_initial"), ] operations = [ migrations.AlterField( model_name="globalpreferencemodel", name="name", field=models.CharField(max_length=150, db_index=True), ), migrations.AlterField( model_name="globalpreferencemodel", name="section", field=models.CharField( max_length=150, blank=True, db_index=True, default=None, null=True ), ), ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/dynamic_preferences/migrations/0003_auto_20151223_1407.py0000644000175000017500000000155314413261622027717 0ustar00agateagate# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ ("dynamic_preferences", "0002_auto_20150712_0332"), ] operations = [ migrations.AlterField( model_name="globalpreferencemodel", name="name", field=models.CharField(max_length=150, db_index=True), preserve_default=True, ), migrations.AlterField( model_name="globalpreferencemodel", name="section", field=models.CharField( max_length=150, null=True, default=None, db_index=True, blank=True, verbose_name="Section Name", ), preserve_default=True, ), ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/dynamic_preferences/migrations/0004_move_user_model.py0000644000175000017500000000100014413261622030465 0ustar00agateagate# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations from django.conf import settings class Migration(migrations.Migration): """ Migration to move the user preferences to a dedicated app, see #33 Borrowed from http://stackoverflow.com/a/26472482/2844093 """ dependencies = [ ("dynamic_preferences", "0003_auto_20151223_1407"), ] # cf https://github.com/agateblue/django-dynamic-preferences/pull/142 operations = [] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/dynamic_preferences/migrations/0005_auto_20181120_0848.py0000644000175000017500000000161414413261622027726 0ustar00agateagate# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("dynamic_preferences", "0004_move_user_model"), ] operations = [ migrations.AlterModelOptions( name="globalpreferencemodel", options={ "verbose_name": "Global preference", "verbose_name_plural": "Global preferences", }, ), migrations.AlterField( model_name="globalpreferencemodel", name="name", field=models.CharField(db_index=True, max_length=150, verbose_name="Name"), ), migrations.AlterField( model_name="globalpreferencemodel", name="raw_value", field=models.TextField(blank=True, null=True, verbose_name="Raw Value"), ), ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/dynamic_preferences/migrations/0006_auto_20191001_2236.py0000644000175000017500000000113714413261622027717 0ustar00agateagate# Generated by Django 2.1.7 on 2019-10-01 14:36 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("dynamic_preferences", "0005_auto_20181120_0848"), ] operations = [ migrations.AlterField( model_name="globalpreferencemodel", name="section", field=models.CharField( blank=True, db_index=True, default=None, max_length=150, null=True, verbose_name="Section Name", ), ), ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1593767263.0 django_dynamic_preferences-1.17.0/dynamic_preferences/migrations/__init__.py0000644000175000017500000000000013677572537026421 0ustar00agateagate././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/dynamic_preferences/models.py0000644000175000017500000000737614413261622023773 0ustar00agateagate""" Preference models, queryset and managers that handle the logic for persisting preferences. """ from django.db import models from django.db.models.query import QuerySet from django.conf import settings from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ from dynamic_preferences.registries import ( preference_models, global_preferences_registry, ) from .utils import update class BasePreferenceModel(models.Model): """ A base model with common logic for all preferences models. """ #: The section under which the preference is declared section = models.CharField( max_length=150, db_index=True, blank=True, null=True, default=None, verbose_name=_("Section Name"), ) #: a name for the preference name = models.CharField(_("Name"), max_length=150, db_index=True) #: a value, serialized to a string. This field should not be accessed directly, use :py:attr:`BasePreferenceModel.value` instead raw_value = models.TextField(_("Raw Value"), null=True, blank=True) class Meta: abstract = True app_label = "dynamic_preferences" @cached_property def preference(self): return self.registry.get(section=self.section, name=self.name, fallback=True) @property def verbose_name(self): return self.preference.get("verbose_name", self.preference.identifier) verbose_name.fget.short_description = _("Verbose Name") @property def help_text(self): return self.preference.get("help_text", "") help_text.fget.short_description = _("Help Text") def set_value(self, value): """ Save serialized self.value to self.raw_value """ self.raw_value = self.preference.serializer.serialize(value) def get_value(self): """ Return deserialized self.raw_value """ return self.preference.serializer.deserialize(self.raw_value) value = property(get_value, set_value) def save(self, **kwargs): if self.pk is None and not self.raw_value: self.value = self.preference.get("default") super(BasePreferenceModel, self).save(**kwargs) def __str__(self): return self.__repr__() def __repr__(self): return "{0} - {1}/{2}".format(self.__class__.__name__, self.section, self.name) class GlobalPreferenceModel(BasePreferenceModel): registry = global_preferences_registry class Meta: unique_together = ("section", "name") app_label = "dynamic_preferences" verbose_name = _("Global preference") verbose_name_plural = _("Global preferences") class PerInstancePreferenceModel(BasePreferenceModel): """For preferences that are tied to a specific model instance""" #: the instance which is concerned by the preference #: use a ForeignKey pointing to the model of your choice instance = None class Meta(BasePreferenceModel.Meta): unique_together = ("instance", "section", "name") abstract = True @classmethod def get_instance_model(cls): return cls._meta.get_field("instance").remote_field.model global_preferences_registry.preference_model = GlobalPreferenceModel # Create default preferences for new instances from django.db.models.signals import post_save def invalidate_cache(sender, created, instance, **kwargs): if not isinstance(instance, BasePreferenceModel): return registry = preference_models.get_by_preference(instance) linked_instance = getattr(instance, "instance", None) kwargs = {} if linked_instance: kwargs["instance"] = linked_instance manager = registry.manager(**kwargs) manager.to_cache(instance) post_save.connect(invalidate_cache) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1697360004.0 django_dynamic_preferences-1.17.0/dynamic_preferences/preferences.py0000644000175000017500000000630314512724204024776 0ustar00agateagate""" Preferences are regular Python objects that can be declared within any django app. Once declared and registered, they can be edited by admins (for :py:class:`SitePreference` and :py:class:`GlobalPreference`) and regular Users (for :py:class:`UserPreference`) UserPreference, SitePreference and GlobalPreference are mapped to corresponding PreferenceModel, which store the actual values. """ from __future__ import unicode_literals import re import warnings from .settings import preferences_settings from .exceptions import MissingDefault from .serializers import UNSET class InvalidNameError(ValueError): pass def check_name(name, obj): error = None if not re.match(r"^\w+$", name): error = "Non-alphanumeric / underscore characters are forbidden in section and preferences names" if preferences_settings.SECTION_KEY_SEPARATOR in name: error = 'Sequence "{0}" is forbidden in section and preferences name, since it is used to access values via managers'.format( preferences_settings.SECTION_KEY_SEPARATOR ) if error: full_message = 'Invalid name "{0}" while instanciating {1} object: {2}'.format( name, obj, error ) raise InvalidNameError(full_message) class Section(object): def __init__(self, name, verbose_name=None): self.name = name self.verbose_name = verbose_name or name if preferences_settings.VALIDATE_NAMES and name: check_name(self.name, self) def __str__(self): if not self.verbose_name: return "" return str(self.verbose_name) EMPTY_SECTION = Section(None) class AbstractPreference(object): """ A base class that handle common logic for preferences """ #: The section under which the preference will be registered section = EMPTY_SECTION #: The preference name name = "" #: A default value for the preference default = UNSET def __init__(self, registry=None): if preferences_settings.VALIDATE_NAMES: check_name(self.name, self) if self.section and not hasattr(self.section, "name"): self.section = Section(name=self.section) warnings.warn( "Implicit section instanciation is deprecated and " "will be removed in future versions of django-dynamic-preferences", DeprecationWarning, stacklevel=2, ) self.registry = registry if self.default == UNSET and not getattr(self, "get_default", None): raise MissingDefault def get(self, attr, default=None): getter = "get_{0}".format(attr) if hasattr(self, getter): return getattr(self, getter)() return getattr(self, attr, default) @property def model(self): return self.registry.preference_model def identifier(self): """ Return the name and the section of the Preference joined with a separator, with the form `sectionname` """ if not self.section or not self.section.name: return self.name return preferences_settings.SECTION_KEY_SEPARATOR.join( [self.section.name, self.name] ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/dynamic_preferences/processors.py0000644000175000017500000000050314413261622024673 0ustar00agateagatefrom .registries import global_preferences_registry as gpr def global_preferences(request): """ Pass the values of global preferences to template context. You can then access value with `global_preferences.
.` """ manager = gpr.manager() return {"global_preferences": manager.all()} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/dynamic_preferences/registries.py0000644000175000017500000001757614413261622024673 0ustar00agateagatefrom django.core.exceptions import FieldDoesNotExist from django.apps import apps # import the logging library import warnings import logging import collections import persisting_theory # Get an instance of a logger logger = logging.getLogger(__name__) #: The package where autodiscover will try to find preferences to register from .managers import PreferencesManager from .settings import preferences_settings from .exceptions import NotFoundInRegistry from .types import StringPreference from .preferences import EMPTY_SECTION, Section class MissingPreference(StringPreference): """ Used as a fallback when the preference object is not found in registries This can happen for example when you delete a preference in the code, but don't remove the corresponding entries in database """ pass class PreferenceModelsRegistry(persisting_theory.Registry): """Store relationships beetween preferences model and preferences registry""" look_into = preferences_settings.REGISTRY_MODULE def register(self, preference_model, preference_registry): self[preference_model] = preference_registry preference_registry.preference_model = preference_model if not hasattr(preference_model, "registry"): setattr(preference_model, "registry", preference_registry) self.attach_manager(preference_model, preference_registry) def attach_manager(self, model, registry): if not hasattr(model, "instance"): return def instance_getter(self): return registry.manager(instance=self) getter = property(instance_getter) instance_class = model._meta.get_field("instance").remote_field.model setattr(instance_class, preferences_settings.MANAGER_ATTRIBUTE, getter) def get_by_preference(self, preference): return self[ preference._meta.proxy_for_model if preference._meta.proxy else preference.__class__ ] def get_by_instance(self, instance): """Return a preference registry using a model instance""" # we iterate through registered preference models in order to get the instance class # and check if instance is an instance of this class for model, registry in self.items(): try: instance_class = model._meta.get_field("instance").remote_field.model if isinstance(instance, instance_class): return registry except FieldDoesNotExist: # global preferences pass return None preference_models = PreferenceModelsRegistry() class PreferenceRegistry(persisting_theory.Registry): """ Registries are special dictionaries that are used by dynamic-preferences to register and access your preferences. dynamic-preferences has one registry per Preference type: - :py:const:`user_preferences` - :py:const:`site_preferences` - :py:const:`global_preferences` In order to register preferences automatically, you must call :py:func:`autodiscover` in your URLconf. """ look_into = preferences_settings.REGISTRY_MODULE #: a name to identify the registry name = "preferences_registry" preference_model = None #: used to reverse urls for sections in form views/templates section_url_namespace = None def __init__(self, *args, **kwargs): super(PreferenceRegistry, self).__init__(*args, **kwargs) self.section_objects = collections.OrderedDict() def register(self, preference_class): """ Store the given preference class in the registry. :param preference_class: a :py:class:`prefs.Preference` subclass """ preference = preference_class(registry=self) self.section_objects[preference.section.name] = preference.section try: self[preference.section.name][preference.name] = preference except KeyError: self[preference.section.name] = collections.OrderedDict() self[preference.section.name][preference.name] = preference return preference_class def _fallback(self, section_name, pref_name): """ Create a fallback preference object, This is used when you have model instances that do not match any registered preferences, see #41 """ message = ( "Creating a fallback preference with " + 'section "{}" and name "{}".' + "This means you have preferences in your database that " + "don't match any registered preference. " + "If you want to delete these entries, please refer to the " + "documentation: https://django-dynamic-preferences.readthedocs.io/en/latest/lifecycle.html" ) # NOQA warnings.warn(message.format(section_name, pref_name)) class Fallback(MissingPreference): section = Section(name=section_name) if section_name else None name = pref_name default = "" help_text = "Obsolete: missing in registry" return Fallback() def get(self, name, section=None, fallback=False): """ Returns a previously registered preference :param section: The section name under which the preference is registered :type section: str. :param name: The name of the preference. You can use dotted notation 'section.name' if you want to avoid providing section param :type name: str. :param fallback: Should we return a dummy preference object instead of raising an error if no preference is found? :type name: bool. :return: a :py:class:`prefs.BasePreference` instance """ # try dotted notation try: _section, name = name.split(preferences_settings.SECTION_KEY_SEPARATOR) return self[_section][name] except ValueError: pass # use standard params try: return self[section][name] except KeyError: if fallback: return self._fallback(section_name=section, pref_name=name) raise NotFoundInRegistry( "No such preference in {0} with section={1} and name={2}".format( self.__class__.__name__, section, name ) ) def get_by_name(self, name): """Get a preference by name only (no section)""" for section in self.values(): for preference in section.values(): if preference.name == name: return preference raise NotFoundInRegistry( "No such preference in {0} with name={1}".format( self.__class__.__name__, name ) ) def manager(self, **kwargs): """Return a preference manager that can be used to retrieve preference values""" return PreferencesManager(registry=self, model=self.preference_model, **kwargs) def sections(self): """ :return: a list of apps with registered preferences :rtype: list """ return self.keys() def preferences(self, section=None): """ Return a list of all registered preferences or a list of preferences registered for a given section :param section: The section name under which the preference is registered :type section: str. :return: a list of :py:class:`prefs.BasePreference` instances """ if section is None: return [self[section][name] for section in self for name in self[section]] else: return [self[section][name] for name in self[section]] class PerInstancePreferenceRegistry(PreferenceRegistry): pass class GlobalPreferenceRegistry(PreferenceRegistry): section_url_namespace = "dynamic_preferences:global.section" def populate(self, **kwargs): return self.models(**kwargs) global_preferences_registry = GlobalPreferenceRegistry() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736550780.0 django_dynamic_preferences-1.17.0/dynamic_preferences/serializers.py0000664000175000017500000003156114740324574025050 0ustar00agateagateimport decimal import os from datetime import date, timedelta, datetime, time, timezone from django.conf import settings from django.core.validators import EMPTY_VALUES from django.utils.dateparse import ( parse_duration, parse_datetime, parse_date, parse_time, ) from django.utils.duration import duration_string from django.utils.encoding import force_str from django.utils.timezone import ( is_aware, make_aware, make_naive, get_default_timezone, ) from django.db.models.fields.files import FieldFile class UnsetValue(object): pass UNSET = UnsetValue() class SerializationError(Exception): pass class BaseSerializer: """ A serializer take a Python variable and returns a string that can be stored safely in database """ exception = SerializationError @classmethod def serialize(cls, value, **kwargs): """ Return a string from a Python var """ return cls.to_db(value, **kwargs) @classmethod def deserialize(cls, value, **kwargs): """ Convert a python string to a var """ return cls.to_python(value, **kwargs) @classmethod def to_python(cls, value, **kwargs): raise NotImplementedError @classmethod def to_db(cls, value, **kwargs): return str(cls.clean_to_db_value(value)) @classmethod def clean_to_db_value(cls, value): return value class InstanciatedSerializer(BaseSerializer): """ In some situations, such as with FileSerializer, we need the serializer to be an instance and not a class """ def serialize(self, value, **kwargs): return self.to_db(value, **kwargs) def deserialize(self, value, **kwargs): return self.to_python(value, **kwargs) def to_python(self, value, **kwargs): raise NotImplementedError def to_db(self, value, **kwargs): return str(self.clean_to_db_value(value)) def clean_to_db_value(self, value): return value class BooleanSerializer(BaseSerializer): true = ( "True", "true", "TRUE", "1", "YES", "Yes", "yes", ) false = ( "False", "false", "FALSE", "0", "No", "no", "NO", ) @classmethod def clean_to_db_value(cls, value): if not isinstance(value, bool): raise cls.exception("{0} is not a boolean".format(value)) return value @classmethod def to_python(cls, value, **kwargs): if value in cls.true: return True elif value in cls.false: return False else: raise cls.exception( "Value {0} can't be deserialized to a Boolean".format(value) ) class IntegerSerializer(BaseSerializer): @classmethod def clean_to_db_value(cls, value): if not isinstance(value, int): raise cls.exception("IntSerializer can only serialize int values") return value @classmethod def to_python(cls, value, **kwargs): try: return int(value) except: raise cls.exception("Value {0} cannot be converted to int".format(value)) IntSerializer = IntegerSerializer class DecimalSerializer(BaseSerializer): @classmethod def clean_to_db_value(cls, value): if not isinstance(value, decimal.Decimal): raise cls.exception( "DecimalSerializer can only serialize Decimal instances" ) return value @classmethod def to_python(cls, value, **kwargs): try: return decimal.Decimal(value) except decimal.InvalidOperation: raise cls.exception( "Value {0} cannot be converted to decimal".format(value) ) class FloatSerializer(BaseSerializer): @classmethod def clean_to_db_value(cls, value): if not isinstance(value, (int, float)): raise cls.exception( "FloatSerializer can only serialize float or int values" ) return float(value) @classmethod def to_python(cls, value, **kwargs): try: return float(value) except float.InvalidOperation: raise cls.exception("Value {0} cannot be converted to float".format(value)) from django.template import defaultfilters class StringSerializer(BaseSerializer): @classmethod def to_db(cls, value, **kwargs): if not isinstance(value, str): raise cls.exception( "Cannot serialize, value {0} is not a string".format(value) ) if kwargs.get("escape_html", False): return defaultfilters.force_escape(value) else: return value @classmethod def to_python(cls, value, **kwargs): """String deserialisation just return the value as a string""" if not value: return "" try: return str(value) except: pass try: return value.encode("utf-8") except: pass raise cls.exception("Cannot deserialize value {0} tostring".format(value)) class ModelSerializer(InstanciatedSerializer): model = None def __init__(self, model): self.model = model def to_db(self, value, **kwargs): if not value or (value == UNSET): return None return str(value.pk) def to_python(self, value, **kwargs): if value is None: return try: pk = self.model._meta.pk.to_python(value) return self.model.objects.get(pk=pk) except: raise self.exception("Value {0} cannot be converted to pk".format(value)) class ModelMultipleSerializer(ModelSerializer): separator = "," sort = True def to_db(self, value, **kwargs): if not value: return if hasattr(value, "pk"): # Support single instances in this serializer to allow # create_deletion_handler to work for model multiple choice preferences value = [value.pk] elif hasattr(value, 'values_list'): value = list(value.values_list("pk", flat=True)) elif isinstance(value, list) and len(value) > 0 and isinstance(value[0], self.model): # Handle lists of model instances value = [i.pk for i in value] else: raise ValueError(f'Cannot handle value {value} of type {type(value)}') if value and self.sort: value = sorted(value) return self.separator.join(map(str, value)) def to_python(self, value, **kwargs): if value in EMPTY_VALUES: return self.model.objects.none() try: pks = value.split(",") pks = [int(i) if str(i).isdigit() else str(i) for i in pks] return self.model.objects.filter(pk__in=pks) except: raise self.exception("Array {0} cannot be converted to int".format(value)) # FieldFile also needs a model instance to save changes. class FakeInstance(object): """ FieldFile needs a model instance to update when file is persisted or deleted """ def save(self): return class FakeField(object): """ FieldFile needs a field object to generate a filename, persist and delete files, so we are effectively mocking that. """ name = "noop" attname = "noop" max_length = 10000 class PreferenceFieldFile(FieldFile): """ In order to have the same API that we have with models.FileField, we must return a FieldFile object. However, there are various things we have to override, since our files are not bound to a model field. """ def __init__(self, preference, storage, name): super(FieldFile, self).__init__(None, name) self.instance = FakeInstance() self.field = FakeField() self.field.storage = storage self.storage = storage self._committed = True self.preference = preference class FileSerializer(InstanciatedSerializer): """ Since this serializer requires additional data from the preference especially the upload path, we cannot do it without binding it to the preference it is therefore designed to be explicitely instanciated by the preference object. """ def __init__(self, preference): self.preference = preference def to_db(self, f, **kwargs): if not f: return saved_path = f.name if not hasattr(f, "save"): path = os.path.join(self.preference.get_upload_path(), f.name) saved_path = self.preference.get_file_storage().save(path, f) return saved_path def to_python(self, value, **kwargs): if not value: return storage = self.preference.get_file_storage() return PreferenceFieldFile( preference=self.preference, storage=storage, name=value ) class DurationSerializer(BaseSerializer): @classmethod def to_db(cls, value, **kwargs): if not isinstance(value, timedelta): raise cls.exception( "Cannot serialize, value {0} is not a timedelta".format(value) ) return duration_string(value) @classmethod def to_python(cls, value, **kwargs): parsed = parse_duration(force_str(value)) if parsed is None: raise cls.exception( "Value {0} cannot be converted to timedelta".format(value) ) return parsed class DateSerializer(BaseSerializer): @classmethod def to_db(cls, value, **kwargs): if not isinstance(value, date): raise cls.exception( "Cannot serialize, value {0} is not a date object".format(value) ) return value.isoformat() @classmethod def to_python(cls, value, **kwargs): parsed = parse_date(force_str(value)) if parsed is None: raise cls.exception( "Value {0} cannot be converted to a date object".format(value) ) return parsed class DateTimeSerializer(BaseSerializer): @classmethod def to_db(cls, value, **kwargs): if not isinstance(value, datetime): raise cls.exception( "Cannot serialize, value {0} is not a datetime object".format(value) ) value = cls.enforce_timezone(value) return value.isoformat() @classmethod def enforce_timezone(cls, value): """ When `self.default_timezone` is `None`, always return naive datetimes. When `self.default_timezone` is not `None`, always return aware datetimes. """ field_timezone = cls.default_timezone() if (field_timezone is not None) and not is_aware(value): return make_aware(value, field_timezone) elif (field_timezone is None) and is_aware(value): return make_naive(value, timezone.utc) return value @classmethod def default_timezone(cls): return get_default_timezone() if settings.USE_TZ else None @classmethod def to_python(cls, value, **kwargs): parsed = parse_datetime(force_str(value)) if parsed is None: raise cls.exception( "Value {0} cannot be converted to a datetime object".format(value) ) return parsed class TimeSerializer(BaseSerializer): @classmethod def to_db(cls, value, **kwargs): if not isinstance(value, time): raise cls.exception( "Cannot serialize, value {0} is not a time object".format(value) ) return value.isoformat() @classmethod def to_python(cls, value, **kwargs): parsed = parse_time(force_str(value)) if parsed is None: raise cls.exception( "Value {0} cannot be converted to a time object".format(value) ) return parsed class MultipleSerializer(BaseSerializer): separator = "," sort = True @classmethod def to_db(cls, value, **kwargs): if not value: return # This makes the use of the separator in choices safe by duplicating # it in each value before they are joined later on # Contract: choices keys cannot be empty value = [str(v).replace(cls.separator, cls.separator * 2) for v in value] if "" in value: raise cls.exception("Choices must not be empty") if cls.sort: value = sorted(value) return cls.separator.join(value) @classmethod def to_python(cls, value, **kwargs): if value in EMPTY_VALUES: return [] ret = value.split(cls.separator) # Duplication of separator is reverted (cf. to_db) while "" in ret: pos = ret.index("") val = ret[pos - 1] + cls.separator + ret[pos + 1] ret = ret[0: pos - 1] + [val] + ret[pos + 2:] return ret ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/dynamic_preferences/settings.py0000644000175000017500000000425414413261622024340 0ustar00agateagate# Taken from django-rest-framework # https://github.com/tomchristie/django-rest-framework # Copyright (c) 2011-2015, Tom Christie All rights reserved. from django.conf import settings SETTINGS_ATTR = "DYNAMIC_PREFERENCES" USER_SETTINGS = None DEFAULTS = { # 'REGISTRY_MODULE': 'prefs', # 'BASE_PREFIX': 'base', # 'SECTIONS_PREFIX': 'sections', # 'PREFERENCES_PREFIX': 'preferences', # 'PERMISSIONS_PREFIX': 'permissions', "MANAGER_ATTRIBUTE": "preferences", "SECTION_KEY_SEPARATOR": "__", "REGISTRY_MODULE": "dynamic_preferences_registry", "ADMIN_ENABLE_CHANGELIST_FORM": False, "ENABLE_GLOBAL_MODEL_AUTO_REGISTRATION": True, "ENABLE_USER_PREFERENCES": True, "ENABLE_CACHE": True, "CACHE_NAME": "default", "VALIDATE_NAMES": True, "FILE_PREFERENCE_UPLOAD_DIR": "dynamic_preferences", # this will be used to cache empty values, since some cache backends # does not support it on get_many "CACHE_NONE_VALUE": "__dynamic_preferences_empty_value", } class PreferenceSettings(object): """ A settings object, that allows API settings to be accessed as properties. For example: from rest_framework.settings import api_settings print(api_settings.DEFAULT_RENDERER_CLASSES) Any setting with string import paths will be automatically resolved and return the class, rather than the string literal. """ def __init__(self, defaults=None): self.defaults = defaults or DEFAULTS @property def user_settings(self): return getattr(settings, SETTINGS_ATTR, {}) def __getattr__(self, attr): if attr not in self.defaults.keys(): raise AttributeError("Invalid preference setting: '%s'" % attr) try: # Check if present in user settings val = self.user_settings[attr] except KeyError: # Fall back to defaults val = self.defaults[attr] # Cache the result # We sometimes need to bypass that, like in tests if getattr(settings, "CACHE_DYNAMIC_PREFERENCES_SETTINGS", True): setattr(self, attr, val) return val preferences_settings = PreferenceSettings(DEFAULTS) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/dynamic_preferences/signals.py0000644000175000017500000000022414413261622024131 0ustar00agateagatefrom django.dispatch import Signal # Arguments provided to listeners: "section", "name", "old_value" and "new_value" preference_updated = Signal() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736552720.6865017 django_dynamic_preferences-1.17.0/dynamic_preferences/templates/0000775000175000017500000000000014740330421024116 5ustar00agateagate././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736552720.6955018 django_dynamic_preferences-1.17.0/dynamic_preferences/templates/dynamic_preferences/0000775000175000017500000000000014740330421030123 5ustar00agateagate././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1593767263.0 django_dynamic_preferences-1.17.0/dynamic_preferences/templates/dynamic_preferences/base.html0000644000175000017500000000072313677572537031753 0ustar00agateagate
{% block content %}{% endblock %}
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1593767263.0 django_dynamic_preferences-1.17.0/dynamic_preferences/templates/dynamic_preferences/form.html0000644000175000017500000000075313677572537032007 0ustar00agateagate{% extends "dynamic_preferences/base.html" %} {% load i18n %} {% block content %} {# we continue to pass the sections key in case someone subclassed the template and use these #} {% include "dynamic_preferences/sections.html" with registry=registry sections=registry.sections %}
{% csrf_token %} {{ form.as_p }}
{% endblock %} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1593767263.0 django_dynamic_preferences-1.17.0/dynamic_preferences/templates/dynamic_preferences/sections.html0000644000175000017500000000052113677572537032664 0ustar00agateagate ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1593767263.0 django_dynamic_preferences-1.17.0/dynamic_preferences/templates/dynamic_preferences/testcontext.html0000644000175000017500000000000013677572537033411 0ustar00agateagate././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/dynamic_preferences/types.py0000644000175000017500000003355714413261622023654 0ustar00agateagate""" You'll find here the final, concrete classes of preferences you can use in your own project. """ from django import forms from django.db.models.signals import pre_delete from django.core.files.storage import default_storage from .preferences import AbstractPreference, Section from .exceptions import MissingModel from dynamic_preferences.serializers import * from dynamic_preferences.settings import preferences_settings class BasePreferenceType(AbstractPreference): """ Used as a base for all other preference classes. You should subclass this one if you want to implement your own preference. """ field_class = None """ A form field that will be used to display and edit the preference use a class, not an instance. :Example: .. code-block:: python from django import forms class MyPreferenceType(BasePreferenceType): field_class = forms.CharField """ #: A serializer class (see dynamic_preferences.serializers) serializer = None field_kwargs = {} """ Additional kwargs to be passed to the form field. :Example: .. code-block:: python class MyPreference(StringPreference): field_kwargs = { 'required': False, 'initial': 'Hello there' } """ @property def initial(self): return self.get_initial() def get_initial(self): """ :return: initial data for form field from field_attribute['initial'] or default """ return self.field_kwargs.get("initial", self.get("default")) @property def field(self): """ :return: an instance of a form field for this preference, with the correct configuration (widget, initial value, validators...) """ return self.setup_field() def setup_field(self, **kwargs): field_class = self.get("field_class") field_kwargs = self.get_field_kwargs() field_kwargs.update(kwargs) return field_class(**field_kwargs) def get_field_kwargs(self): """ Return a dict of arguments to use as parameters for the field class instianciation. This will use :py:attr:`field_kwargs` as a starter, and use sensible defaults for a few attributes: - :py:attr:`instance.verbose_name` for the field label - :py:attr:`instance.help_text` for the field help text - :py:attr:`instance.widget` for the field widget - :py:attr:`instance.required` defined if the value is required or not - :py:attr:`instance.initial` defined if the initial value """ kwargs = self.field_kwargs.copy() kwargs.setdefault("label", self.get("verbose_name")) kwargs.setdefault("help_text", self.get("help_text")) kwargs.setdefault("widget", self.get("widget")) kwargs.setdefault("required", self.get("required")) kwargs.setdefault("initial", self.initial) kwargs.setdefault("validators", []) kwargs["validators"].append(self.validate) return kwargs def api_repr(self, value): """ Used only to represent a preference value using Rest Framework """ return value def get_api_additional_data(self): """ Additional data to serialize for use on front-end side, for example """ return {} def get_api_field_data(self): """ Field data to serialize for use on front-end side, for example will include choices available for a choice field """ field = self.setup_field() d = { "class": field.__class__.__name__, "widget": {"class": field.widget.__class__.__name__}, } try: d["input_type"] = field.widget.input_type except AttributeError: # some widgets, such as Select do not have an input type # in django < 1.11 d["input_type"] = None return d def validate(self, value): """ Used to implement custom cleaning logic for use in forms and serializers. The method will be passed as a validator to the preference form field. :Example: .. code-block:: python def validate(self, value): if value == '42': raise ValidationError('Wrong value!') """ return class BooleanPreference(BasePreferenceType): """ A preference type that stores a boolean. """ field_class = forms.BooleanField serializer = BooleanSerializer required = False class IntegerPreference(BasePreferenceType): """ A preference type that stores an integer. """ field_class = forms.IntegerField serializer = IntegerSerializer IntPreference = IntegerPreference class DecimalPreference(BasePreferenceType): """ A preference type that stores a :py:class:`decimal.Decimal`. """ field_class = forms.DecimalField serializer = DecimalSerializer class FloatPreference(BasePreferenceType): """ A preference type that stores a float. """ field_class = forms.FloatField serializer = FloatSerializer class StringPreference(BasePreferenceType): """ A preference type that stores a string. """ field_class = forms.CharField serializer = StringSerializer class LongStringPreference(StringPreference): """ A preference type that stores a string, but with a textarea widget. """ widget = forms.Textarea class ChoicePreference(BasePreferenceType): """ A preference type that stores a string among a list of choices. """ choices = () """ Expects the same values as for django :py:class:`forms.ChoiceField`. :Example: .. code-block:: python class MyChoicePreference(ChoicePreference): choices = [ ('c', 'Carrot'), ('t', 'Tomato'), ] """ field_class = forms.ChoiceField serializer = StringSerializer def get_field_kwargs(self): field_kwargs = super(ChoicePreference, self).get_field_kwargs() field_kwargs["choices"] = self.get("choices") or self.field_attribute["initial"] return field_kwargs def get_api_additional_data(self): d = super(ChoicePreference, self).get_api_additional_data() d["choices"] = self.get("choices") return d def get_choice_values(self): return [c[0] for c in self.get("choices")] def validate(self, value): if value not in self.get_choice_values(): raise forms.ValidationError("{} is not a valid choice".format(value)) def create_deletion_handler(preference): """ Will generate a dynamic handler to purge related preference on instance deletion """ def delete_related_preferences(sender, instance, *args, **kwargs): queryset = preference.registry.preference_model.objects.filter( name=preference.name, section=preference.section ) related_preferences = queryset.filter( raw_value=preference.serializer.serialize(instance) ) related_preferences.delete() return delete_related_preferences class ModelChoicePreference(BasePreferenceType): """ A preference type that stores a reference to a model instance. :Example: .. code-block:: python from myapp.blog.models import BlogEntry @registry.register class FeaturedEntry(ModelChoicePreference): section = Section('blog') name = 'featured_entry' queryset = BlogEntry.objects.filter(status='published') blog_entry = BlogEntry.objects.get(pk=12) manager['blog__featured_entry'] = blog_entry # accessing the value will return the model instance assert manager['blog__featured_entry'].pk == 12 .. note:: You should provide either the :py:attr:`queryset` or :py:attr:`model` attribute """ field_class = forms.ModelChoiceField serializer_class = ModelSerializer model = None """ Which model class to link the preference to. You can skip this if you define the :py:attr:`queryset` attribute. """ queryset = None """ A queryset to filter available model instances. """ signals_handlers = {} def __init__(self, *args, **kwargs): super(ModelChoicePreference, self).__init__(*args, **kwargs) if self.model is not None: # Set queryset following model attribute self.queryset = self.model.objects.all() elif self.queryset is not None: # Set model following queryset attribute self.model = self.queryset.model else: raise MissingModel self.serializer = self.serializer_class(self.model) self._setup_signals() def _setup_signals(self): handler = create_deletion_handler(self) # We need to keep a reference to the handler or it will cause # weakref to die and our handler will not be called self.signals_handlers["pre_delete"] = [handler] pre_delete.connect(handler, sender=self.model) def get_field_kwargs(self): kw = super(ModelChoicePreference, self).get_field_kwargs() kw["queryset"] = self.get("queryset") return kw def api_repr(self, value): if not value: return None if value.__class__.__name__ == "QuerySet": return [val.pk for val in value] return value.pk class ModelMultipleChoicePreference(ModelChoicePreference): """ A preference type that stores a reference list to the model instances. :Example: .. code-block:: python from myapp.blog.models import BlogEntry @registry.register class FeaturedEntries(ModelMultipleChoicePreference): section = Section('blog') name = 'featured_entries' queryset = BlogEntry.objects.all() blog_entries = BlogEntry.objects.filter(status='published') manager['blog__featured_entries'] = blog_entries # accessing the value will return the model queryset assert manager['blog__featured_entries'] == blog_entries .. note:: You should provide either the :py:attr:`queryset` or :py:attr:`model` attribute """ serializer_class = ModelMultipleSerializer field_class = forms.ModelMultipleChoiceField def _setup_signals(self): pass class FilePreference(BasePreferenceType): """ A preference type that stores a a reference to a model. :Example: .. code-block:: python from django.core.files.uploadedfile import SimpleUploadedFile @registry.register class Logo(FilePreference): section = Section('blog') name = 'logo' logo = SimpleUploadedFile( "logo.png", b"file_content", content_type="image/png") manager['blog__logo'] = logo # accessing the value will return a FieldFile object, just as # django.db.models.FileField assert manager['blog__logo'].read() == b'file_content' manager['blog__logo'].delete() """ field_class = forms.FileField serializer_class = FileSerializer default = None @property def serializer(self): """ The serializer need additional data about the related preference to upload file to correct directory """ return self.serializer_class(self) def get_field_kwargs(self): kwargs = super(FilePreference, self).get_field_kwargs() kwargs["required"] = self.get("required", False) return kwargs def get_upload_path(self): return os.path.join( preferences_settings.FILE_PREFERENCE_UPLOAD_DIR, self.identifier() ) def get_file_storage(self): """ Override this method if you want to use a custom storage """ return default_storage def api_repr(self, value): if value: return value.url class DurationPreference(BasePreferenceType): """ A preference type that stores a timedelta. """ field_class = forms.DurationField serializer = DurationSerializer def api_repr(self, value): return duration_string(value) class DatePreference(BasePreferenceType): """ A preference type that stores a date. """ field_class = forms.DateField serializer = DateSerializer def api_repr(self, value): return value.isoformat() class DateTimePreference(BasePreferenceType): """ A preference type that stores a datetime. """ field_class = forms.DateTimeField serializer = DateTimeSerializer def api_repr(self, value): return value.isoformat() class TimePreference(BasePreferenceType): """ A preference type that stores a time. """ field_class = forms.TimeField serializer = TimeSerializer def api_repr(self, value): return value.isoformat() class MultipleChoicePreference(ChoicePreference): """ A preference type that stores multiple strings among a list of choices. :Example: .. code-block:: python @registry.register class FeaturedEntries(MultipleChoicePreference): section = Section('blog') name = 'featured_entries' choices = [ ('c', 'Carrot'), ('t', 'Tomato'), ] .. note:: Internally, the selected choices are stored as a string, separated by a separator. The separator defaults to ','. The way this is implemented still is sae also on keys that cotain the separator, but if in doubt, you can still set the :py:attr:`separator` to any other character. """ widget = forms.CheckboxSelectMultiple field_class = forms.MultipleChoiceField serializer = MultipleSerializer def validate(self, value): for v in value: super().validate(v) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/dynamic_preferences/urls.py0000644000175000017500000000170614413261622023464 0ustar00agateagatetry: from django.urls import include, re_path except ImportError: from django.conf.urls import include, url as re_path from django.contrib.admin.views.decorators import staff_member_required from . import views from .registries import global_preferences_registry from .forms import GlobalPreferenceForm app_name = "dynamic_preferences" urlpatterns = [ re_path( r"^global/$", staff_member_required( views.PreferenceFormView.as_view( registry=global_preferences_registry, form_class=GlobalPreferenceForm ) ), name="global", ), re_path( r"^global/(?P
[\w\ ]+)$", staff_member_required( views.PreferenceFormView.as_view( registry=global_preferences_registry, form_class=GlobalPreferenceForm ) ), name="global.section", ), re_path(r"^user/", include("dynamic_preferences.users.urls")), ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736552720.6965017 django_dynamic_preferences-1.17.0/dynamic_preferences/users/0000775000175000017500000000000014740330421023261 5ustar00agateagate././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1593767263.0 django_dynamic_preferences-1.17.0/dynamic_preferences/users/__init__.py0000644000175000017500000000000013677572537025406 0ustar00agateagate././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/dynamic_preferences/users/admin.py0000644000175000017500000000141714413261622024727 0ustar00agateagatefrom django.contrib import admin as django_admin from django import forms from ..settings import preferences_settings from .. import admin from .models import UserPreferenceModel from .forms import UserSinglePreferenceForm class UserPreferenceAdmin(admin.PerInstancePreferenceAdmin): search_fields = ["instance__username"] + admin.DynamicPreferenceAdmin.search_fields form = UserSinglePreferenceForm changelist_form = UserSinglePreferenceForm def get_queryset(self, request, *args, **kwargs): # Instanciate default prefs getattr(request.user, preferences_settings.MANAGER_ATTRIBUTE).all() return super(UserPreferenceAdmin, self).get_queryset(request, *args, **kwargs) django_admin.site.register(UserPreferenceModel, UserPreferenceAdmin) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/dynamic_preferences/users/apps.py0000644000175000017500000000114714413261622024602 0ustar00agateagatefrom django.apps import AppConfig, apps from django.conf import settings from django.utils.translation import gettext_lazy as _ from ..registries import preference_models from .registries import user_preferences_registry class UserPreferencesConfig(AppConfig): name = "dynamic_preferences.users" verbose_name = _("Preferences - Users") label = "dynamic_preferences_users" default_auto_field = "django.db.models.AutoField" def ready(self): UserPreferenceModel = self.get_model("UserPreferenceModel") preference_models.register(UserPreferenceModel, user_preferences_registry) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736550780.0 django_dynamic_preferences-1.17.0/dynamic_preferences/users/forms.py0000664000175000017500000000173214740324574025000 0ustar00agateagatefrom django import forms from django.core.exceptions import ValidationError from collections import OrderedDict from .registries import user_preferences_registry from ..forms import ( SinglePerInstancePreferenceForm, preference_form_builder, PreferenceForm, ) from ..exceptions import NotFoundInRegistry from .models import UserPreferenceModel class UserSinglePreferenceForm(SinglePerInstancePreferenceForm): class Meta: model = UserPreferenceModel fields = SinglePerInstancePreferenceForm.Meta.fields def user_preference_form_builder(instance, preferences=[], **kwargs): """ A shortcut :py:func:`preference_form_builder(UserPreferenceForm, preferences, **kwargs)` :param user: a :py:class:`django.contrib.auth.models.User` instance """ return preference_form_builder( UserPreferenceForm, preferences, instance=instance, **kwargs ) class UserPreferenceForm(PreferenceForm): registry = user_preferences_registry ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736552720.6975017 django_dynamic_preferences-1.17.0/dynamic_preferences/users/migrations/0000775000175000017500000000000014740330421025435 5ustar00agateagate././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/dynamic_preferences/users/migrations/0001_initial.py0000644000175000017500000000341314413261622030102 0ustar00agateagate# Generated by Django 2.0.6 on 2018-06-15 16:20 from django.conf import settings from django.db import migrations, models import django.db.models.deletion class Migration(migrations.Migration): initial = True dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( name="UserPreferenceModel", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ( "section", models.CharField( blank=True, db_index=True, default=None, max_length=150, null=True, ), ), ("name", models.CharField(db_index=True, max_length=150)), ("raw_value", models.TextField(blank=True, null=True)), ( "instance", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, ), ), ], options={ "verbose_name": "user preference", "verbose_name_plural": "user preferences", "abstract": False, }, ), migrations.AlterUniqueTogether( name="userpreferencemodel", unique_together={("instance", "section", "name")}, ), ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/dynamic_preferences/users/migrations/0002_auto_20200821_0837.py0000644000175000017500000000175214413261622031065 0ustar00agateagate# Generated by Django 3.1 on 2020-08-21 08:37 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("dynamic_preferences_users", "0001_initial"), ] operations = [ migrations.AlterField( model_name="userpreferencemodel", name="name", field=models.CharField(db_index=True, max_length=150, verbose_name="Name"), ), migrations.AlterField( model_name="userpreferencemodel", name="raw_value", field=models.TextField(blank=True, null=True, verbose_name="Raw Value"), ), migrations.AlterField( model_name="userpreferencemodel", name="section", field=models.CharField( blank=True, db_index=True, default=None, max_length=150, null=True, verbose_name="Section Name", ), ), ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1593767263.0 django_dynamic_preferences-1.17.0/dynamic_preferences/users/migrations/__init__.py0000644000175000017500000000000013677572537027562 0ustar00agateagate././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/dynamic_preferences/users/models.py0000644000175000017500000000101114413261622025110 0ustar00agateagatefrom django.db import models from django.conf import settings from django.utils.translation import gettext_lazy as _ from dynamic_preferences.models import PerInstancePreferenceModel class UserPreferenceModel(PerInstancePreferenceModel): instance = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) class Meta(PerInstancePreferenceModel.Meta): app_label = "dynamic_preferences_users" verbose_name = _("user preference") verbose_name_plural = _("user preferences") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/dynamic_preferences/users/registries.py0000644000175000017500000000035414413261622026016 0ustar00agateagatefrom ..registries import PerInstancePreferenceRegistry class UserPreferenceRegistry(PerInstancePreferenceRegistry): section_url_namespace = "dynamic_preferences:user.section" user_preferences_registry = UserPreferenceRegistry() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1593767263.0 django_dynamic_preferences-1.17.0/dynamic_preferences/users/serializers.py0000644000175000017500000000020613677572537026213 0ustar00agateagatefrom dynamic_preferences.api.serializers import PreferenceSerializer class UserPreferenceSerializer(PreferenceSerializer): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/dynamic_preferences/users/urls.py0000644000175000017500000000071614413261622024625 0ustar00agateagatetry: from django.urls import include, re_path except ImportError: from django.conf.urls import include, url as re_path from django.contrib.auth.decorators import login_required from . import views urlpatterns = [ re_path(r"^$", login_required(views.UserPreferenceFormView.as_view()), name="user"), re_path( r"^(?P
[\w\ ]+)$", login_required(views.UserPreferenceFormView.as_view()), name="user.section", ), ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/dynamic_preferences/users/views.py0000644000175000017500000000102614413261622024770 0ustar00agateagatefrom ..views import PreferenceFormView from .forms import user_preference_form_builder from .registries import user_preferences_registry class UserPreferenceFormView(PreferenceFormView): """ Will pass `request.user` to form_builder """ registry = user_preferences_registry def get_form_class(self, *args, **kwargs): section = self.kwargs.get("section", None) form_class = user_preference_form_builder( instance=self.request.user, section=section ) return form_class ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1593767263.0 django_dynamic_preferences-1.17.0/dynamic_preferences/users/viewsets.py0000644000175000017500000000067513677572537025542 0ustar00agateagatefrom rest_framework import permissions from dynamic_preferences.api import viewsets from . import serializers from . import models class UserPreferencesViewSet(viewsets.PerInstancePreferenceViewSet): queryset = models.UserPreferenceModel.objects.all() serializer_class = serializers.UserPreferenceSerializer permission_classes = [permissions.IsAuthenticated] def get_related_instance(self): return self.request.user ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/dynamic_preferences/utils.py0000644000175000017500000000071614413261622023637 0ustar00agateagatetry: from collections.abc import Mapping except ImportError: from collections import Mapping def update(d, u): """ Custom recursive update of dictionary from http://stackoverflow.com/questions/3232943/update-value-of-a-nested-dictionary-of-varying-depth """ for k, v in u.iteritems(): if isinstance(v, Mapping): r = update(d.get(k, {}), v) d[k] = r else: d[k] = u[k] return d ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/dynamic_preferences/views.py0000644000175000017500000000344014413261622023631 0ustar00agateagatefrom django.views.generic import TemplateView, FormView from django.http import Http404 from .forms import preference_form_builder """Todo : remove these views and use only context processors""" class RegularTemplateView(TemplateView): """Used for testing context""" template_name = "dynamic_preferences/testcontext.html" class PreferenceFormView(FormView): """ Display a form for updating preferences of the given section provided via URL arg. If no section is provided, will display a form for all fields of a given registry. """ #: the registry for preference lookups registry = None #: will be used by :py:func:`forms.preference_form_builder` # to create the form form_class = None template_name = "dynamic_preferences/form.html" def dispatch(self, request, *args, **kwargs): self.section_name = kwargs.get("section", None) if self.section_name: try: self.section = self.registry.section_objects[self.section_name] except KeyError: raise Http404 else: self.section = None return super(PreferenceFormView, self).dispatch(request, *args, **kwargs) def get_form_class(self, *args, **kwargs): form_class = preference_form_builder(self.form_class, section=self.section_name) return form_class def get_context_data(self, *args, **kwargs): context = super(PreferenceFormView, self).get_context_data(*args, **kwargs) context["registry"] = self.registry context["section"] = self.section return context def get_success_url(self): return self.request.path def form_valid(self, form): form.update_preferences() return super(PreferenceFormView, self).form_valid(form) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736552720.6995018 django_dynamic_preferences-1.17.0/setup.cfg0000644000175000017500000000013414740330421017730 0ustar00agateagate[wheel] universal = 1 [flake8] max-line-length = 90 [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736552153.0 django_dynamic_preferences-1.17.0/setup.py0000775000175000017500000000252414740327331017641 0ustar00agateagate#!/usr/bin/env python # -*- coding: utf-8 -*- import os import sys import dynamic_preferences try: from setuptools import setup except ImportError: from distutils.core import setup version = dynamic_preferences.__version__ if sys.argv[-1] == "publish": os.system("python setup.py sdist upload") print("You probably want to also tag the version now:") print(" git tag -a %s -m 'version %s'" % (version, version)) print(" git push --tags") sys.exit() readme = open("README.rst").read() setup( name="django-dynamic-preferences", version=version, description="""Dynamic global and instance settings for your django project""", long_description=readme, author="Agate Blue", author_email="me+github@agate.blue", url="https://github.com/agateblue/django-dynamic-preferences", packages=["dynamic_preferences"], include_package_data=True, install_requires=[ "django>=4.2", "persisting_theory==1.0", ], license="BSD", zip_safe=False, keywords="django-dynamic-preferences", classifiers=[ "Development Status :: 5 - Production/Stable", "Framework :: Django", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Natural Language :: English", "Programming Language :: Python :: 3", ], ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736552720.6985018 django_dynamic_preferences-1.17.0/tests/0000775000175000017500000000000014740330421017255 5ustar00agateagate././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/tests/test_checkpreferences_command.py0000644000175000017500000000201414413261622025661 0ustar00agateagatefrom io import StringIO from django.core.management import call_command def call(*args, **kwargs): out = StringIO() call_command( "checkpreferences", *args, stdout=out, stderr=StringIO(), **kwargs, ) return out.getvalue().strip() def test_dry_run(db): out = call(verbosity=0) expected_output = "\n".join( [ "Creating missing global preferences...", "Deleted 0 global preferences", "Deleted 0 GlobalPreferenceModel preferences", "Deleted 0 UserPreferenceModel preferences", "Creating missing preferences for User model...", ] ) assert out == expected_output def test_skip_create(db): out = call("--skip_create", verbosity=0) expected_output = "\n".join( [ "Deleted 0 global preferences", "Deleted 0 GlobalPreferenceModel preferences", "Deleted 0 UserPreferenceModel preferences", ] ) assert out == expected_output ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/tests/test_global_preferences.py0000644000175000017500000001703514413261622024516 0ustar00agateagatefrom datetime import timezone from decimal import Decimal from datetime import date, timedelta, datetime, time from django.apps import apps from django.urls import reverse from django.core.management import call_command from django.core.files.uploadedfile import SimpleUploadedFile from django.utils.timezone import make_aware from dynamic_preferences.registries import global_preferences_registry as registry from dynamic_preferences.models import GlobalPreferenceModel from dynamic_preferences.forms import global_preference_form_builder from .test_app.models import BlogEntry def test_preference_model_manager_to_dict(db): manager = registry.manager() call_command("checkpreferences", verbosity=1) expected = { "test__TestGlobal1": "default value", "test__TestGlobal2": False, "test__TestGlobal3": False, "type__cost": Decimal(0), "exam__duration": timedelta(hours=3), "no_section": False, "user__max_users": 100, "user__items_per_page": 25, "blog__featured_entry": None, "blog__logo": None, "blog__logo2": None, "company__RegistrationDate": date(1998, 9, 4), "child__BirthDateTime": datetime( 1992, 5, 4, 3, 4, 10, 150, tzinfo=timezone.utc ), "company__OpenningTime": time(hour=8, minute=0), "user__registration_allowed": False, } assert manager.all() == expected def test_registry_default_preference_model(settings): app_config = apps.app_configs["dynamic_preferences"] registry.preference_model = None settings.DYNAMIC_PREFERENCES = {"ENABLE_GLOBAL_MODEL_AUTO_REGISTRATION": False} app_config.ready() assert registry.preference_model is None settings.DYNAMIC_PREFERENCES = {"ENABLE_GLOBAL_MODEL_AUTO_REGISTRATION": True} app_config.ready() assert registry.preference_model is GlobalPreferenceModel def test_can_build_global_preference_form(db): # We want to display a form with two global preferences # RegistrationAllowed and MaxUsers form = global_preference_form_builder( preferences=["user__registration_allowed", "user__max_users"] )() assert len(form.fields) == 2 assert form.fields["user__registration_allowed"].initial is False def test_can_build_preference_form_from_sections(db): form = global_preference_form_builder(section="test")() assert len(form.fields) == 3 def test_can_build_global_preference_form_from_sections(db): form = global_preference_form_builder(section="test")() assert len(form.fields) == 3 def test_global_preference_view_requires_staff_member( fake_admin, assert_redirect, client ): url = reverse("dynamic_preferences:global") response = client.get(url) assert_redirect(response, "/admin/login/?next=/global/") client.login(username="henri", password="test") response = client.get(url) assert_redirect(response, "/admin/login/?next=/global/") client.login(username="admin", password="test") response = client.get(url) assert fake_admin.is_authenticated is True assert response.status_code == 200 def test_global_preference_view_display_form(admin_client): url = reverse("dynamic_preferences:global") response = admin_client.get(url) assert len(response.context["form"].fields) == 15 assert response.context["registry"] == registry def test_global_preference_view_section_verbose_names(admin_client): url = reverse("admin:dynamic_preferences_globalpreferencemodel_changelist") response = admin_client.get(url) for key, section in registry.section_objects.items(): if section.name != section.verbose_name: # Assert verbose_name in table assert str(response._container).count(section.verbose_name + "") >= 1 # Assert verbose_name in filter link assert str(response._container).count(section.verbose_name + "") >= 1 def test_formview_includes_section_in_context(admin_client): url = reverse("dynamic_preferences:global.section", kwargs={"section": "user"}) response = admin_client.get(url) assert response.context["section"] == registry.section_objects["user"] def test_formview_with_bad_section_returns_404(admin_client): url = reverse("dynamic_preferences:global.section", kwargs={"section": "nope"}) response = admin_client.get(url) assert response.status_code == 404 def test_global_preference_filters_by_section(admin_client): url = reverse("dynamic_preferences:global.section", kwargs={"section": "user"}) response = admin_client.get(url) assert len(response.context["form"].fields) == 3 def test_preference_are_updated_on_form_submission(admin_client): blog_entry = BlogEntry.objects.create(title="test", content="test") url = reverse("dynamic_preferences:global") data = { "user__max_users": 67, "user__registration_allowed": True, "user__items_per_page": 12, "test__TestGlobal1": "new value", "test__TestGlobal2": True, "test__TestGlobal3": True, "no_section": True, "blog__featured_entry": blog_entry.pk, "company__RegistrationDate": date(1976, 4, 1), "child__BirthDateTime": datetime.now(), "type__cost": 1, "exam__duration": timedelta(hours=5), "company__OpenningTime": time(hour=8, minute=0), } admin_client.post(url, data) for key, expected_value in data.items(): try: section, name = key.split("__") except ValueError: section, name = (None, key) p = GlobalPreferenceModel.objects.get(name=name, section=section) if name == "featured_entry": expected_value = blog_entry if name == "BirthDateTime": expected_value = make_aware(expected_value) assert p.value == expected_value def test_preference_are_updated_on_form_submission_by_section(admin_client): url = reverse("dynamic_preferences:global.section", kwargs={"section": "user"}) response = admin_client.post( url, { "user__max_users": 95, "user__registration_allowed": True, "user__items_per_page": 12, }, follow=True, ) assert response.status_code == 200 assert ( GlobalPreferenceModel.objects.get(section="user", name="max_users").value == 95 ) assert ( GlobalPreferenceModel.objects.get( section="user", name="registration_allowed" ).value is True ) assert ( GlobalPreferenceModel.objects.get(section="user", name="items_per_page").value == 12 ) def test_template_gets_global_preferences_via_template_processor(db, client): global_preferences = registry.manager() url = reverse("dynamic_preferences.test.templateview") response = client.get(url) assert response.context["global_preferences"] == global_preferences.all() def test_file_preference(admin_client): blog_entry = BlogEntry.objects.create(title="Hello", content="World") content = b"hello" logo = SimpleUploadedFile("logo.png", content, content_type="image/png") url = reverse("dynamic_preferences:global.section", kwargs={"section": "blog"}) response = admin_client.post( url, {"blog__featured_entry": blog_entry.pk, "blog__logo": logo}, follow=True ) assert response.status_code == 200 assert ( GlobalPreferenceModel.objects.get(section="blog", name="featured_entry").value == blog_entry ) assert ( GlobalPreferenceModel.objects.get(section="blog", name="logo").value.read() == content ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/tests/test_manager.py0000644000175000017500000000451014413261622022301 0ustar00agateagatefrom django.urls import reverse from dynamic_preferences.registries import global_preferences_registry as registry from dynamic_preferences.models import GlobalPreferenceModel def test_can_get_preferences_objects_from_manager(db): manager = registry.manager() cached_prefs = dict(manager.all()) qs = manager.queryset assert len(qs) == len(cached_prefs) assert list(qs) == list(GlobalPreferenceModel.objects.all()) def test_can_get_db_pref_from_manager(db): manager = registry.manager() manager.queryset.delete() pref = manager.get_db_pref(section="test", name="TestGlobal1") assert pref.section == "test" assert pref.name == "TestGlobal1" assert pref.raw_value == registry.get("test__TestGlobal1").default def test_do_not_restore_default_when_calling_all(db, cache): manager = registry.manager() new_value = "test_new_value" manager["test__TestGlobal1"] = new_value assert manager["test__TestGlobal1"] == new_value cache.clear() manager.all() cache.clear() assert manager["test__TestGlobal1"] == new_value assert manager.all()["test__TestGlobal1"] == new_value def test_invalidates_cache_when_saving_database_preference(db, cache): manager = registry.manager() cache.clear() new_value = "test_new_value" key = manager.get_cache_key("test", "TestGlobal1") manager["test__TestGlobal1"] = new_value pref = manager.get_db_pref(section="test", name="TestGlobal1") assert pref.raw_value == new_value assert manager.cache.get(key) == new_value pref.raw_value = "reset" pref.save() assert manager.cache.get(key) == "reset" def test_invalidates_cache_when_saving_from_admin(admin_client): manager = registry.manager() pref = manager.get_db_pref(section="test", name="TestGlobal1") url = reverse( "admin:dynamic_preferences_globalpreferencemodel_change", args=(pref.id,) ) key = manager.get_cache_key("test", "TestGlobal1") response = admin_client.post(url, {"raw_value": "reset1"}) assert manager.cache.get(key) == "reset1" assert manager.all()["test__TestGlobal1"] == "reset1" response = admin_client.post(url, {"raw_value": "reset2"}, follow=True) assert response.status_code == 200 assert manager.cache.get(key) == "reset2" assert manager.all()["test__TestGlobal1"] == "reset2" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736550780.0 django_dynamic_preferences-1.17.0/tests/test_preferences.py0000664000175000017500000001431214740324574023204 0ustar00agateagateimport pytest from dynamic_preferences.registries import ( MissingPreference, global_preferences_registry, ) from dynamic_preferences import preferences, exceptions from dynamic_preferences.types import IntegerPreference, StringPreference from dynamic_preferences.signals import preference_updated from .test_app import dynamic_preferences_registry as prefs from .test_app.models import BlogEntry try: from unittest.mock import MagicMock except ImportError: from mock import MagicMock def test_can_retrieve_preference_using_dotted_notation(db): registration_allowed = global_preferences_registry.get( name="registration_allowed", section="user" ) dotted_result = global_preferences_registry.get("user__registration_allowed") assert registration_allowed == dotted_result def test_can_register_and_retrieve_preference_with_section_none(db): no_section_pref = global_preferences_registry.get(name="no_section") assert no_section_pref.section == preferences.EMPTY_SECTION def test_cannot_instanciate_preference_or_section_with_invalid_name(): invalid_names = ["with space", "with__separator", "with-hyphen"] for n in invalid_names: with pytest.raises(ValueError): preferences.Section(n) with pytest.raises(ValueError): class P(IntegerPreference): name = n P() def test_preference_order_match_register_call(): expected = [ "registration_allowed", "max_users", "items_per_page", "featured_entry", ] assert [p.name for p in global_preferences_registry.preferences()][:4] == expected def test_preferences_manager_get(db): global_preferences = global_preferences_registry.manager() assert global_preferences["no_section"] is False def test_preferences_manager_set(db): global_preferences = global_preferences_registry.manager() global_preferences["no_section"] = True assert global_preferences["no_section"] is True def test_can_cache_single_preference(db, django_assert_num_queries): manager = global_preferences_registry.manager() manager["no_section"] with django_assert_num_queries(0): manager["no_section"] manager["no_section"] manager["no_section"] def test_can_bypass_cache_in_get(db, settings, django_assert_num_queries): settings.DYNAMIC_PREFERENCES = {"ENABLE_CACHE": False} manager = global_preferences_registry.manager() manager["no_section"] with django_assert_num_queries(3): manager["no_section"] manager["no_section"] manager["no_section"] def test_can_bypass_cache_in_get_all(db, settings): settings.DYNAMIC_PREFERENCES = {"ENABLE_CACHE": False} settings.DEBUG = True from django.db import connection manager = global_preferences_registry.manager() queries_before = len(connection.queries) manager.all() manager_queries = len(connection.queries) - queries_before manager.all() assert len(connection.queries) > manager_queries def test_can_cache_all_preferences(db, django_assert_num_queries): BlogEntry.objects.create(title="test", content="test") manager = global_preferences_registry.manager() manager.all() with django_assert_num_queries(3): # one request each time we retrieve the blog entry manager.all() manager.all() manager.all() def test_preferences_manager_by_name(db): manager = global_preferences_registry.manager() assert manager.by_name()["max_users"] == manager["user__max_users"] assert len(manager.all()) == len(manager.by_name()) def test_cache_invalidate_on_save(db, django_assert_num_queries): manager = global_preferences_registry.manager() model_instance = manager.create_db_pref( section=None, name="no_section", value=False ) with django_assert_num_queries(0): assert not manager["no_section"] manager["no_section"] model_instance.value = True model_instance.save() with django_assert_num_queries(0): assert manager["no_section"] manager["no_section"] def test_can_get_single_pref_with_cache_disabled(settings, db): settings.DYNAMIC_PREFERENCES = {"ENABLE_CACHE": False} manager = global_preferences_registry.manager() v = manager["no_section"] assert isinstance(v, bool) is True def test_can_get_single_pref_bypassing_cache(db): manager = global_preferences_registry.manager() v = manager.get("no_section", no_cache=True) assert isinstance(v, bool) is True def test_do_not_crash_if_preference_is_missing_in_registry(db): """see #41""" manager = global_preferences_registry.manager() instance = manager.create_db_pref(section=None, name="bad_pref", value="something") assert isinstance(instance.preference, MissingPreference) is True assert instance.preference.section is None assert instance.preference.name == "bad_pref" assert instance.value == "something" def test_can_get_to_string_notation(db): pref = global_preferences_registry.get("user__registration_allowed") assert pref.identifier() == "user__registration_allowed" def test_preference_requires_default_value(): with pytest.raises(exceptions.MissingDefault): prefs.NoDefault() def test_modelchoicepreference_requires_model_value(): with pytest.raises(exceptions.MissingModel): prefs.NoModel() def test_get_field_uses_field_kwargs(): class P(StringPreference): name = "test" default = "" field_kwargs = {"required": False} p = P() kwargs = p.get_field_kwargs() assert kwargs["required"] is False def test_preferences_manager_signal(db): global_preferences = global_preferences_registry.manager() global_preferences["no_section"] = False pref = global_preferences.get_db_pref(name="no_section", section=None) receiver = MagicMock() preference_updated.connect(receiver) global_preferences["no_section"] = True assert receiver.call_count == 1 call_args = receiver.call_args[1] assert { "sender": global_preferences.__class__, "section": None, "name": "no_section", "old_value": False, "new_value": True, "instance": pref }.items() <= call_args.items() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736550780.0 django_dynamic_preferences-1.17.0/tests/test_rest_framework.py0000664000175000017500000002414314740324574023740 0ustar00agateagateimport json from decimal import Decimal from django.urls import reverse from dynamic_preferences.api import serializers from dynamic_preferences.api.serializers import GlobalPreferenceSerializer from dynamic_preferences.registries import \ global_preferences_registry as registry from dynamic_preferences.signals import preference_updated from dynamic_preferences.users.registries import ( user_preferences_registry as user_registry, ) from dynamic_preferences.users.serializers import UserPreferenceSerializer try: from unittest.mock import MagicMock except ImportError: from mock import MagicMock def test_can_serialize_preference(db): manager = registry.manager() pref = manager.get_db_pref(section="user", name="max_users") serializer = serializers.GlobalPreferenceSerializer(pref) data = serializer.data assert data["default"] == pref.preference.api_repr(pref.preference.default) assert data["value"] == pref.preference.api_repr(pref.value) assert data["identifier"] == pref.preference.identifier() assert data["section"] == pref.section assert data["name"] == pref.name assert data["verbose_name"] == pref.preference.verbose_name assert data["help_text"] == pref.preference.help_text assert data["field"]["class"] == "IntegerField" assert data["field"]["input_type"] == "number" assert data["field"]["widget"]["class"] == "NumberInput" pref = manager.get_db_pref(section="exam", name="duration") serializer = serializers.GlobalPreferenceSerializer(pref) data = serializer.data assert data["value"] == "03:00:00" pref = manager.get_db_pref(section="company", name="RegistrationDate") serializer = serializers.GlobalPreferenceSerializer(pref) data = serializer.data assert data["value"] == "1998-09-04" pref = manager.get_db_pref(section="child", name="BirthDateTime") serializer = serializers.GlobalPreferenceSerializer(pref) data = serializer.data assert data["value"] == "1992-05-04T03:04:10.000150+00:00" def test_can_change_preference_value_using_serializer(db): manager = registry.manager() pref = manager.get_db_pref(section="user", name="max_users") data = {"value": 666} serializer = serializers.GlobalPreferenceSerializer(pref, data=data) is_valid = serializer.is_valid() assert is_valid is True serializer.save() pref = manager.get_db_pref(section="user", name="max_users") assert pref.value == data["value"] def test_serializer_also_uses_custom_clean_method(db): manager = registry.manager() pref = manager.get_db_pref(section="user", name="max_users") # will fail because of preference cleaning data = {"value": 1001} serializer = serializers.GlobalPreferenceSerializer(pref, data=data) is_valid = serializer.is_valid() assert is_valid is False assert "value" in serializer.errors def test_serializer_includes_additional_data_if_any(fake_user): manager = user_registry.manager(instance=fake_user) pref = manager.get_db_pref(section="user", name="favorite_vegetable") serializer = UserPreferenceSerializer(pref) assert serializer.data["additional_data"][ "choices"] == pref.preference.choices def test_global_preference_list_requires_permission(db, client): url = reverse("api:global-list") # anonymous response = client.get(url) assert response.status_code == 403 client.login(username="test", password="test") response = client.get(url) assert response.status_code == 403 def test_can_list_preferences(admin_client): manager = registry.manager() url = reverse("api:global-list") response = admin_client.get(url) assert response.status_code == 200 payload = json.loads(response.content.decode("utf-8")) assert len(payload) == len(registry.preferences()) for e in payload: pref = manager.get_db_pref(section=e["section"], name=e["name"]) serializers.GlobalPreferenceSerializer(pref) assert pref.preference.identifier() == e["identifier"] def test_can_list_preferences_with_section_filter(admin_client): manager = registry.manager() url = reverse("api:global-list") response = admin_client.get(url, {"section": "user"}) assert response.status_code == 200 payload = json.loads(response.content.decode("utf-8")) assert len(payload) == len(registry.preferences("user")) for e in payload: pref = manager.get_db_pref(section=e["section"], name=e["name"]) serializers.GlobalPreferenceSerializer(pref) assert pref.preference.identifier() == e["identifier"] def test_can_detail_preference(admin_client): manager = registry.manager() pref = manager.get_db_pref(section="user", name="max_users") url = reverse("api:global-detail", kwargs={"pk": pref.preference.identifier()}) response = admin_client.get(url) assert response.status_code == 200 payload = json.loads(response.content.decode("utf-8")) assert pref.preference.identifier(), payload["identifier"] assert pref.value == payload["value"] def test_can_update_preference(admin_client): manager = registry.manager() pref = manager.get_db_pref(section="user", name="max_users") url = reverse("api:global-detail", kwargs={"pk": pref.preference.identifier()}) response = admin_client.patch( url, json.dumps({"value": 16}), content_type="application/json" ) assert response.status_code == 200 pref = manager.get_db_pref(section="user", name="max_users") assert pref.value == 16 def test_can_update_decimal_preference(admin_client): manager = registry.manager() pref = manager.get_db_pref(section="type", name="cost") url = reverse("api:global-detail", kwargs={"pk": pref.preference.identifier()}) response = admin_client.patch( url, json.dumps({"value": "111.11"}), content_type="application/json" ) assert response.status_code == 200 pref = manager.get_db_pref(section="type", name="cost") assert pref.value == Decimal("111.11") def test_can_update_multiple_preferences(admin_client): manager = registry.manager() url = reverse("api:global-bulk") payload = { "user__max_users": 16, "user__registration_allowed": True, } response = admin_client.post( url, json.dumps(payload), content_type="application/json" ) assert response.status_code == 200 pref1 = manager.get_db_pref(section="user", name="max_users") pref2 = manager.get_db_pref(section="user", name="registration_allowed") assert pref1.value == 16 assert pref2.value is True def test_update_preference_returns_validation_error(admin_client): manager = registry.manager() pref = manager.get_db_pref(section="user", name="max_users") url = reverse("api:global-detail", kwargs={"pk": pref.preference.identifier()}) response = admin_client.patch( url, json.dumps({"value": 1001}), content_type="application/json" ) assert response.status_code == 400 payload = json.loads(response.content.decode("utf-8")) assert payload["value"] == ["Wrong value!"] def test_update_multiple_preferences_with_validation_errors_rollback( admin_client): manager = registry.manager() pref = manager.get_db_pref(section="user", name="max_users") url = reverse("api:global-bulk") payload = { "user__max_users": 1001, "user__registration_allowed": True, } response = admin_client.post( url, json.dumps(payload), content_type="application/json" ) assert response.status_code == 400 errors = json.loads(response.content.decode("utf-8")) assert errors[pref.preference.identifier()]["value"] == ["Wrong value!"] pref1 = manager.get_db_pref(section="user", name="max_users") pref2 = manager.get_db_pref(section="user", name="registration_allowed") assert pref1.value == pref1.preference.default assert pref2.value == pref2.preference.default def test_update_preference_send_signal(admin_client): manager = registry.manager() pref = manager.get_db_pref(section="user", name="max_users") receiver = MagicMock() preference_updated.connect(receiver) url = reverse("api:global-detail", kwargs={"pk": pref.preference.identifier()}) response = admin_client.patch( url, json.dumps({"value": 16}), content_type="application/json" ) assert response.status_code == 200 assert receiver.call_count == 1 call_args = receiver.call_args[1] assert { "sender": GlobalPreferenceSerializer, "section": "user", "name": "max_users", "old_value": 100, "new_value": 16, "instance": pref }.items() <= call_args.items() def test_update_multiple_preferences_send_signal(admin_client): manager = registry.manager() max_user_pref = manager.get_db_pref(section="user", name="max_users") registration_allowed_pref = manager.get_db_pref(section="user", name="registration_allowed") receiver = MagicMock() preference_updated.connect(receiver) url = reverse("api:global-bulk") payload = { "user__max_users": 16, "user__registration_allowed": True, } response = admin_client.post( url, json.dumps(payload), content_type="application/json" ) assert response.status_code == 200 assert receiver.call_count == 2 call_args = receiver.call_args_list[0][1] assert { "sender": GlobalPreferenceSerializer, "section": "user", "name": "max_users", "old_value": 100, "new_value": 16, "instance": max_user_pref }.items() <= call_args.items() call_args = receiver.call_args_list[1][1] assert { "sender": GlobalPreferenceSerializer, "section": "user", "name": "registration_allowed", "old_value": False, "new_value": True, "instance": registration_allowed_pref }.items() <= call_args.items() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696213.0 django_dynamic_preferences-1.17.0/tests/test_serializers.py0000644000175000017500000002423514413261625023234 0ustar00agateagateimport pytest from datetime import timezone from decimal import Decimal from datetime import date, timedelta, datetime, time from django.test import override_settings from django.template import defaultfilters from dynamic_preferences import serializers from .test_app.models import BlogEntry, BlogEntryWithNonIntPk @pytest.fixture def blog_entries(db): BlogEntry.objects.bulk_create( [ BlogEntry(title="This is a test", content="Hello World"), BlogEntry(title="This is only a test", content="Hello World"), ] ) BlogEntryWithNonIntPk.objects.bulk_create( [ BlogEntryWithNonIntPk(title="This is a test", content="Hello World"), BlogEntryWithNonIntPk(title="This is only a test", content="Hello World"), ] ) def test_boolean_serialization(): s = serializers.BooleanSerializer assert s.serialize(True) == "True" assert s.serialize(False) == "False" with pytest.raises(s.exception): s.serialize("yolo") def test_boolean_deserialization(): s = serializers.BooleanSerializer for v in s.true: assert s.deserialize(v) is True for v in s.false: assert s.deserialize(v) is False with pytest.raises(s.exception): s.deserialize("I'm a true value") def test_int_serialization(): s = serializers.IntSerializer assert s.serialize(1) == "1" assert s.serialize(666) == "666" assert s.serialize(-144) == "-144" assert s.serialize(0) == "0" assert s.serialize(123456) == "123456" with pytest.raises(s.exception): s.serialize("I'm an integer") def test_decimal_serialization(): s = serializers.DecimalSerializer assert s.serialize(Decimal("1")) == "1" assert s.serialize(Decimal("-1")) == "-1" assert s.serialize(Decimal("-666.6")) == "-666.6" assert s.serialize(Decimal("666.6")) == "666.6" with pytest.raises(s.exception): s.serialize("I'm a decimal") def test_float_serialization(): s = serializers.FloatSerializer assert s.serialize(1.0) == "1.0" assert s.serialize(-1.0) == "-1.0" assert s.serialize(1) == "1.0" assert s.serialize(-1) == "-1.0" assert s.serialize(-666.6) == "-666.6" assert s.serialize(666.6) == "666.6" with pytest.raises(s.exception): s.serialize("I'm a float") def test_float_deserialization(): s = serializers.FloatSerializer assert s.deserialize("1.0") == float("1.0") assert s.deserialize("-1.0") == float("-1.0") assert s.deserialize("-666.6") == float("-666.6") assert s.deserialize("666.6") == float("666.6") with pytest.raises(s.exception): s.serialize("I'm a float") def test_int_deserialization(): s = serializers.DecimalSerializer assert s.deserialize("1") == Decimal("1") assert s.deserialize("-1") == Decimal("-1") assert s.deserialize("-666.6") == Decimal("-666.6") assert s.deserialize("666.6") == Decimal("666.6") with pytest.raises(s.exception): s.serialize("I'm a decimal!") def test_string_serialization(): s = serializers.StringSerializer assert s.serialize("Bonjour") == "Bonjour" assert s.serialize("12") == "12" assert ( s.serialize("I'm a long sentence, but I rock") == "I'm a long sentence, but I rock" ) # check for HTML escaping kwargs = { "escape_html": True, } assert s.serialize( "Please, I don't wanna disappear", **kwargs ) == defaultfilters.force_escape("Please, I don't wanna disappear") with pytest.raises(s.exception): s.serialize(("I", "Want", "To", "Be", "A", "String")) def test_string_deserialization(): s = serializers.StringSerializer assert s.deserialize("Bonjour") == "Bonjour" assert s.deserialize("12") == "12" assert ( s.deserialize("I'm a long sentence, but I rock") == "I'm a long sentence, but I rock" ) # check case where empty string (value can be None) assert s.deserialize(None) == "" assert s.deserialize("") == "" kwargs = { "escape_html": True, } assert s.deserialize( s.serialize("Please, I don't wanna disappear", **kwargs) ) == defaultfilters.force_escape("Please, I don't wanna disappear") def test_duration_serialization(): s = serializers.DurationSerializer assert s.serialize(timedelta(minutes=1)) == "00:01:00" assert s.serialize(timedelta(milliseconds=1)) == "00:00:00.001000" assert s.serialize(timedelta(weeks=1)) == "7 00:00:00" with pytest.raises(s.exception): s.serialize("Not a timedelta") def test_duration_deserialization(): s = serializers.DurationSerializer assert s.deserialize("7 00:00:00") == timedelta(weeks=1) with pytest.raises(s.exception): s.deserialize("Invalid duration string") def test_date_serialization(): s = serializers.DateSerializer assert s.serialize(date(2017, 10, 5)) == "2017-10-05" with pytest.raises(s.exception): s.serialize("Not a date") def test_date_deserialization(): s = serializers.DateSerializer assert s.deserialize("1900-01-01") == date(1900, 1, 1) with pytest.raises(s.exception): s.deserialize("Invalid date string") def test_datetime_serialization(): s = serializers.DateTimeSerializer # If TZ is enabled default timezone is America/Chicago # https://docs.djangoproject.com/en/1.11/ref/settings/#std:setting-TIME_ZONE assert ( s.serialize(datetime(2017, 10, 5, 23, 45, 1, 792346)) == "2017-10-05T23:45:01.792346-05:00" ) with override_settings(USE_TZ=False): assert s.serialize( datetime(2017, 10, 5, 23, 45, 1, 792346) ), "2017-10-05T23:45:01.792346" with pytest.raises(s.exception) as ex: s.serialize("a string") assert ex.exception.args == ( "Cannot serialize, value 'a string' is not a datetime object", ) def test_datetime_deserialization(): s = serializers.DateTimeSerializer assert s.deserialize("2017-10-05T23:45:01.792346") == datetime( 2017, 10, 5, 23, 45, 1, 792346 ) assert s.deserialize("2017-10-05T23:45:01.792346+00:00") == datetime( 2017, 10, 5, 23, 45, 1, 792346, tzinfo=timezone.utc ) with pytest.raises(s.exception) as ex: s.deserialize("abcd") assert ex.exception.args == ( "Value abcd cannot be converted to a datetime object", ) def test_time_serialization(): s = serializers.TimeSerializer assert s.serialize(time(hour=5)) == "05:00:00" assert s.serialize(time(minute=30)) == "00:30:00" assert s.serialize(time(23, 59, 59, 999999)) == "23:59:59.999999" with pytest.raises(s.exception): s.serialize("Not a time") def test_time_deserialization(): s = serializers.TimeSerializer assert s.deserialize("23:00:00") == time(hour=23) with pytest.raises(s.exception): s.deserialize("Invalid time string") def test_multiple_serialization(): s = serializers.MultipleSerializer assert s.serialize(["a", "b", "c"]) == "a,b,c" assert ( s.serialize(["key,with,comma", "b", "another,key,with,comma"]) == "another,,key,,with,,comma,b,key,,with,,comma" ) with pytest.raises(s.exception): s.serialize(["a", "", "c"]) def test_multiple_deserialization(): s = serializers.MultipleSerializer assert s.deserialize("a,b,c") == ["a", "b", "c"] assert s.deserialize("key,,with,,comma,b,another,,key,,with,,comma") == [ "key,with,comma", "b", "another,key,with,comma", ] def test_model_multiple_serialization(blog_entries): s = serializers.ModelMultipleSerializer(BlogEntry) blog_entries = BlogEntry.objects.all() assert s.serialize(blog_entries), s.separator.join( map(str, sorted(list(blog_entries.values_list("pk", flat=True)))) ) def test_model_multiple_deserialization(blog_entries): s = serializers.ModelMultipleSerializer(BlogEntry) blog_entries = BlogEntry.objects.all() pks = s.separator.join( map(str, sorted(list(blog_entries.values_list("pk", flat=True)))) ) assert list(s.deserialize(pks)) == list(blog_entries) def test_model_multiple_single_serialization(blog_entries): s = serializers.ModelMultipleSerializer(BlogEntry) blog_entry = BlogEntry.objects.all().first() assert s.serialize(blog_entry) == s.separator.join(map(str, [blog_entry.pk])) def test_model_multiple_serialization_with_non_int_pk(blog_entries): s = serializers.ModelMultipleSerializer(BlogEntryWithNonIntPk) blog_entries = BlogEntryWithNonIntPk.objects.all() assert s.serialize(blog_entries) == s.separator.join( map(str, sorted(list(blog_entries.values_list("pk", flat=True)))) ) def test_model_multiple_deserialization_with_non_int_pk(blog_entries): s = serializers.ModelMultipleSerializer(BlogEntryWithNonIntPk) blog_entries = BlogEntryWithNonIntPk.objects.all() pks = s.separator.join( map(str, sorted(list(blog_entries.values_list("pk", flat=True)))) ) deserialized_ids = sorted([instance.pk for instance in s.deserialize(pks)]) blog_entries_ids = sorted([entry.pk for entry in blog_entries]) assert deserialized_ids, blog_entries_ids def test_model_multiple_single_serialization_with_non_int_pk(blog_entries): s = serializers.ModelMultipleSerializer(BlogEntryWithNonIntPk) blog_entry = BlogEntryWithNonIntPk.objects.all().first() assert s.serialize(blog_entry) == s.separator.join(map(str, [blog_entry.pk])) def test_model_multiple_to_db_empty(blog_entries): result = serializers.ModelMultipleSerializer(BlogEntry).to_db([]) assert result is None def test_model_multiple_to_db_multiple(blog_entries): entry1 = BlogEntry.objects.get(title="This is a test",) entry2 = BlogEntry.objects.get(title="This is only a test",) result = serializers.ModelMultipleSerializer(BlogEntry).to_db([ entry1, entry2, ]) assert result == f'{entry1.pk},{entry2.pk}' def test_model_multiple_to_db_invalid(blog_entries): with pytest.raises(ValueError, match=r"Cannot handle value.* of type .*"): serializers.ModelMultipleSerializer(BlogEntry).to_db('invalid') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/tests/test_tutorial.py0000644000175000017500000000174214413261622022536 0ustar00agateagatefrom dynamic_preferences.registries import global_preferences_registry from dynamic_preferences.models import GlobalPreferenceModel from dynamic_preferences.users.models import UserPreferenceModel def test_quickstart(henri): global_preferences = global_preferences_registry.manager() assert global_preferences["user__registration_allowed"] is False global_preferences["user__registration_allowed"] = True assert global_preferences["user__registration_allowed"] is True assert ( GlobalPreferenceModel.objects.get( section="user", name="registration_allowed" ).value is True ) assert henri.preferences["misc__favourite_colour"] == "Green" henri.preferences["misc__favourite_colour"] = "Blue" assert henri.preferences["misc__favourite_colour"] == "Blue" assert ( UserPreferenceModel.objects.get( section="misc", name="favourite_colour", instance=henri ).value == "Blue" ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1697360004.0 django_dynamic_preferences-1.17.0/tests/test_types.py0000644000175000017500000002036114512724204022035 0ustar00agateagateimport os import decimal import pickle import pytest from datetime import date, timedelta, datetime, time from django import forms from django.db.models import signals from django.core.files.uploadedfile import SimpleUploadedFile from django.conf import settings from dynamic_preferences.models import GlobalPreferenceModel from dynamic_preferences.settings import preferences_settings from dynamic_preferences.registries import global_preferences_registry from dynamic_preferences import types from .test_app.models import BlogEntry @pytest.fixture def no_validate_names(settings): settings.DYNAMIC_PREFERENCES = {"VALIDATE_NAMES": False} @pytest.fixture def blog_entry(db): return BlogEntry.objects.create(title="Hello", content="World") def test_default_accepts_callable(no_validate_names): class P(types.IntPreference): def get_default(self): return 4 assert P().get("default") == 4 def test_getter(no_validate_names): class PNoGetter(types.IntPreference): default = 1 help_text = "Hello" class PGetter(types.IntPreference): def get_default(self): return 1 def get_help_text(self): return "Hello" p_no_getter = PNoGetter() p_getter = PGetter() for attribute, expected in [("default", 1), ("help_text", "Hello")]: assert p_no_getter.get(attribute) == expected assert p_getter.get(attribute) == expected def test_field(no_validate_names): class P(types.IntPreference): default = 1 verbose_name = "P" p = P() assert p.field.initial == 1 assert p.field.label == "P" assert p.field.__class__ == forms.IntegerField def test_boolean_field_class_instantiation(no_validate_names): class P(types.BooleanPreference): default = False preference = P() assert preference.field.initial is False def test_char_field_class_instantiation(no_validate_names): class P(types.StringPreference): default = "hello world!" preference = P() assert preference.field.initial == "hello world!" def test_longstring_preference_widget(no_validate_names): class P(types.LongStringPreference): default = "hello world!" preference = P() assert isinstance(preference.field.widget, forms.Textarea) is True def test_decimal_preference(no_validate_names): class P(types.DecimalPreference): default = decimal.Decimal("2.5") preference = P() assert preference.field.initial == decimal.Decimal("2.5") def test_float_preference(no_validate_names): class P(types.FloatPreference): default = 0.35 preference = P() assert preference.field.initial == 0.35 assert preference.field.initial != 0.3 assert preference.field.initial != 0.3001 def test_duration_preference(no_validate_names): class P(types.DurationPreference): default = timedelta(0) preference = P() assert preference.field.initial == timedelta(0) def test_date_preference(no_validate_names): class P(types.DatePreference): default = date.today() preference = P() assert preference.field.initial == date.today() def test_datetime_preference(no_validate_names): initial_date_time = datetime(2017, 10, 4, 23, 7, 20, 682380) class P(types.DateTimePreference): default = initial_date_time preference = P() assert preference.field.initial == initial_date_time def test_time_preference(no_validate_names): class P(types.TimePreference): default = time(0) preference = P() assert preference.field.initial == time(0) def test_file_preference_defaults_to_none(no_validate_names): class P(types.FilePreference): pass preference = P() assert preference.field.initial is None def test_can_get_upload_path(no_validate_names): class P(types.FilePreference): pass p = P() assert p.get_upload_path() == ( preferences_settings.FILE_PREFERENCE_UPLOAD_DIR + "/" + p.identifier() ) def test_file_preference_store_file_path(db): f = SimpleUploadedFile( "test_file_1ce410e5-6814-4910-afd7-be1486d3644f.txt", "hello world".encode("utf-8"), ) p = global_preferences_registry.get(section="blog", name="logo") manager = global_preferences_registry.manager() manager["blog__logo"] = f assert manager["blog__logo"].read() == b"hello world" assert manager["blog__logo"].url == os.path.join( settings.MEDIA_URL, p.get_upload_path(), f.name ) assert manager["blog__logo"].path == os.path.join( settings.MEDIA_ROOT, p.get_upload_path(), f.name ) def test_file_preference_conflicting_file_names(db): """ f2 should have a different file name to f, since Django storage needs to differentiate between the two """ f = SimpleUploadedFile( "test_file_c95d02ef-0e5d-4d36-98c0-1b54505860d0.txt", "hello world".encode("utf-8"), ) f2 = SimpleUploadedFile( "test_file_c95d02ef-0e5d-4d36-98c0-1b54505860d0.txt", "hello world 2".encode("utf-8"), ) manager = global_preferences_registry.manager() manager["blog__logo"] = f manager["blog__logo2"] = f2 assert manager["blog__logo2"].read() == b"hello world 2" assert manager["blog__logo"].read() == b"hello world" assert manager["blog__logo"].url != manager["blog__logo2"].url assert manager["blog__logo"].path != manager["blog__logo2"].path def test_can_delete_file_preference(db): f = SimpleUploadedFile( "test_file_bf2e72ef-092f-4a71-9cda-f2442d6166d0.txt", "hello world".encode("utf-8"), ) p = global_preferences_registry.get(section="blog", name="logo") manager = global_preferences_registry.manager() manager["blog__logo"] = f path = os.path.join(settings.MEDIA_ROOT, p.get_upload_path(), f.name) assert os.path.exists(path) is True manager["blog__logo"].delete() assert os.path.exists(path) is False def test_file_preference_api_repr_returns_path(db): f = SimpleUploadedFile( "test_file_24485a80-8db9-4191-ae49-da7fe2013794.txt", "hello world".encode("utf-8"), ) p = global_preferences_registry.get(section="blog", name="logo") manager = global_preferences_registry.manager() manager["blog__logo"] = f f = manager["blog__logo"] assert p.api_repr(f) == f.url def test_file_preference_if_pickleable(db): manager = global_preferences_registry.manager() f = SimpleUploadedFile( "test_file_24485a80-8db9-4191-ae49-da7fe2013794.txt", "hello world".encode("utf-8"), ) try: manager["blog__logo"] = f pickle.dumps(manager["blog__logo"]) except Exception: pytest.fail("FilePreference not pickleable") def test_choice_preference(fake_user): fake_user.preferences["user__favorite_vegetable"] = "C" assert fake_user.preferences["user__favorite_vegetable"] == "C" fake_user.preferences["user__favorite_vegetable"] = "P" assert fake_user.preferences["user__favorite_vegetable"] == "P" with pytest.raises(forms.ValidationError): fake_user.preferences["user__favorite_vegetable"] = "Nope" def test_multiple_choice_preference(fake_user): fake_user.preferences["user__favorite_vegetables"] = ["C", "T"] assert fake_user.preferences["user__favorite_vegetables"] == ["C", "T"] fake_user.preferences["user__favorite_vegetables"] = ["P"] assert fake_user.preferences["user__favorite_vegetables"] == ["P"] with pytest.raises(forms.ValidationError): fake_user.preferences["user__favorite_vegetables"] = ["Nope", "C"] def test_model_choice_preference(blog_entry): global_preferences = global_preferences_registry.manager() global_preferences["blog__featured_entry"] = blog_entry in_db = GlobalPreferenceModel.objects.get(section="blog", name="featured_entry") assert in_db.value == blog_entry assert in_db.raw_value == str(blog_entry.pk) def test_deleting_model_also_delete_preference(blog_entry): global_preferences = global_preferences_registry.manager() global_preferences["blog__featured_entry"] = blog_entry assert len(signals.pre_delete.receivers) > 0 blog_entry.delete() with pytest.raises(GlobalPreferenceModel.DoesNotExist): GlobalPreferenceModel.objects.get(section="blog", name="featured_entry") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1680696210.0 django_dynamic_preferences-1.17.0/tests/test_user_preferences.py0000644000175000017500000001774514413261622024244 0ustar00agateagateimport json import pytest from django.urls import reverse from django.contrib.auth.models import User from django.db import IntegrityError from dynamic_preferences.users.registries import user_preferences_registry as registry from dynamic_preferences.users.models import UserPreferenceModel from dynamic_preferences.users import serializers from dynamic_preferences.managers import PreferencesManager from dynamic_preferences.users.forms import user_preference_form_builder def test_adding_user_create_default_preferences(db): u = User.objects.create(username="post_create") assert len(u.preferences) == len(registry.preferences()) def test_manager_is_attached_to_each_referenced_instance(fake_user): assert isinstance(fake_user.preferences, PreferencesManager) is True def test_preference_is_saved_to_database(fake_user): fake_user.preferences["test__TestUserPref1"] = "new test value" assert UserPreferenceModel.objects.filter( section="test", name="TestUserPref1", instance=fake_user ).exists() assert fake_user.preferences["test__TestUserPref1"] == "new test value" def test_per_instance_preference_stay_unique_in_db(fake_user): fake_user.preferences["test__TestUserPref1"] = "new value" duplicate = UserPreferenceModel( section="test", name="TestUserPref1", instance=fake_user ) with pytest.raises(IntegrityError): duplicate.save() def test_preference_value_set_to_default(fake_user): pref = registry.get("TestUserPref1", "test") value = fake_user.preferences["test__TestUserPref1"] assert pref.default == value assert UserPreferenceModel.objects.filter( section="test", name="TestUserPref1", instance=fake_user ).exists() def test_user_preference_model_manager_to_dict(fake_user): expected = { "misc__favourite_colour": "Green", "misc__is_zombie": True, "user__favorite_vegetable": "C", "user__favorite_vegetables": ["C", "P"], "test__SUserStringPref": "Hello world!", "test__SiteBooleanPref": False, "test__TestUserPref1": "default value", "test__TestUserPref2": "default value", } assert fake_user.preferences.all() == expected def test_can_build_user_preference_form_from_sections(fake_admin): form = user_preference_form_builder(instance=fake_admin, section="test")() assert len(form.fields) == 4 def test_user_preference_form_is_bound_with_current_user(henri_client, henri): assert ( UserPreferenceModel.objects.get_or_create( instance=henri, section="misc", name="favourite_colour" )[0].value == "Green" ) assert ( UserPreferenceModel.objects.get_or_create( instance=henri, section="misc", name="is_zombie" )[0].value is True ) url = reverse("dynamic_preferences:user.section", kwargs={"section": "misc"}) response = henri_client.post( url, {"misc__favourite_colour": "Purple", "misc__is_zombie": False}, follow=True ) assert response.status_code == 200 assert henri.preferences["misc__favourite_colour"] == "Purple" assert henri.preferences["misc__is_zombie"] is False def test_preference_list_requires_authentication(client): url = reverse("api:user-list") # anonymous response = client.get(url) assert response.status_code == 403 def test_can_list_preferences(user_client, fake_user): manager = registry.manager(instance=fake_user) url = reverse("api:user-list") response = user_client.get(url) assert response.status_code == 200 payload = json.loads(response.content.decode("utf-8")) assert len(payload) == len(registry.preferences()) for e in payload: pref = manager.get_db_pref(section=e["section"], name=e["name"]) serializers.UserPreferenceSerializer(pref) assert pref.preference.identifier() == e["identifier"] def test_can_list_preference_of_requesting_user(fake_user, user_client): second_user = User( username="user2", email="user2@user.com", is_superuser=True, is_staff=True ) second_user.set_password("test") second_user.save() manager = registry.manager(instance=fake_user) url = reverse("api:user-list") response = user_client.get(url) assert response.status_code == 200 payload = json.loads(response.content.decode("utf-8")) assert len(payload) == len(registry.preferences()) url = reverse("api:user-list") user_client.login(username="user2", password="test") response = user_client.get(url) assert response.status_code == 200 payload = json.loads(response.content.decode("utf-8")) # This should be 7 because each user gets 7 preferences by default. assert len(payload) == 8 for e in payload: pref = manager.get_db_pref(section=e["section"], name=e["name"]) serializers.UserPreferenceSerializer(pref) assert pref.preference.identifier() == e["identifier"] def test_can_detail_preference(fake_user, user_client): manager = registry.manager(instance=fake_user) pref = manager.get_db_pref(section="user", name="favorite_vegetable") url = reverse("api:user-detail", kwargs={"pk": pref.preference.identifier()}) response = user_client.get(url) assert response.status_code == 200 payload = json.loads(response.content.decode("utf-8")) assert pref.preference.identifier() == payload["identifier"] assert pref.value == payload["value"] def test_can_update_preference(fake_user, user_client): manager = registry.manager(instance=fake_user) pref = manager.get_db_pref(section="user", name="favorite_vegetable") url = reverse("api:user-detail", kwargs={"pk": pref.preference.identifier()}) response = user_client.patch( url, json.dumps({"value": "P"}), content_type="application/json" ) assert response.status_code == 200 pref = manager.get_db_pref(section="user", name="favorite_vegetable") assert pref.value == "P" def test_can_update_multiple_preferences(fake_user, user_client): manager = registry.manager(instance=fake_user) manager.get_db_pref(section="user", name="favorite_vegetable") url = reverse("api:user-bulk") payload = { "user__favorite_vegetable": "C", "misc__favourite_colour": "Blue", } response = user_client.post( url, json.dumps(payload), content_type="application/json" ) assert response.status_code == 200 pref1 = manager.get_db_pref(section="user", name="favorite_vegetable") pref2 = manager.get_db_pref(section="misc", name="favourite_colour") assert pref1.value == "C" assert pref2.value == "Blue" def test_update_preference_returns_validation_error(fake_user, user_client): manager = registry.manager(instance=fake_user) pref = manager.get_db_pref(section="user", name="favorite_vegetable") url = reverse("api:user-detail", kwargs={"pk": pref.preference.identifier()}) response = user_client.patch( url, json.dumps({"value": "Z"}), content_type="application/json" ) assert response.status_code == 400 payload = json.loads(response.content.decode("utf-8")) assert "valid choice" in payload["value"][0] def test_update_multiple_preferences_with_validation_errors_rollback( user_client, fake_user ): manager = registry.manager(instance=fake_user) pref = manager.get_db_pref(section="user", name="favorite_vegetable") url = reverse("api:user-bulk") payload = { "user__favorite_vegetable": "Z", "misc__favourite_colour": "Blue", } response = user_client.post( url, json.dumps(payload), content_type="application/json" ) assert response.status_code == 400 errors = json.loads(response.content.decode("utf-8")) assert "valid choice" in errors[pref.preference.identifier()]["value"][0] pref1 = manager.get_db_pref(section="user", name="favorite_vegetable") pref2 = manager.get_db_pref(section="misc", name="favourite_colour") assert pref1.value == pref1.preference.default assert pref2.value == pref2.preference.default