Days without fail: {{CONTENT_SETTINGS.DAYS_WITHOUT_FAIL}}
10 |
11 | Favorites:
12 |
13 |
14 | {% for item in CONTENT_SETTINGS.FAVORITE_SUBJECTS %}
15 |
{{item}}
16 | {% endfor %}
17 |
18 |
19 | Prices:
20 |
21 |
22 | {% for item in CONTENT_SETTINGS.PRICES__positive %}
23 |
{{item}}
24 | {% endfor %}
25 |
26 |
27 | {{CONTENT_SETTINGS.MY_YAML}}
28 |
29 |
Artists:
30 |
31 |
32 | {% for item in artists %}
33 |
{% content_settings_call "ARTIST_LINE" item %}
34 | {% endfor %}
35 |
36 |
--------------------------------------------------------------------------------
/cs_test/songs/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from django.views.generic import TemplateView
3 | from django.http import HttpResponse
4 |
5 | from content_settings.conf import content_settings
6 | from content_settings.views import FetchSettingsView, gen_hastag
7 | from content_settings.context_managers import content_settings_context
8 |
9 | from .models import Artist
10 |
11 |
12 | def math(request, multi=content_settings.lazy__DAYS_WITHOUT_FAIL):
13 | a = int(request.GET.get("a", 1))
14 | with content_settings_context(DAYS_WITHOUT_FAIL="17"):
15 | b = int(request.GET.get("b", 1)) * multi
16 | return HttpResponse(f"{a} + {b} = {a + b}")
17 |
18 |
19 | urlpatterns = [
20 | path(
21 | "",
22 | TemplateView.as_view(
23 | template_name="songs/index.html",
24 | extra_context={"artists": Artist.objects.all()},
25 | ),
26 | name="index",
27 | ),
28 | path("math/", math, name="math"),
29 | path("fetch/main/", FetchSettingsView.as_view(names=gen_hastag("main"))),
30 | ]
31 |
--------------------------------------------------------------------------------
/docs/api.md:
--------------------------------------------------------------------------------
1 | # API & Views
2 |
3 | ## Simple Example - FetchAllSettingsView
4 |
5 | Sometimes, you need to organize access to your content settings via API for your front-end applications. While you can access content settings directly in Python code, the module provides a fetching view that simplifies exposing content settings through APIs.
6 |
7 | Add the fetching view to your `urls.py`:
8 |
9 | ```python
10 | from django.urls import path
11 | from content_settings.views import FetchAllSettingsView
12 |
13 | urlpatterns = [
14 | path(
15 | "fetch/all/",
16 | FetchAllSettingsView.as_view(),
17 | name="fetch_all_info",
18 | ),
19 | ]
20 | ```
21 |
22 | The API call will return all registered content settings that the user has permission to fetch (based on the `fetch_permission` attribute, explained later in the article).
23 |
24 | ## Group of Settings to Fetch - FetchSettingsView
25 |
26 | If you want to limit the fetched settings to specific names, use the `FetchSettingsView`.
27 |
28 | Example:
29 |
30 | ```python
31 | from django.urls import path
32 | from content_settings.views import FetchSettingsView
33 |
34 | urlpatterns = [
35 | path(
36 | "fetch/main/",
37 | FetchSettingsView.as_view(
38 | names=[
39 | "TITLE",
40 | "DESCRIPTION",
41 | ]
42 | ),
43 | name="fetch_main_info",
44 | ),
45 | ]
46 | ```
47 |
48 | The `names` attribute specifies the settings included in the API. `FetchSettingsView` checks permissions for each setting using the `fetch_permission` attribute. By default, settings are not fetchable, so you need to update the settings' `fetch_permission` attribute. [Learn more about permissions here](permissions.md).
49 |
50 | ```python
51 | from content_settings import permissions # <-- Update
52 |
53 | TITLE = SimpleString(
54 | "My Site",
55 | fetch_permission=permissions.any, # <-- Update
56 | help="The title of the site",
57 | )
58 |
59 | DESCRIPTION = SimpleString(
60 | "Isn't it cool?",
61 | fetch_permission=permissions.any, # <-- Update
62 | help="The description of the site",
63 | )
64 | ```
65 |
66 | Now any user can access the `TITLE` and `DESCRIPTION` settings using the `fetch/main/` API.
67 |
68 | ```bash
69 | $ curl http://127.0.0.1/fetch/main/
70 | {"TITLE":"My Site","DESCRIPTION":"Isn't it cool?"}
71 | ```
72 |
73 | ## Other Options for Using the `names` Attribute
74 |
75 | ### Fetch All Settings Matching Specific Conditions
76 |
77 | Instead of specifying setting names directly, you can use a function to fetch all settings that meet certain criteria.
78 |
79 | #### Example: Matching All Settings with a Specific Tag
80 |
81 | ```python
82 | from content_settings.views import FetchSettingsView, gen_hastag
83 |
84 | FetchSettingsView.as_view(
85 | names=gen_hastag("general")
86 | )
87 | ```
88 |
89 | #### Example: Matching All Settings with a Specific Prefix
90 |
91 | ```python
92 | from content_settings.views import FetchSettingsView, gen_startswith
93 |
94 | FetchSettingsView.as_view(
95 | names=gen_startswith("GENERAL_")
96 | )
97 | ```
98 |
99 | #### Example: Combining Criteria
100 |
101 | Fetch all settings that start with `"GENERAL_"` and also include the setting `TITLE`.
102 |
103 | ```python
104 | from content_settings.views import FetchSettingsView, gen_startswith
105 |
106 | FetchSettingsView.as_view(
107 | names=[
108 | gen_startswith("GENERAL_"),
109 | "TITLE",
110 | ]
111 | )
112 | ```
113 |
114 | ### Using a Suffix
115 |
116 | ```python
117 | FetchSettingsView.as_view(
118 | names=[
119 | "TITLE",
120 | "BOOKS__available_names",
121 | ],
122 | )
123 | ```
124 |
125 | ### Define Specific Keys for the Result JSON
126 |
127 | You can customize the keys in the resulting JSON by using a tuple in the `names` list. The first value in the tuple will be the key.
128 |
129 | ```python
130 | FetchSettingsView.as_view(
131 | names=[
132 | "TITLE",
133 | ("NAMES", "BOOKS__available_names"),
134 | ],
135 | )
136 | ```
137 |
138 | In this example, the key `"NAMES"` will store the value of `content_settings.BOOKS__available_names`. This is useful if you change the setting name in Python but want to retain the old name in the API interface.
139 |
140 | ## FAQ
141 |
142 | ### What Happens if a User Lacks Permission to Fetch a Setting?
143 |
144 | The response will still have a status of `200`. The JSON response will include only the settings the user is allowed to access. An additional header, `X-Content-Settings-Errors`, will provide details about excluded settings.
145 |
146 | ### How Can I Hide Errors from the Response Headers?
147 |
148 | Set the `show_error_headers` attribute to `False`.
149 |
150 | Example:
151 |
152 | ```python
153 | FetchSettingsView.as_view(
154 | names=[
155 | "TITLE",
156 | "DESCRIPTION",
157 | ],
158 | show_error_headers=False,
159 | )
160 | ```
161 |
162 | ### How Can I Create a Custom View That Still Checks Permissions?
163 |
164 | To check permissions, use the `can_fetch` method of the setting type. You can retrieve the setting type by name in two ways:
165 |
166 | #### Using the `type__` Prefix
167 |
168 | ```python
169 | content_settings.type__TITLE.can_fetch(user)
170 | ```
171 |
172 | #### Using `get_type_by_name` from the Caching Module
173 |
174 | ```python
175 | from content_settings.caching import get_type_by_name
176 |
177 | get_type_by_name("TITLE").can_fetch(user)
178 | ```
179 |
180 | ### How Can I Customize the JSON Representation for Complex Settings?
181 |
182 | - Overwrite the `SimpleString.json_view_value(self, value: Any, **kwargs)` method. The method should return a string in JSON format.
183 | - Use the `json_encoder` parameter to specify a custom JSON serializer (default: `DjangoJSONEncoder`).
184 |
185 | [](https://stand-with-ukraine.pp.ua)
186 |
--------------------------------------------------------------------------------
/docs/caching.md:
--------------------------------------------------------------------------------
1 | # Caching
2 |
3 | There are two storage mechanisms for raw data: the database (DB) and thread-local storage. The cache is used to signal when the local cache needs to be updated.
4 |
5 | During the first run, a checksum is calculated for all registered variables. This checksum acts as a cache key that signals when a value has been updated and needs to be refreshed from the DB.
6 |
7 | Values from the DB are retrieved only when at least one setting in the thread is requested. In this case, data is fetched from the DB only when required.
8 |
9 | Once at least one setting is requested, all raw values are fetched and saved in the thread-local storage. This ensures that all settings remain consistent.
10 |
11 | Raw data for a setting is converted to a Python object only when the setting is requested. This avoids unnecessary processing of all raw data. Since raw data can be converted to a Python object at any time, this approach is efficient.
12 |
13 | For every request, the system checks the cache to verify if the checksum value has changed. If it has, the thread-local data is marked as unpopulated.
14 |
15 | When the system repopulates (i.e., a setting is requested again after being marked unpopulated), it updates all raw values. However, Python objects are invalidated only if the corresponding raw value has changed.
16 |
17 | The cache trigger class is responsible for updating Python objects when the corresponding database values are updated.
18 |
19 | The default trigger class is `content_settings.cache_triggers.VersionChecksum`, and its name is stored in [`CONTENT_SETTINGS_CACHE_TRIGGER`](settings.md#content_settings_cache_trigger).
20 |
21 | ---
22 |
23 | ## Raw to Python to Value
24 |
25 | The journey from a database raw value to the setting's final returned value (`settings` attribute) consists of two stages:
26 |
27 | 1. **Creating a Python Object from the Raw Value**:
28 | - Retrieving an updated value involves converting the raw value into a Python object.
29 |
30 | 2. **Using the `give` Function**:
31 | - The `give` function of the type class converts the Python object into the attribute's final value.
32 |
33 | ### Behavior for Different Types:
34 | - **Simple Types**:
35 | - The `give` function directly returns the Python object.
36 |
37 | - **Complex Types**:
38 | - The `give` function can:
39 | - Utilize the context in which the attribute is used.
40 | - Apply suffixes of the attribute to modify the returned value.
41 |
42 | ---
43 |
44 | ## When Is the Checksum Validated?
45 |
46 | The checksum is validated in the following scenarios:
47 |
48 | - **At the Beginning of a Request**:
49 | - Triggered by `signals.check_update_for_request`.
50 |
51 | - **Before a Celery Task (if Celery is available)**:
52 | - Triggered by `signals.check_update_for_celery`.
53 |
54 | - **Before a Huey Task (if Huey is available)**:
55 | - Triggered by `signals.check_update_for_huey`.
56 |
57 | ---
58 |
59 | ## Precached Python Values
60 |
61 | *Experimental Feature*
62 |
63 | The system allows all Python objects to be precached for each thread at the time the thread starts (e.g., when the DB connection is initialized).
64 |
65 | To activate this feature, set: `CONTENT_SETTINGS_PRECACHED_PY_VALUES = True`
66 |
67 | Note: This feature might cause issues if a new thread is started for every request.
68 |
69 | See also [`content_settings.caching`](source.md#caching).
70 |
71 | [](https://stand-with-ukraine.pp.ua)
--------------------------------------------------------------------------------
/docs/commands.md:
--------------------------------------------------------------------------------
1 | # Commands
2 |
3 | This is a list of available Django commands for content settings.
4 |
5 | ---
6 |
7 | ## `content_settings_migrate`
8 |
9 | You can migrate settings updates using this command. It is particularly useful if you have the Django setting `CONTENT_SETTINGS_UPDATE_DB_VALUES_BY_MIGRATE=False`. (See [settings](settings.md#content_settings_update_db_values_by_migrate)).
10 |
11 | ```bash
12 | $ python manage.py content_settings_migrate
13 | ```
14 |
15 | Without arguments, the command does not apply any changes but displays the changes that would be applied.
16 |
17 | ```bash
18 | $ python manage.py content_settings_migrate --apply
19 | ```
20 |
21 | Add `--apply` to apply the changes to the database.
22 |
23 | ---
24 |
25 | ## `content_settings_export`
26 |
27 | Export values from the database to STDOUT in JSON format.
28 |
29 | ```bash
30 | $ python manage.py content_settings_export
31 | ```
32 |
33 | Without arguments, this command exports all settings.
34 |
35 | ```bash
36 | $ python manage.py content_settings_export > backup.json
37 | ```
38 |
39 | Redirect the output to a file to create a complete backup of your settings.
40 |
41 | ```bash
42 | $ python manage.py content_settings_export --names TITLE DESCRIPTION
43 | ```
44 |
45 | Export only specific settings. In the example above, only the settings "TITLE" and "DESCRIPTION" are exported.
46 |
47 | *You can also perform exports through the Django Admin Panel. [Read more about it here](ui.md#export).*
48 |
49 | ---
50 |
51 | ## `content_settings_import`
52 |
53 | Import data into the database from a JSON file generated by `content_settings_export` or the [Django Admin UI](ui.md#export).
54 |
55 | ### Example Commands:
56 |
57 | ```bash
58 | $ python manage.py content_settings_import file.json
59 | ```
60 |
61 | This command does not immediately import data but displays what would be imported, along with any errors.
62 |
63 | ```bash
64 | $ python manage.py content_settings_import file.json --show-only-errors
65 | ```
66 |
67 | Use this option to display only errors, skipping valid or approved records.
68 |
69 | ```bash
70 | $ python manage.py content_settings_import file.json --show-skipped
71 | ```
72 |
73 | By default, only changes to the database are displayed. Use `--show-skipped` to display valid records that do not result in changes.
74 |
75 | ```bash
76 | $ python manage.py content_settings_import file.json --import
77 | ```
78 |
79 | Add `--import` to apply all approved records to the database.
80 |
81 | ```bash
82 | $ python manage.py content_settings_import file.json --preview-for admin
83 | ```
84 |
85 | Use `--preview-for {username}` to add all approved values to the user's preview group.
86 |
87 | ```bash
88 | $ python manage.py content_settings_import file.json --import --names TITLE DESCRIPTION
89 | ```
90 |
91 | Limit the imported values by specifying them with the `--names` argument.
92 |
93 | *You can also perform imports through the Django Admin Panel. [Read more about it here](ui.md#import).*
94 |
95 | ---
96 |
97 | [](https://stand-with-ukraine.pp.ua)
98 |
--------------------------------------------------------------------------------
/docs/contribute.md:
--------------------------------------------------------------------------------
1 | # How to Contribute to the Project
2 |
3 | ## What Can I Do?
4 |
5 | - Review the [List of Open Tickets](https://github.com/occipital/django-content-settings/issues) to find tasks or issues to work on.
6 | - Test the project and write tests to increase test coverage. Use the command `make test-cov` to check coverage.
7 |
8 | ---
9 |
10 | ## How to Set Up the Environment
11 |
12 | Follow these steps to set up your development environment:
13 |
14 | ```bash
15 | pre-commit install
16 | make init
17 | make test
18 | ```
19 |
20 | ---
21 |
22 | ## How to Set Up the `cs_test` Project
23 |
24 | The `cs_test` project/folder is used for testing and verifying the front-end portion of the project.
25 |
26 | 1. After setting up the environment, run:
27 | ```bash
28 | make cs-test-migrate
29 | ```
30 | This creates a database for the project.
31 |
32 | 2. To start a local runserver with content settings configured:
33 | ```bash
34 | make cs-test
35 | ```
36 |
37 | 3. To access the content settings shell:
38 | ```bash
39 | make cs-test-shell
40 | ```
41 |
42 | ---
43 |
44 | ### Docker Container for Testing Different Backends
45 |
46 | To test and adjust the MySQL backend, a Docker Compose file is included for the current `cs_test` project.
47 |
48 | - Build the Docker container:
49 | ```bash
50 | make cs-test-docker-build
51 | ```
52 |
53 | - Start the container:
54 | ```bash
55 | make cs-test-docker-up
56 | ```
57 |
58 | Feel free to modify the Docker setup to suit your testing needs.
59 |
60 | ---
61 |
62 | ## When Updating Documentation
63 |
64 | Creating high-quality documentation is challenging, and improvements are always welcome! If you’re contributing to documentation, please keep the following in mind:
65 |
66 | - Use terms consistently from the [Glossary](glossary.md), as the system introduces many new concepts.
67 | - If you update docstrings (documentation inside Python files), run:
68 | ```bash
69 | make mdsource
70 | ```
71 | This collects the updated docstrings into [source.md](source.md).
72 |
73 | ---
74 |
75 | ## Tests
76 |
77 | It's essential to create tests for new functionality and improve tests for existing functionality. Submitting a pull request with additional tests is highly encouraged.
78 |
79 | ### Testing Tools
80 |
81 | We use the following modules for testing:
82 |
83 | ```
84 | pytest = "^7.4.3"
85 | pytest-mock = "^3.12.0"
86 | pytest-django = "^4.7.0"
87 | django-webtest = "^1.9.11"
88 | pytest-cov = "^4.1.0"
89 | nox = "^2023.4.22"
90 | ```
91 |
92 | ### Testing Commands
93 |
94 | The following `make` commands can help streamline your testing process:
95 |
96 | - `make test`: Runs all tests in the current Poetry environment.
97 | - `make test-full`: Runs tests with extended settings.
98 | - `make test-min`: Runs tests with minimal settings to limit functionality.
99 | - `make test-cov`: Checks the current test coverage.
100 | - `make test-cov-xml`: Generates test coverage in `cov.xml`, which can help identify untested areas.
101 | - `make test-nox`: (Takes longer) Runs tests under all supported Python and Django versions.
102 | - `make test-nox-oldest`: Runs tests under the oldest supported combination of Python and Django versions.
103 |
104 | ---
105 |
106 | [](https://stand-with-ukraine.pp.ua)
107 |
--------------------------------------------------------------------------------
/docs/cookbook.md:
--------------------------------------------------------------------------------
1 | # Cookbook
2 |
3 | This section covers practical use cases for the `django-content-settings` module that might be useful in your projects.
4 |
5 | ---
6 |
7 | ### Grouping Multiple Settings by the Same Rule
8 |
9 | Suppose you have a group of settings with the same permission and want to append a note to the help text for those settings. While you can configure each setting individually, grouping them simplifies the process.
10 |
11 | ```python
12 | from content_settings.types.basic import SimpleString
13 | from content_settings.permissions import superuser
14 | from content_settings.defaults.context import defaults
15 | from content_settings.defaults.modifiers import help_suffix
16 |
17 | with defaults(help_suffix("Only superuser can change that"), update_permission=superuser):
18 | SITE_TITLE = SimpleString("Book Store", help="title for the site.")
19 | SITE_KEYWORDS = SimpleString("books, store, popular", help="head keywords.")
20 | ```
21 |
22 | The above code can be replaced with individual configurations as follows:
23 |
24 | ```python
25 | # same imports
26 |
27 | SITE_TITLE = SimpleString(
28 | "Book Store",
29 | update_permission=superuser,
30 | help="title for the site. Only superuser can change that",
31 | )
32 | SITE_KEYWORDS = SimpleString(
33 | "books, store, popular",
34 | update_permission=superuser,
35 | help="head keywords. Only superuser can change that",
36 | )
37 | ```
38 |
39 | ---
40 |
41 | ### Setting as a Class Attribute (Lazy Settings)
42 |
43 | Consider the following `content_settings.py`:
44 |
45 | ```python
46 | from content_settings.types.basic import SimpleInt
47 |
48 | POSTS_PER_PAGE = SimpleInt(10, help="How many blog posts will be shown per page")
49 | ```
50 |
51 | In a `views.py`:
52 |
53 | ```python
54 | from django.views.generic import ListView
55 | from blog.models import Post
56 | from content_settings.conf import content_settings
57 |
58 |
59 | class PostListView(ListView):
60 | model = Post
61 | paginate_by = content_settings.POSTS_PER_PAGE
62 | ```
63 |
64 | The above will work until you update `POSTS_PER_PAGE` in the Django admin, at which point the change won’t reflect. Instead, use a lazy value:
65 |
66 | ```python
67 | # same imports
68 |
69 | class PostListView(ListView):
70 | model = Post
71 | paginate_by = content_settings.lazy__POSTS_PER_PAGE # <-- update
72 | ```
73 |
74 | ---
75 |
76 | ### How to Test Setting Changes
77 |
78 | Use `content_settings_context` from `content_settings.context_managers` to test setting changes.
79 |
80 | #### As a Decorator:
81 |
82 | ```python
83 | @content_settings_context(TITLE="New Book Store")
84 | def test_get_simple_text_updated():
85 | assert content_settings.TITLE == "New Book Store"
86 | ```
87 |
88 | #### As a Context Manager:
89 |
90 | ```python
91 | def test_get_simple_text_updated_twice():
92 | client = get_anonymous_client()
93 | with content_settings_context(TITLE="New Book Store"):
94 | assert content_settings.TITLE == "New Book Store"
95 |
96 | with content_settings_context(TITLE="SUPER New Book Store"):
97 | assert content_settings.TITLE == "SUPER New Book Store"
98 | ```
99 |
100 | ---
101 |
102 | ### Handling Endless Running Commands
103 |
104 | If you have an endless running command and want to keep settings updated, manually check updates inside the loop. Use `check_update` from `content_settings.caching`.
105 |
106 | ```python
107 | from django.core.management.base import BaseCommand
108 | from content_settings.caching import check_update
109 |
110 | class Command(BaseCommand):
111 | def handle(self, *args, **options):
112 | while True:
113 | check_update()
114 |
115 | # your logic
116 | ```
117 |
118 | ---
119 |
120 | ### Triggering a Procedure When a Variable Changes
121 |
122 | To trigger an action, such as data synchronization, when a setting changes, add a `post_save` signal handler for `models.ContentSetting`.
123 |
124 | #### Case #1: Manually Convert Raw Data
125 |
126 | ```python
127 | from django.db.models.signals import post_save
128 | from django.dispatch import receiver
129 | from content_settings.models import ContentSetting
130 |
131 | @receiver(post_save, sender=ContentSetting)
132 | def process_variable_update(instance, created, **kwargs):
133 | if instance.name != 'VARIABLE':
134 | return
135 | val = content_settings.type__VARIABLE.give_python(instance.value)
136 |
137 | # process value
138 | ```
139 |
140 | #### Case #2: Use `content_settings_context`
141 |
142 | ```python
143 | # same imports
144 | from content_settings.context_managers import content_settings_context
145 |
146 | @receiver(post_save, sender=ContentSetting)
147 | def process_variable_update(instance, created, **kwargs):
148 | if instance.name != 'VARIABLE':
149 | return
150 |
151 | with content_settings_context(VARIABLE=instance.value):
152 | val = content_settings.VARIABLE
153 |
154 | # process value
155 | ```
156 |
157 | ---
158 |
159 | ### Upgrading a Variable from SimpleText to Template
160 |
161 | If you previously used a `SimpleText` variable and later need a template, you don’t have to update all references from `VARNAME` to `VARNAME()`.
162 |
163 | Use `GiveCallMixin` or `NoArgs` types such as `DjangoTemplateNoArgs` or `SimpleEvalNoArgs`. For the opposite scenario, use `MakeCallMixin`.
164 |
165 | ---
166 |
167 | ### Using `DjangoModelTemplate` Without Directly Importing a Model
168 |
169 | If you cannot import a model to assign a query to `template_model_queryset`, use `DjangoTemplate` with `gen_args_call_validator`.
170 |
171 | ```python
172 | def getting_first_profile():
173 | from accounts.models import Profile
174 |
175 | return Profile.objects.first()
176 |
177 | NEW_SETTING = DjangoTemplate(
178 | "{{object.name}}",
179 | validators=[gen_args_call_validator(getting_first_profile)],
180 | template_args_default={'object': require}
181 | )
182 | ```
183 |
184 | [](https://stand-with-ukraine.pp.ua)
185 |
--------------------------------------------------------------------------------
/docs/epilogue.md:
--------------------------------------------------------------------------------
1 | # Epilogue
2 |
3 | I once heard a song in my dreams. When I woke up, I tried to write it down, but it was never as perfect as it was in my dream. This version, however, is the closest I’ve come so far.
4 |
5 | Thank you for taking the time to explore this project.
6 |
7 | [](https://stand-with-ukraine.pp.ua)
8 |
--------------------------------------------------------------------------------
/docs/extends.md:
--------------------------------------------------------------------------------
1 | # Possible Extensions
2 |
3 | The aim of this article is to showcase the various ways you can extend the basic functionality of `django-content-settings`.
4 |
5 | ---
6 |
7 | ## Create Your Own Classes
8 |
9 | The most basic and common way to extend functionality is by creating your own classes based on the ones in `content_settings.types`.
10 |
11 | - Check how other types are created.
12 | - Review the extension points for [`content_settings.types.basic.SimpleString`](source.md#class-simplestringbasesettingsource).
13 |
14 | ---
15 |
16 | ## Generating Tags
17 |
18 | Using the Django setting [`CONTENT_SETTINGS_TAGS`](settings.md#content_settings_tags), you can leverage built-in tags, such as `content_settings.tags.changed`.
19 |
20 | Additionally, you can create custom functions to generate tags for your settings based on their content. For inspiration, review the [source code](source.md#tags) for existing tags.
21 |
22 | ---
23 |
24 | ## Redefine Default Attributes for All Settings
25 |
26 | Using the Django setting [`CONTENT_SETTINGS_DEFAULTS`](settings.md#content_settings_defaults), you can customize how default attributes are applied to all (or specific) settings.
27 |
28 | - Refer to the [collections module](source.md#defaultscollections), which includes defaults for CodeMirror support.
29 | - Similarly, configure defaults to support other code editors or UI components.
30 |
31 | ---
32 |
33 | ## Custom Access Rules
34 |
35 | Access rules for settings can be defined by assigning specific functions to attributes. For more information, see [permissions](permissions.md).
36 |
37 | To go beyond predefined functions, you can create your own custom access rule functions to implement unique logic.
38 |
39 | ---
40 |
41 | ## Custom Prefix for Settings
42 |
43 | Several built-in prefixes, such as `withtag__` and `lazy__`, are available ([see full list here](access.md#prefix)). However, you can register your own prefixes using the `store.register_prefix` decorator.
44 |
45 | #### Example:
46 |
47 | ```python
48 | from content_settings.store import register_prefix
49 | from content_settings.caching import get_value
50 |
51 | @register_prefix("endswith")
52 | def endswith_prefix(name: str, suffix: str):
53 | return {
54 | k: get_value(k, suffix) for k in dir(content_settings) if k.endswith(name)
55 | }
56 | ```
57 |
58 | #### Usage:
59 |
60 | ```python
61 | for name, value in content_settings.endswith__BETA:
62 | ...
63 | ```
64 |
65 | ---
66 |
67 | ## Integration with Task Management Systems
68 |
69 | There are built-in integrations for task managers like Celery and Huey. These integrations are simple, and you can create your own.
70 |
71 | #### Example: Celery Integration
72 |
73 | This example ensures that all settings are updated before a task begins. It can be found in [signals.py](https://github.com/occipital/django-content-settings/blob/master/content_settings/signals.py):
74 |
75 | ```python
76 | try:
77 | from celery.signals import task_prerun
78 | except ImportError:
79 | pass
80 | else:
81 |
82 | @task_prerun.connect
83 | def check_update_for_celery(*args, **kwargs):
84 | check_update()
85 | ```
86 |
87 | The idea is to verify that all settings are up-to-date before starting a task.
88 |
89 | ---
90 |
91 | ## Middleware for Preview
92 |
93 | To enable the preview functionality for settings, add the middleware `content_settings.middleware.preview_on_site` to your project’s settings.
94 |
95 | The middleware:
96 | - Checks if the user has preview objects.
97 | - Processes the response under the updated settings context.
98 |
99 | You can review the middleware’s [source code here](https://github.com/occipital/django-content-settings/blob/master/content_settings/middlewares.py).
100 |
101 | ### Custom Middleware
102 |
103 | You may create custom middleware for specialized use cases, such as integrating with [django-impersonate](https://pypi.org/project/django-impersonate/).
104 |
105 | ---
106 |
107 | ## Cache Triggers
108 |
109 | Cache triggers are part of the [caching functionality](caching.md). They allow you to configure when to update py objects related to settings.
110 |
111 | - See the source code for [`content_settings.cache_triggers`](source.md#cache_triggers) for more information.
112 |
113 | ---
114 |
115 | [](https://stand-with-ukraine.pp.ua)
116 |
--------------------------------------------------------------------------------
/docs/faq.md:
--------------------------------------------------------------------------------
1 | # Frequently Asked Questions
2 |
3 | Many common use cases are covered in the [Cookbook](cookbook.md) and [Possible Extensions](extends.md), but here I want to address some frequently asked questions from other users.
4 |
5 | ---
6 |
7 | ### Can I create a variable in the Django admin before it’s defined in `content_settings.py`?
8 |
9 | Yes, you can. Check out the [User Defined Variables](uservar.md) section for more information.
10 |
11 | ---
12 |
13 | ### Why are there two functions, `give` and `to_python`, when `give` often just returns its input?
14 |
15 | The `give` function is designed to adapt data specifically for use in the project’s code, whereas `to_python` converts a string value into a Python object.
16 |
17 | The key difference:
18 | - **`to_python`**: Converts a string value to a Python object when the string value changes or at project startup.
19 | - **`give`**: Adapts the Python object for use in the project, and this happens whenever the data is requested (e.g., from an attribute or another source).
20 |
21 | ---
22 |
23 | ### Why is the version still 0?
24 |
25 | The module is still in active development. The design, including the naming conventions for types, may change. Version 1.0 will include a more stable and finalized design.
26 |
27 | ---
28 |
29 | ### Can I see the changes on the site before applying them to all users?
30 |
31 | Yes. The preview functionality allows you to see changes before applying them globally. Learn more about it in the [UI article](ui.md#preview-functionality).
32 |
33 | ---
34 |
35 | ### I need to change multiple variables at once for a desired effect. How can I avoid users seeing an incomplete configuration?
36 |
37 | This can be handled in several ways:
38 |
39 | 1. **Edit Multiple Values in the Change List**:
40 | - Use the "mark" functionality to edit multiple settings on the same page and submit them together. Read more in the [UI article](ui.md#apply-multiple-settings-at-once).
41 |
42 | 2. **Use Preview Settings**:
43 | - Add multiple changes to the preview and apply them all in one click. Read more in the [UI article](ui.md#preview-functionality).
44 |
45 | ---
46 |
47 | ### The guide didn’t help. What should I do?
48 |
49 | This happens, as the documentation is still a work in progress and not all scenarios are covered yet.
50 |
51 | Here’s how you can get additional support:
52 |
53 | - **Have a specific question?** Use [Discussions on GitHub](https://github.com/occipital/django-content-settings/discussions).
54 | - **Found a bug or unexpected behavior?** Report it on [Issues in GitHub](https://github.com/occipital/django-content-settings/issues).
55 | - **Want to improve the documentation?** Contributions are welcome! You can find the Markdown sources in the [docs folder](https://github.com/occipital/django-content-settings/tree/master/docs).
56 |
57 | ---
58 |
59 | [](https://stand-with-ukraine.pp.ua)
60 |
--------------------------------------------------------------------------------
/docs/glossary.md:
--------------------------------------------------------------------------------
1 | # Glossary
2 |
3 | Some of the terms used in other articles are explained here. The terms from the glossary are shown in *italic* in the other articles.
4 |
5 | In order to better understand these terms, I'll use the following example.
6 |
7 | ```python
8 | FAVORITE_SUBJECTS = SimpleStringsList(
9 | "mysubject",
10 | comment_starts_with="//",
11 | help="my favorite songs subjects"
12 | )
13 | ```
14 |
15 | ### admin preview
16 |
17 | *or setting admin preview*
18 |
19 | When the admin changes the *raw value* in Django Admin, the admin panel shows a preview that somehow illustrates the *value*. Sometimes, it is tricky when you need to show a value of *callable type*.
20 |
21 | ### callable type
22 |
23 | When the *setting value* is callable, so in code you need to call the value, for example `content_settings.FAVORITE_SUBJECTS()`. Most of the *callable types* can be found in `content_settings.types.template`.
24 |
25 | ### content tags
26 |
27 | Tags for the setting generated by the value of the setting.
28 |
29 | ### db value
30 |
31 | *or setting db value*
32 |
33 | *Raw value* that is stored in the database.
34 |
35 | ### default value
36 |
37 | *or setting default value*
38 |
39 | `"mysubject"` - *raw value* that will be used for database initialization or for cases when *db value* is not set.
40 |
41 | ### definition
42 |
43 | *or setting definition*
44 |
45 | The example above shows the full setting definition. Includes *name* and *type definition*.
46 |
47 | ### django settings
48 |
49 | As we use settings to refer to content settings, we will use django settings for actual Django settings constants.
50 |
51 | ### instance
52 |
53 | *or setting instance*
54 |
55 | The result of calling the *type definition*. The instance is responsible for converting, parsing, and validating processes.
56 |
57 | ### JSON value
58 |
59 | *or setting JSON value*
60 |
61 | JSON representation of the *value* for API. Generated by `json_view_value` of the *instance*.
62 |
63 | ### lazy value
64 |
65 | *or setting lazy value*
66 |
67 | *Use setting* with *prefix* that returns *value* as a lazy object. Can be useful when you need to save a reference to the *value* in a global environment before the *python object* generation. Example: `content_settings.lazy__FAVORITE_SUBJECTS`. Generated by `lazy_give` of the *instance*, see also `content_settings.types.lazy`.
68 |
69 | ### mixin
70 |
71 | *or setting mixin*
72 |
73 | In the *setting definition*, you can extend *setting type* with a list of mixins using the `mixin` function (the function and most of the available mixins can be found in `content_settings.types.mixins`). Example: `DAYS_WITHOUT_FAIL = mix(MinMaxValidationMixin, SimpleInt)("5", min_value=0, max_value=10, help="How many days without fail")` - *type* `SimpleInt` was extended with the mixin `MinMaxValidationMixin`, which adds new optional attributes `min_value` and `max_value` and validates if the *python object* is within a given range.
74 |
75 | ### name
76 |
77 | *or setting name*
78 |
79 | `FAVORITE_SUBJECTS` - the unique name of your setting. Should always be uppercased. By the same name, you can *use setting* and change it in Django Admin.
80 |
81 | ### prefix
82 |
83 | *or setting prefix*
84 |
85 | A content setting method that can return something other than *setting value*. For example, `content_settings.lazy__FAVORITE_SUBJECTS` - `lazy` is a prefix and the whole *use* returns the *lazy value* of the setting `FAVORITE_SUBJECTS`. The `register_prefix` allows new prefix registration.
86 |
87 | ### python object
88 |
89 | *or setting python object or py object*
90 |
91 | The object generated by converting the raw value when starting the server (or when the raw value is changed). Generated by the method `to_python` of the *setting instance*.
92 |
93 | ### raw value
94 |
95 | *or setting raw value*
96 |
97 | The initial value, always a string. This value is parsed/converted using the *setting instance*.
98 |
99 | ### suffix
100 |
101 | *or setting suffix*
102 |
103 | An extra attribute that extends the `give` method that returns *value*. For example, `content_settings.FAVORITE_SUBJECTS__first` - `first` is a *suffix*. Suffixes are convenient for cases when you need to extract some other data from the *python object*, not only the *value*, or you need to return *value* in a different way for special cases.
104 |
105 | ### type
106 |
107 | *or setting type*
108 |
109 | `SimpleStringsList` - All of the built-in classes can be found in `content_settings.types`.
110 |
111 | ### type arguments
112 |
113 | *or setting type arguments*
114 |
115 | `comment_starts_with="//", help="my favorite songs subjects"`.
116 |
117 | ### type definition
118 |
119 | *or setting type definition*
120 |
121 | `SimpleStringsList("mysubject", comment_starts_with="//", help="my favorite songs subjects")`.
122 |
123 | ### use setting
124 |
125 | *or setting use*
126 |
127 | When you use a setting in the Python code `content_settings.FAVORITE_SUBJECTS` or in a template `{{CONTENT_SETTINGS.FAVORITE_SUBJECTS}}`. All of these code examples return the *setting value*.
128 |
129 | ### user defined types
130 |
131 | Types that are allowed to be used for user-defined settings. [More about it here](uservar.md).
132 |
133 | ### user defined settings
134 |
135 | Settings that are created in Django Admin by the user (not by code). [More about it here](uservar.md).
136 |
137 | ### value
138 |
139 | *or setting value*
140 |
141 | The value that will be returned when you *use setting*. Generated by the `give` method for the *setting instance* for each *use*. For the most basic types, *value* is the same as the *python object*.
142 |
143 | [](https://stand-with-ukraine.pp.ua)
--------------------------------------------------------------------------------
/docs/img/dict_suffixes_preview.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/occipital/django-content-settings/26060296a8f93700a8861b365be96170ad2ce2c5/docs/img/dict_suffixes_preview.gif
--------------------------------------------------------------------------------
/docs/img/preview.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/occipital/django-content-settings/26060296a8f93700a8861b365be96170ad2ce2c5/docs/img/preview.gif
--------------------------------------------------------------------------------
/docs/img/preview_on_site.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/occipital/django-content-settings/26060296a8f93700a8861b365be96170ad2ce2c5/docs/img/preview_on_site.png
--------------------------------------------------------------------------------
/docs/img/split_translation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/occipital/django-content-settings/26060296a8f93700a8861b365be96170ad2ce2c5/docs/img/split_translation.png
--------------------------------------------------------------------------------
/docs/img/title.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/occipital/django-content-settings/26060296a8f93700a8861b365be96170ad2ce2c5/docs/img/title.png
--------------------------------------------------------------------------------
/docs/img/ui/batch_changes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/occipital/django-content-settings/26060296a8f93700a8861b365be96170ad2ce2c5/docs/img/ui/batch_changes.png
--------------------------------------------------------------------------------
/docs/img/ui/django_history.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/occipital/django-content-settings/26060296a8f93700a8861b365be96170ad2ce2c5/docs/img/ui/django_history.png
--------------------------------------------------------------------------------
/docs/img/ui/edit_page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/occipital/django-content-settings/26060296a8f93700a8861b365be96170ad2ce2c5/docs/img/ui/edit_page.png
--------------------------------------------------------------------------------
/docs/img/ui/history_button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/occipital/django-content-settings/26060296a8f93700a8861b365be96170ad2ce2c5/docs/img/ui/history_button.png
--------------------------------------------------------------------------------
/docs/img/ui/history_export.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/occipital/django-content-settings/26060296a8f93700a8861b365be96170ad2ce2c5/docs/img/ui/history_export.png
--------------------------------------------------------------------------------
/docs/img/ui/import_json.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/occipital/django-content-settings/26060296a8f93700a8861b365be96170ad2ce2c5/docs/img/ui/import_json.png
--------------------------------------------------------------------------------
/docs/img/ui/import_json_error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/occipital/django-content-settings/26060296a8f93700a8861b365be96170ad2ce2c5/docs/img/ui/import_json_error.png
--------------------------------------------------------------------------------
/docs/img/ui/import_json_preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/occipital/django-content-settings/26060296a8f93700a8861b365be96170ad2ce2c5/docs/img/ui/import_json_preview.png
--------------------------------------------------------------------------------
/docs/img/ui/list_view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/occipital/django-content-settings/26060296a8f93700a8861b365be96170ad2ce2c5/docs/img/ui/list_view.png
--------------------------------------------------------------------------------
/docs/img/ui/list_view_actions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/occipital/django-content-settings/26060296a8f93700a8861b365be96170ad2ce2c5/docs/img/ui/list_view_actions.png
--------------------------------------------------------------------------------
/docs/img/ui/list_view_actions_export.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/occipital/django-content-settings/26060296a8f93700a8861b365be96170ad2ce2c5/docs/img/ui/list_view_actions_export.png
--------------------------------------------------------------------------------
/docs/img/ui/list_view_bottom.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/occipital/django-content-settings/26060296a8f93700a8861b365be96170ad2ce2c5/docs/img/ui/list_view_bottom.png
--------------------------------------------------------------------------------
/docs/img/ui/list_view_preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/occipital/django-content-settings/26060296a8f93700a8861b365be96170ad2ce2c5/docs/img/ui/list_view_preview.png
--------------------------------------------------------------------------------
/docs/img/ui/list_view_preview_panel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/occipital/django-content-settings/26060296a8f93700a8861b365be96170ad2ce2c5/docs/img/ui/list_view_preview_panel.png
--------------------------------------------------------------------------------
/docs/img/ui/list_view_tag_filter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/occipital/django-content-settings/26060296a8f93700a8861b365be96170ad2ce2c5/docs/img/ui/list_view_tag_filter.png
--------------------------------------------------------------------------------
/docs/img/ui/main_admin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/occipital/django-content-settings/26060296a8f93700a8861b365be96170ad2ce2c5/docs/img/ui/main_admin.png
--------------------------------------------------------------------------------
/docs/permissions.md:
--------------------------------------------------------------------------------
1 | # Permissions
2 |
3 | ## Overview
4 |
5 | The `content_settings.permissions` *([source](source.md#permissions))* module in Django provides functions that can be used as arguments for the permission attributes of your settings, such as:
6 |
7 | - `fetch_permission`: Controls API access to variables through `views.FetchSettingsView`.
8 | - `update_permission`: Restricts the ability to change a variable in the admin panel.
9 | - `view_permission`: Determines who can see the variable in the admin panel (it will not be listed for unauthorized users).
10 |
11 | ---
12 |
13 | ## Functions in the Module
14 |
15 | ### `any`
16 |
17 | Allows access for all users.
18 |
19 | ### `none`
20 |
21 | Denies access to all users.
22 |
23 | ### `authenticated`
24 |
25 | Grants access only to authenticated users.
26 |
27 | ### `staff`
28 |
29 | Restricts access to staff users.
30 |
31 | ### `superuser`
32 |
33 | Restricts access to superusers.
34 |
35 | ### `has_perm(perm)`
36 |
37 | Allows access to users with a specific permission.
38 |
39 | **Example**:
40 |
41 | ```python
42 | has_perm('app_label.permission_codename')
43 | ```
44 |
45 | ---
46 |
47 | ## Functions from `functools` Module
48 |
49 | ### `and_(*funcs)`
50 |
51 | Combines multiple permission functions using a logical AND.
52 |
53 | **Example**:
54 |
55 | ```python
56 | and_(authenticated, has_perm('app_label.permission_codename'))
57 | ```
58 |
59 | ### `or_(*funcs)`
60 |
61 | Combines multiple permission functions using a logical OR.
62 |
63 | **Example**:
64 |
65 | ```python
66 | or_(staff, has_perm('app_label.permission_codename'))
67 | ```
68 |
69 | ### `not_(*funcs)`
70 |
71 | Applies a logical NOT to the given permission functions.
72 |
73 | ---
74 |
75 | ## Usage Examples
76 |
77 | ### Example 1: Setting Multiple Permissions
78 |
79 | Restrict a variable so that only staff members or users with a specific permission can update it:
80 |
81 | ```python
82 | from content_settings.types.basic import SimpleString, SimpleDecimal
83 | from content_settings.permissions import staff, has_perm
84 | from content_settings.functools import or_
85 |
86 | TITLE = SimpleString(
87 | "default value",
88 | update_permission=or_(staff, has_perm("app_label.permission_codename"))
89 | )
90 |
91 | MAX_PRICE = SimpleDecimal(
92 | "9.99",
93 | fetch_permission=staff,
94 | )
95 | ```
96 |
97 | In this example:
98 | - `TITLE` can be updated by either staff members or users with the specified permission.
99 | - `MAX_PRICE` can only be fetched by staff members.
100 |
101 | ---
102 |
103 | ### Example 2: Using Permission Names Instead of Functions
104 |
105 | You can use permission names directly if they are defined in the `content_settings.permissions` module:
106 |
107 | ```python
108 | from content_settings.types.basic import SimpleString, SimpleDecimal
109 | from content_settings.permissions import has_perm
110 | from content_settings.functools import or_
111 |
112 | TITLE = SimpleString(
113 | "default value",
114 | update_permission=or_("staff", has_perm("app_label.permission_codename"))
115 | )
116 |
117 | MAX_PRICE = SimpleDecimal(
118 | "9.99",
119 | fetch_permission="staff",
120 | )
121 | ```
122 |
123 | Alternatively, use the full import path for custom permissions:
124 |
125 | ```python
126 | from content_settings.types.basic import SimpleDecimal
127 |
128 | MAX_PRICE = SimpleDecimal(
129 | "9.99",
130 | fetch_permission="my_project.permissions.main_users",
131 | )
132 | ```
133 |
134 | ---
135 |
136 | [](https://stand-with-ukraine.pp.ua)
137 |
--------------------------------------------------------------------------------
/docs/requirements.txt:
--------------------------------------------------------------------------------
1 | mkdocs==1.5.3
--------------------------------------------------------------------------------
/docs/uservar.md:
--------------------------------------------------------------------------------
1 | # User Defined Variables
2 |
3 | ## Introduction
4 |
5 | The main concept of content settings is to allow you to define a constant in code that can be edited in the Django Admin panel. However, there might be a case when you need to create new content settings not in code but in the admin panel, and this is where user-defined types are used.
6 |
7 | ## How is it useful?
8 |
9 | 1. **Use in Template Variables**: If you have several template settings containing the same text, do not copy and paste the same text in each value - you can simply create your own content settings variable and use it for every template value.
10 |
11 | 2. **Flexibility for Developers**: You can have a view that fetches variables by tag (see [API](api.md#all-settings-that-matches-specific-conditions)), and by creating a new variable, you can add new data to the API response. Later, you can define the setting in code, which replaces the user-defined setting with a simple setting. On top of that, we have the prefix "withtag__", which allows you to get all of the settings with a specific tag.
12 |
13 | ## Setting Up User Defined Types
14 |
15 | To enable the creation of such variables, you need to set up a specific setting that lists all the types available for creation:
16 |
17 | ```python
18 | CONTENT_SETTINGS_USER_DEFINED_TYPES=[
19 | ("text", "content_settings.types.basic.SimpleText", "Text"),
20 | ("html", "content_settings.types.basic.SimpleHTML", "HTML"),
21 | ]
22 | ```
23 |
24 | Read more about this setting [here](settings.md#content_settings_user_defined_types).
25 |
26 | Having the Django setting set admin should see the "Add Content Settings" button at the top of the list of all available content settings (see [UI](ui.md#list-of-available-settings))
27 |
28 | ## Overwriting User-Defined Variables
29 |
30 | It's important to note that if you decide to create a variable in the code that should overwrite a variable previously made in the admin panel, you will encounter a migration error. To avoid this, explicitly state that the code variable will overwrite the admin-created variable by setting `overwrite_user_defined=True`.
31 |
32 | ## Conclusion
33 |
34 | User User-defined types in `django-content-settings` offer significant flexibility and customization for managing variables in Django applications. This feature empowers administrators to create and modify variables directly from the admin panel while still providing the option for developers to override these variables in the code if needed.
35 |
36 | [](https://stand-with-ukraine.pp.ua)
--------------------------------------------------------------------------------
/mdsource.py:
--------------------------------------------------------------------------------
1 | import ast
2 | import os
3 | from pathlib import Path
4 |
5 |
6 | GITHUB_PREFIX = "https://github.com/occipital/django-content-settings/blob/master/"
7 | SOURCE_FOLDER = "content_settings"
8 | IGNORE_MODULES = ["receivers.py", "apps.py", "admin.py"]
9 |
10 |
11 | def split_path(path):
12 | return list(Path(path).parts)
13 |
14 |
15 | def path_to_linux(path):
16 | return "/".join(split_path(path))
17 |
18 |
19 | def get_base_classes(bases):
20 | """Extract the names of base classes from the bases list in a class definition."""
21 | base_class_names = []
22 | for base in bases:
23 | if isinstance(base, ast.Name):
24 | base_class_names.append(base.id)
25 | elif isinstance(base, ast.Attribute):
26 | base_class_names.append(ast.unparse(base))
27 | else:
28 | base_class_names.append(ast.unparse(base))
29 | return ", ".join(base_class_names)
30 |
31 |
32 | def get_function_signature(func):
33 | """Generate the signature for a function or method."""
34 | args = []
35 | # Extract arguments and their default values
36 | defaults = [None] * (
37 | len(func.args.args) - len(func.args.defaults)
38 | ) + func.args.defaults
39 | for arg, default in zip(func.args.args, defaults):
40 | if isinstance(arg.annotation, ast.expr):
41 | # Get the annotation if present
42 | annotation = ast.unparse(arg.annotation)
43 | arg_desc = f"{arg.arg}: {annotation}"
44 | else:
45 | arg_desc = arg.arg
46 |
47 | if default is not None:
48 | default_value = ast.unparse(default)
49 | arg_desc += f" = {default_value}"
50 | args.append(arg_desc)
51 | return f"({', '.join(args)})"
52 |
53 |
54 | def md_from_node(node, prefix, file_path):
55 | for n in node.body:
56 | if isinstance(n, ast.ClassDef):
57 | if class_doc := ast.get_docstring(n):
58 | yield f"\n\n{prefix} class {n.name}({get_base_classes(n.bases)})"
59 | yield f"[source]({GITHUB_PREFIX}{path_to_linux(file_path)}#L{n.lineno})\n\n"
60 | yield class_doc
61 |
62 | yield from md_from_node(n, prefix=prefix + "#", file_path=file_path)
63 |
64 | elif isinstance(n, ast.FunctionDef):
65 | if func_doc := ast.get_docstring(n):
66 | yield f"\n\n{prefix} def {n.name}"
67 | yield get_function_signature(n)
68 | yield f"[source]({GITHUB_PREFIX}{path_to_linux(file_path)}#L{n.lineno})\n\n"
69 | yield func_doc
70 |
71 |
72 | def md_from_file(file_path):
73 | with open(file_path, "r") as file:
74 | node = ast.parse(file.read(), filename=file_path)
75 |
76 | if module_doc := ast.get_docstring(node):
77 | yield module_doc
78 |
79 | yield from md_from_node(node, prefix="###", file_path=file_path)
80 |
81 |
82 | module_list = []
83 | main_lines = []
84 |
85 | for dirname, dirs, files in os.walk(SOURCE_FOLDER):
86 | if dirname.endswith("__pycache__"):
87 | continue
88 |
89 | for name in sorted(files):
90 | if not name.endswith(".py"):
91 | continue
92 | if name in IGNORE_MODULES:
93 | continue
94 |
95 | mddoc = "".join(md_from_file(os.path.join(dirname, name)))
96 | if not mddoc:
97 | continue
98 |
99 | # Save generated doc to the docfile
100 |
101 | dir = dirname[len(SOURCE_FOLDER) + 1 :]
102 | module_name = ".".join(Path(dir).parts + (os.path.splitext(name)[0],))
103 |
104 | main_lines.append(f"\n\n## {module_name}")
105 | module_list.append(f"- [{module_name}](#{module_name.replace('.', '')})")
106 |
107 | main_lines.append("\n\n")
108 | main_lines.append(mddoc)
109 |
110 | with open(os.path.join("docs", "source.md"), "w") as fh:
111 | fh.write("# Module List\n\n")
112 | fh.write("\n".join(module_list))
113 | fh.write("\n\n")
114 |
115 | fh.write("\n".join(main_lines))
116 | fh.write(
117 | """
118 |
119 | [](https://stand-with-ukraine.pp.ua)
120 |
121 | """
122 | )
123 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: Django Content Settings
2 | nav:
3 | - Home: index.md
4 | - Getting Started: first.md
5 | - Setting Types and Attributes: types.md
6 | - Template Types: template_types.md
7 | - Using Settings: access.md
8 | - Permissions: permissions.md
9 | - Defaults Context: defaults.md
10 | - API & Views: api.md
11 | - Available Django Settings: settings.md
12 | - User Interface for Django Admin: ui.md
13 | - Commands: commands.md
14 | - How Caching is Organized: caching.md
15 | - User Defined Variables: uservar.md
16 | - Possible Extensions: extends.md
17 | - Cookbook: cookbook.md
18 | - Frequently Asked Questions: faq.md
19 | - Changelog: changelog.md
20 | - Glossary: glossary.md
21 | - How to contribute: contribute.md
22 | - Source Doc: source.md
23 | - Epilogue: epilogue.md
24 | theme: readthedocs
25 | repo_url: https://github.com/occipital/django-content-settings/
--------------------------------------------------------------------------------
/noxfile.py:
--------------------------------------------------------------------------------
1 | import nox
2 |
3 |
4 | @nox.session(python=["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"])
5 | @nox.parametrize("django", ["3.2", "4.2", "5.0", "5.1"])
6 | @nox.parametrize("pyyaml", [True, False])
7 | def tests(session, django, pyyaml):
8 | if django in ["5.0", "5.1"] and session.python in (
9 | "3.8",
10 | "3.9",
11 | ):
12 | return
13 | session.install(f"django=={django}")
14 | if pyyaml:
15 | session.install("PyYAML")
16 | if session.python in ["3.12", "3.13"]:
17 | session.install("setuptools")
18 | session.install("pytest~=7.4.3")
19 | session.install("pytest-mock~=3.12.0")
20 | session.install("pytest-django~=4.7.0")
21 | session.install("django-webtest~=1.9.11")
22 | session.install("-e", ".")
23 | for testing_settings in ["min", "full", "normal"]:
24 | for precache in (True, False):
25 | session.run(
26 | "pytest",
27 | env={
28 | "TESTING_SETTINGS": testing_settings,
29 | **({"TESTING_PRECACHED_PY_VALUES": "1"} if precache else {}),
30 | },
31 | )
32 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "django-content-settings"
3 | version = "0.29.2"
4 | description = "DCS - the most advanced admin editable setting"
5 | homepage = "https://django-content-settings.readthedocs.io/"
6 | repository = "https://github.com/occipital/django-content-settings/"
7 | authors = ["oduvan "]
8 | keywords = ["Django", "settings"]
9 | readme = "README_SHORT.md"
10 | classifiers = [
11 | "Framework :: Django :: 3.2",
12 | "Framework :: Django :: 4",
13 | "Framework :: Django :: 4.0",
14 | "Framework :: Django :: 4.1",
15 | "Framework :: Django :: 4.2",
16 | "Framework :: Django :: 5.0",
17 | "Framework :: Django :: 5.1",
18 | "Programming Language :: Python :: 3.8",
19 | "Programming Language :: Python :: 3.9",
20 | "Programming Language :: Python :: 3.10",
21 | "Programming Language :: Python :: 3.11",
22 | "Programming Language :: Python :: 3.12",
23 | "Programming Language :: Python :: 3.13",
24 | ]
25 | license = "MIT"
26 | packages = [{include = "content_settings"}]
27 |
28 | [tool.poetry.dependencies]
29 | python = ">=3.8"
30 | django = ">=3.2"
31 |
32 |
33 | [tool.poetry.group.test.dependencies]
34 | pytest = "^7.4.3"
35 | pytest-mock = "^3.12.0"
36 | pytest-django = "^4.7.0"
37 | django-webtest = "^1.9.11"
38 | pytest-cov = "^4.1.0"
39 | nox = "^2023.4.22"
40 |
41 |
42 | [tool.poetry.group.dev.dependencies]
43 | ipdb = "^0.13.13"
44 |
45 |
46 | [tool.poetry.group.docs.dependencies]
47 | mkdocs = "^1.5.3"
48 |
49 | [build-system]
50 | requires = ["poetry-core"]
51 | build-backend = "poetry.core.masonry.api"
52 |
--------------------------------------------------------------------------------
/set_version.py:
--------------------------------------------------------------------------------
1 | import tomllib
2 |
3 | with open("pyproject.toml", "rb") as toml_file:
4 | data = tomllib.load(toml_file)
5 |
6 | print(data["tool"]["poetry"]["name"])
7 | print(data["tool"]["poetry"]["version"])
8 |
9 | with open("content_settings/__init__.py", "w") as f:
10 | f.write(f'__version__ = "{data["tool"]["poetry"]["version"]}"\n')
11 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | yaml_installed = False
4 | try:
5 | import yaml
6 |
7 | yaml_installed = True
8 | except ImportError:
9 | pass
10 |
11 | testing_settings = os.environ.get("TESTING_SETTINGS", "normal")
12 | testing_settings_normal = testing_settings == "normal"
13 | testing_settings_full = testing_settings == "full"
14 | testing_settings_min = testing_settings == "min"
15 | testing_precached_py_values = os.environ.get("TESTING_PRECACHED_PY_VALUES", False)
16 |
--------------------------------------------------------------------------------
/tests/books/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 |
4 | class Book(models.Model):
5 | title = models.CharField(max_length=255)
6 | description = models.TextField()
7 |
8 | class Meta:
9 | permissions = [
10 | ("can_read_todo", "Can view all books"),
11 | ("can_edit_todo", "Can view all books"),
12 | ]
13 |
14 | def __str__(self):
15 | return self.title
16 |
--------------------------------------------------------------------------------
/tests/books/templates/books/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ CONTENT_SETTINGS.TITLE }}
5 |
6 |
7 |
8 |
9 |
Title
10 |
Price
11 |
12 | {% for book in CONTENT_SETTINGS.BOOKS %}
13 |