././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1598001367.297035 django-dynamic-preferences-1.10.1/0000775000175000017500000000000000000000000017144 5ustar00agateagate00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/AUTHORS.rst0000644000175000017500000000163700000000000021030 0ustar00agateagate00000000000000======= Credits ======= Development Lead ---------------- * Eliot Berriot 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/CONTRIBUTING.rst0000644000175000017500000000740600000000000021612 0ustar00agateagate00000000000000============ 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/EliotBerriot/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/EliotBerriot/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, 2.7, and 3.4. Check https://travis-ci.org/EliotBerriot/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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1598001265.0 django-dynamic-preferences-1.10.1/HISTORY.rst0000664000175000017500000004211500000000000021042 0ustar00agateagate00000000000000.. :changelog: Changelog ========= 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 - @EliotBerriot 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: - @EliotBerriot - @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/EliotBerriot/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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/LICENSE0000644000175000017500000000271600000000000020155 0ustar00agateagate00000000000000Copyright (c) 2014, Eliot Berriot 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.././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/MANIFEST.in0000644000175000017500000000037400000000000020704 0ustar00agateagate00000000000000include 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 ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1598001367.297035 django-dynamic-preferences-1.10.1/PKG-INFO0000664000175000017500000001300200000000000020235 0ustar00agateagate00000000000000Metadata-Version: 1.1 Name: django-dynamic-preferences Version: 1.10.1 Summary: Dynamic global and instance settings for your django project Home-page: https://github.com/EliotBerriot/django-dynamic-preferences Author: Eliot Berriot Author-email: contact@eliotberriot.com License: BSD Description: ============================= 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://travis-ci.org/EliotBerriot/django-dynamic-preferences.svg?branch=master :target: https://travis-ci.org/EliotBerriot/django-dynamic-preferences .. image:: https://travis-ci.org/EliotBerriot/django-dynamic-preferences.svg?branch=develop :target: https://travis-ci.org/EliotBerriot/django-dynamic-preferences .. image:: https://img.shields.io/codecov/c/github/EliotBerriot/django-dynamic-preferences/master.svg :target: https://codecov.io/github/EliotBerriot/django-dynamic-preferences?branch=master .. 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 .. warning:: There is a critical bug in version 1.2 that can result in dataloss. Please upgrade to 1.3 as soon as possible and do not use 1.2 in production. See `#81 `_ for more details. 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 2.7 and 3.4, 3.5 and 3.6, with django >=1.8. 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 Keywords: django-dynamic-preferences Platform: UNKNOWN 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 :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/README.rst0000644000175000017500000000763500000000000020644 0ustar00agateagate00000000000000============================= 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://travis-ci.org/EliotBerriot/django-dynamic-preferences.svg?branch=master :target: https://travis-ci.org/EliotBerriot/django-dynamic-preferences .. image:: https://travis-ci.org/EliotBerriot/django-dynamic-preferences.svg?branch=develop :target: https://travis-ci.org/EliotBerriot/django-dynamic-preferences .. image:: https://img.shields.io/codecov/c/github/EliotBerriot/django-dynamic-preferences/master.svg :target: https://codecov.io/github/EliotBerriot/django-dynamic-preferences?branch=master .. 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 .. warning:: There is a critical bug in version 1.2 that can result in dataloss. Please upgrade to 1.3 as soon as possible and do not use 1.2 in production. See `#81 `_ for more details. 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 2.7 and 3.4, 3.5 and 3.6, with django >=1.8. 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 ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1598001367.293035 django-dynamic-preferences-1.10.1/django_dynamic_preferences.egg-info/0000775000175000017500000000000000000000000026165 5ustar00agateagate00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1598001367.0 django-dynamic-preferences-1.10.1/django_dynamic_preferences.egg-info/PKG-INFO0000644000175000017500000001300200000000000027254 0ustar00agateagate00000000000000Metadata-Version: 1.1 Name: django-dynamic-preferences Version: 1.10.1 Summary: Dynamic global and instance settings for your django project Home-page: https://github.com/EliotBerriot/django-dynamic-preferences Author: Eliot Berriot Author-email: contact@eliotberriot.com License: BSD Description: ============================= 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://travis-ci.org/EliotBerriot/django-dynamic-preferences.svg?branch=master :target: https://travis-ci.org/EliotBerriot/django-dynamic-preferences .. image:: https://travis-ci.org/EliotBerriot/django-dynamic-preferences.svg?branch=develop :target: https://travis-ci.org/EliotBerriot/django-dynamic-preferences .. image:: https://img.shields.io/codecov/c/github/EliotBerriot/django-dynamic-preferences/master.svg :target: https://codecov.io/github/EliotBerriot/django-dynamic-preferences?branch=master .. 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 .. warning:: There is a critical bug in version 1.2 that can result in dataloss. Please upgrade to 1.3 as soon as possible and do not use 1.2 in production. See `#81 `_ for more details. 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 2.7 and 3.4, 3.5 and 3.6, with django >=1.8. 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 Keywords: django-dynamic-preferences Platform: UNKNOWN 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 :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1598001367.0 django-dynamic-preferences-1.10.1/django_dynamic_preferences.egg-info/SOURCES.txt0000644000175000017500000000514000000000000030047 0ustar00agateagate00000000000000AUTHORS.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/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././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1598001367.0 django-dynamic-preferences-1.10.1/django_dynamic_preferences.egg-info/dependency_links.txt0000644000175000017500000000000100000000000032231 0ustar00agateagate00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/django_dynamic_preferences.egg-info/not-zip-safe0000644000175000017500000000000100000000000030411 0ustar00agateagate00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1598001367.0 django-dynamic-preferences-1.10.1/django_dynamic_preferences.egg-info/requires.txt0000644000175000017500000000005200000000000030560 0ustar00agateagate00000000000000django>=1.11 six persisting_theory>=0.2.1 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1598001367.0 django-dynamic-preferences-1.10.1/django_dynamic_preferences.egg-info/top_level.txt0000644000175000017500000000002400000000000030711 0ustar00agateagate00000000000000dynamic_preferences ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1598001367.293035 django-dynamic-preferences-1.10.1/dynamic_preferences/0000775000175000017500000000000000000000000023151 5ustar00agateagate00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1598001265.0 django-dynamic-preferences-1.10.1/dynamic_preferences/__init__.py0000664000175000017500000000014000000000000025255 0ustar00agateagate00000000000000__version__ = "1.10.1" default_app_config = "dynamic_preferences.apps.DynamicPreferencesConfig" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/admin.py0000644000175000017500000000652400000000000024620 0ustar00agateagate00000000000000from 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") 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 ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1598001367.293035 django-dynamic-preferences-1.10.1/dynamic_preferences/api/0000775000175000017500000000000000000000000023722 5ustar00agateagate00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/api/__init__.py0000644000175000017500000000000000000000000026017 0ustar00agateagate00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/api/serializers.py0000644000175000017500000000400600000000000026626 0ustar00agateagate00000000000000from rest_framework import serializers from dynamic_preferences.models import GlobalPreferenceModel 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): instance.value = validated_data['value'] instance.save() return instance class GlobalPreferenceSerializer(PreferenceSerializer): pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/api/viewsets.py0000644000175000017500000001330700000000000026147 0ustar00agateagate00000000000000from 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 ] query = queries[0] 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 binded to the preferences """ raise NotImplementedError ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/apps.py0000644000175000017500000000134700000000000024471 0ustar00agateagate00000000000000from 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 class DynamicPreferencesConfig(AppConfig): name = 'dynamic_preferences' verbose_name = _("Dynamic Preferences") def ready(self): 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/exceptions.py0000644000175000017500000000206100000000000025701 0ustar00agateagate00000000000000 class 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' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/forms.py0000644000175000017500000001176600000000000024662 0ustar00agateagate00000000000000from six import string_types from 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, string_types): 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 ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1598001367.293035 django-dynamic-preferences-1.10.1/dynamic_preferences/locale/0000775000175000017500000000000000000000000024410 5ustar00agateagate00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1598001367.293035 django-dynamic-preferences-1.10.1/dynamic_preferences/locale/ar/0000775000175000017500000000000000000000000025012 5ustar00agateagate00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1598001367.293035 django-dynamic-preferences-1.10.1/dynamic_preferences/locale/ar/LC_MESSAGES/0000775000175000017500000000000000000000000026577 5ustar00agateagate00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/locale/ar/LC_MESSAGES/django.mo0000644000175000017500000000203400000000000030373 0ustar00agateagate00000000000000Þ• 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 القيمة Ø§Ù„Ø§ÙØªØ±Ø§Ø¶ÙŠØ©Ø§Ù„ØªÙØ¶ÙŠÙ„ات Ø§Ù„Ø¯ÙŠÙ†Ø§Ù…ÙŠÙƒÙŠØ©Ø§Ù„ØªÙØ¶ÙŠÙ„ Ø§Ù„Ø¹Ø§Ù…Ø§Ù„ØªÙØ¶ÙŠÙ„ العامنص المساعدةالاسمالقيمة الأوليةإسم القسمإرسالاسم مطول././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/locale/ar/LC_MESSAGES/django.po0000644000175000017500000000262100000000000030400 0ustar00agateagate00000000000000# 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 "إرسال" ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1598001367.293035 django-dynamic-preferences-1.10.1/dynamic_preferences/locale/de/0000775000175000017500000000000000000000000025000 5ustar00agateagate00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1598001367.293035 django-dynamic-preferences-1.10.1/dynamic_preferences/locale/de/LC_MESSAGES/0000775000175000017500000000000000000000000026565 5ustar00agateagate00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/locale/de/LC_MESSAGES/django.mo0000644000175000017500000000202200000000000030356 0ustar00agateagate00000000000000Þ•Œü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././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/locale/de/LC_MESSAGES/django.po0000644000175000017500000000272700000000000030375 0ustar00agateagate00000000000000# 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" ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1598001367.293035 django-dynamic-preferences-1.10.1/dynamic_preferences/locale/fr/0000775000175000017500000000000000000000000025017 5ustar00agateagate00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1598001367.293035 django-dynamic-preferences-1.10.1/dynamic_preferences/locale/fr/LC_MESSAGES/0000775000175000017500000000000000000000000026604 5ustar00agateagate00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/locale/fr/LC_MESSAGES/django.mo0000644000175000017500000000157200000000000030406 0ustar00agateagate00000000000000Þ• 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é././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/locale/fr/LC_MESSAGES/django.po0000644000175000017500000000235400000000000030410 0ustar00agateagate00000000000000# 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" ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1598001367.293035 django-dynamic-preferences-1.10.1/dynamic_preferences/management/0000775000175000017500000000000000000000000025265 5ustar00agateagate00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/management/__init__.py0000644000175000017500000000003400000000000027371 0ustar00agateagate00000000000000__author__ = 'eliotberriot' ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1598001367.297035 django-dynamic-preferences-1.10.1/dynamic_preferences/management/commands/0000775000175000017500000000000000000000000027066 5ustar00agateagate00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/management/commands/__init__.py0000644000175000017500000000003400000000000031172 0ustar00agateagate00000000000000__author__ = 'eliotberriot' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/management/commands/checkpreferences.py0000644000175000017500000000372000000000000032737 0ustar00agateagate00000000000000from django.core.management.base import BaseCommand, CommandError from dynamic_preferences.exceptions import NotFoundInRegistry from dynamic_preferences.models import GlobalPreferenceModel from dynamic_preferences.registries import preference_models, global_preferences_registry import logging logger = logging.getLogger(__name__) 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: pref = 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" def handle(self, *args, **options): # Create needed preferences # Global logger.info('Creating missing global preferences...') manager = global_preferences_registry.manager() manager.all() deleted = delete_preferences(GlobalPreferenceModel.objects.all()) logger.info("Deleted {0} global preferences".format(len(deleted))) for preference_model, registry in preference_models.items(): deleted = delete_preferences(preference_model.objects.all()) logger.info("Deleted {0} {1} preferences".format(len(deleted), preference_model.__name__)) if not hasattr(preference_model, 'get_instance_model'): continue logger.info('Creating missing preferences for {0} model...'.format(preference_model.get_instance_model().__name__)) for instance in preference_model.get_instance_model().objects.all(): instance.preferences.all() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/managers.py0000644000175000017500000001750500000000000025326 0ustar00agateagate00000000000000import collections from .settings import preferences_settings from .exceptions import CachedValueNotFound, DoesNotExist from .signals import preference_updated class PreferencesManager(collections.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['default'] 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, pref): """ Update/create the cache value for the given preference model instance """ 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 self.cache.set(key, value, None) 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) 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} 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: self.to_cache(db_pref) a[preference.identifier()] = db_pref.value return a ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1598001367.297035 django-dynamic-preferences-1.10.1/dynamic_preferences/migrations/0000775000175000017500000000000000000000000025325 5ustar00agateagate00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/migrations/0001_initial.py0000644000175000017500000000215600000000000027772 0ustar00agateagate00000000000000# -*- 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')]), ), ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/migrations/0002_auto_20150712_0332.py0000644000175000017500000000125700000000000030743 0ustar00agateagate00000000000000# -*- 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), ), ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/migrations/0003_auto_20151223_1407.py0000644000175000017500000000137400000000000030746 0ustar00agateagate00000000000000# -*- 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, ), ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/migrations/0004_move_user_model.py0000644000175000017500000000100200000000000031515 0ustar00agateagate00000000000000# -*- 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/EliotBerriot/django-dynamic-preferences/pull/142 operations = [] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/migrations/0005_auto_20181120_0848.py0000644000175000017500000000153500000000000030756 0ustar00agateagate00000000000000# -*- 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'), ), ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/migrations/0006_auto_20191001_2236.py0000644000175000017500000000076000000000000030746 0ustar00agateagate00000000000000# 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'), ), ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/migrations/__init__.py0000644000175000017500000000000000000000000027422 0ustar00agateagate00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/models.py0000644000175000017500000000732400000000000025012 0ustar00agateagate00000000000000""" 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/preferences.py0000644000175000017500000000614500000000000026030 0ustar00agateagate00000000000000""" 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('^\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]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/processors.py0000644000175000017500000000051300000000000025722 0ustar00agateagate00000000000000from .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()} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1598001261.0 django-dynamic-preferences-1.10.1/dynamic_preferences/registries.py0000664000175000017500000001744000000000000025711 0ustar00agateagate00000000000000from 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 throught registered preference models in order to get the instance class # and check if instance is and 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() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767504.0 django-dynamic-preferences-1.10.1/dynamic_preferences/serializers.py0000664000175000017500000003004200000000000026056 0ustar00agateagate00000000000000from __future__ import unicode_literals import decimal import os from datetime import date, timedelta, datetime, time 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 utc, is_aware, make_aware, make_naive, get_default_timezone from six import string_types, text_type 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 text_type(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 text_type(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, float): raise cls.exception('FloatSerializer can only serialize Float instances') return 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, string_types): 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 = int(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 value = list(value.values_list('pk', flat=True)) if 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() pks = value.split(",") return self.model.objects.filter(pk__in=pks) 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) # 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 self.instance = FakeInstance() class FakeField(object): """ FieldFile needs a field object to generate a filename, persist and delete files, so we are effectively mocking that. """ name = 'noop' max_length = 10000 def generate_filename(field, instance, name): return os.path.join( self.preference.get_upload_path(), f.name) self.field = FakeField() 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, 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/settings.py0000644000175000017500000000413300000000000025362 0ustar00agateagate00000000000000# 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_USER_PREFERENCES': True, 'ENABLE_CACHE': True, '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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/signals.py0000644000175000017500000000017600000000000025165 0ustar00agateagate00000000000000from django.dispatch import Signal preference_updated = Signal(providing_args=("section", "name", "old_value", "new_value")) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1598001367.293035 django-dynamic-preferences-1.10.1/dynamic_preferences/templates/0000775000175000017500000000000000000000000025147 5ustar00agateagate00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1598001367.297035 django-dynamic-preferences-1.10.1/dynamic_preferences/templates/dynamic_preferences/0000775000175000017500000000000000000000000031154 5ustar00agateagate00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/templates/dynamic_preferences/base.html0000644000175000017500000000072300000000000032754 0ustar00agateagate00000000000000
{% block content %}{% endblock %}
././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/templates/dynamic_preferences/form.html0000644000175000017500000000075300000000000033010 0ustar00agateagate00000000000000{% 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 %} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/templates/dynamic_preferences/sections.html0000644000175000017500000000052100000000000033665 0ustar00agateagate00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/templates/dynamic_preferences/testcontext.html0000644000175000017500000000000000000000000034412 0ustar00agateagate00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767504.0 django-dynamic-preferences-1.10.1/dynamic_preferences/types.py0000664000175000017500000003351400000000000024675 0ustar00agateagate00000000000000""" 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 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 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/urls.py0000644000175000017500000000142200000000000024505 0ustar00agateagate00000000000000from django.conf.urls import include, url 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 = [ url(r'^global/$', staff_member_required(views.PreferenceFormView.as_view( registry=global_preferences_registry, form_class=GlobalPreferenceForm)), name="global"), url(r'^global/(?P
[\w\ ]+)$', staff_member_required(views.PreferenceFormView.as_view( registry=global_preferences_registry, form_class=GlobalPreferenceForm)), name="global.section"), url(r'^user/', include('dynamic_preferences.users.urls')), ] ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1598001367.297035 django-dynamic-preferences-1.10.1/dynamic_preferences/users/0000775000175000017500000000000000000000000024312 5ustar00agateagate00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/users/__init__.py0000644000175000017500000000000000000000000026407 0ustar00agateagate00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/users/admin.py0000644000175000017500000000143400000000000025754 0ustar00agateagate00000000000000from 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/users/apps.py0000644000175000017500000000107600000000000025631 0ustar00agateagate00000000000000from 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' def ready(self): UserPreferenceModel = self.get_model('UserPreferenceModel') preference_models.register( UserPreferenceModel, user_preferences_registry) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/users/forms.py0000644000175000017500000000201200000000000026003 0ustar00agateagate00000000000000from six import string_types from 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 ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1598001367.297035 django-dynamic-preferences-1.10.1/dynamic_preferences/users/migrations/0000775000175000017500000000000000000000000026466 5ustar00agateagate00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/users/migrations/0001_initial.py0000644000175000017500000000241500000000000031131 0ustar00agateagate00000000000000# 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')}, ), ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1598001261.0 django-dynamic-preferences-1.10.1/dynamic_preferences/users/migrations/0002_auto_20200821_0837.py0000664000175000017500000000157300000000000032116 0ustar00agateagate00000000000000# 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'), ), ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/users/migrations/__init__.py0000644000175000017500000000000000000000000030563 0ustar00agateagate00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/users/models.py0000644000175000017500000000102200000000000026140 0ustar00agateagate00000000000000from 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") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/users/registries.py0000644000175000017500000000035400000000000027044 0ustar00agateagate00000000000000from ..registries import PerInstancePreferenceRegistry class UserPreferenceRegistry(PerInstancePreferenceRegistry): section_url_namespace = 'dynamic_preferences:user.section' user_preferences_registry = UserPreferenceRegistry() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/users/serializers.py0000644000175000017500000000020600000000000027214 0ustar00agateagate00000000000000from dynamic_preferences.api.serializers import PreferenceSerializer class UserPreferenceSerializer(PreferenceSerializer): pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/users/urls.py0000644000175000017500000000056200000000000025652 0ustar00agateagate00000000000000from django.conf.urls import include, url from django.contrib.auth.decorators import login_required from . import views urlpatterns = [ url(r'^$', login_required(views.UserPreferenceFormView.as_view()), name="user"), url(r'^(?P
[\w\ ]+)$', login_required(views.UserPreferenceFormView.as_view()), name="user.section"), ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/users/views.py0000644000175000017500000000101400000000000026013 0ustar00agateagate00000000000000from ..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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/users/viewsets.py0000644000175000017500000000067500000000000026543 0ustar00agateagate00000000000000from 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/utils.py0000644000175000017500000000061700000000000024665 0ustar00agateagate00000000000000import collections 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, collections.Mapping): r = update(d.get(k, {}), v) d[k] = r else: d[k] = u[k] return d././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/dynamic_preferences/views.py0000644000175000017500000000350400000000000024660 0ustar00agateagate00000000000000from 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) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1598001367.297035 django-dynamic-preferences-1.10.1/setup.cfg0000644000175000017500000000007500000000000020765 0ustar00agateagate00000000000000[wheel] universal = 1 [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1593767263.0 django-dynamic-preferences-1.10.1/setup.py0000755000175000017500000000300100000000000020651 0ustar00agateagate00000000000000#!/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="Eliot Berriot", author_email="contact@eliotberriot.com", url="https://github.com/EliotBerriot/django-dynamic-preferences", packages=["dynamic_preferences"], include_package_data=True, install_requires=[ 'django>=1.11', 'six', 'persisting_theory>=0.2.1', ], 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 :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", ], )