Skip to content

Commit d0516b1

Browse files
committed
Transform dynamic settings logic to validation
1 parent 1bc3591 commit d0516b1

File tree

7 files changed

+134
-82
lines changed

7 files changed

+134
-82
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from types import SimpleNamespace
2+
3+
rest_filters = SimpleNamespace(
4+
reserved_names=(
5+
'page',
6+
'page_size',
7+
'format',
8+
'order',
9+
'order_by',
10+
'search',
11+
'type',
12+
'host_filter',
13+
'count_disabled',
14+
'no_truncate',
15+
'limit',
16+
'validate',
17+
),
18+
dab_rest_filters=(
19+
'ansible_base.rest_filters.rest_framework.type_filter_backend.TypeFilterBackend',
20+
'ansible_base.rest_filters.rest_framework.field_lookup_backend.FieldLookupBackend',
21+
'rest_framework.filters.SearchFilter',
22+
'ansible_base.rest_filters.rest_framework.order_backend.OrderByBackend',
23+
),
24+
)
25+
26+
27+
api_documentation = SimpleNamespace(
28+
auto_schema='drf_spectacular.openapi.AutoSchema',
29+
dab_spectacular_settings={
30+
'TITLE': 'Open API',
31+
'DESCRIPTION': 'Open API',
32+
'VERSION': 'v1',
33+
'SCHEMA_PATH_PREFIX': '/api/v1/',
34+
'COMPONENT_NO_READ_ONLY_REQUIRED': True,
35+
},
36+
)

ansible_base/lib/dynamic_config/dynaconf_helpers.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,16 @@
1616
from dynaconf.utils.files import glob
1717
from dynaconf.utils.functional import empty
1818

19-
from ansible_base.lib.dynamic_config.settings_logic import get_mergeable_dab_settings
19+
from .settings_logic import get_mergeable_dab_settings
20+
from .validators import dab_validators
2021

2122

2223
def factory(
2324
module_name: str, # name of the module that calls this function
2425
app_name: str, # main app name to be used to name env_switcher and envvar_prefix
2526
*,
2627
add_dab_settings: bool = True, # add DAB settings to the settings object
28+
add_dab_validators: bool = True, # add DAB validators
2729
validators: list[Validator] | None = None, # custom validators to be used in Dynaconf
2830
extra_envvar_prefixes: list[str] | None = None, # extra prefixes to be used in envvar loader
2931
**options, # options to be passed to Dynaconf
@@ -125,7 +127,11 @@ def factory(
125127
if validators:
126128
settings.validators.register(*validators)
127129

128-
add_dab_settings and load_dab_settings(settings)
130+
if add_dab_settings:
131+
load_dab_settings(settings)
132+
133+
if add_dab_validators:
134+
load_dab_validators(settings)
129135

130136
# Dynaconf allows composed modes
131137
# so current_env can be a comma separated string of modes (e.g. "development,quiet")
@@ -176,6 +182,14 @@ def load_dab_settings(settings: Dynaconf):
176182
settings.update(dab_settings, loader_identifier="load_dab_settings")
177183

178184

185+
def load_dab_validators(settings: Dynaconf):
186+
"""Add DAB settings validators to the dynaconf settings object."""
187+
installed_apps = settings.get("INSTALLED_APPS")
188+
for app_path, app_validators in dab_validators.items():
189+
if app_path in installed_apps:
190+
settings.validators.register(*app_validators)
191+
192+
179193
def load_standard_settings_files(settings: Dynaconf):
180194
"""Load the standard settings files for Ansible Automation Platform.
181195

ansible_base/lib/dynamic_config/settings_logic.py

Lines changed: 7 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
from ansible_base.lib.cache.fallback_cache import FALLBACK_CACHE, PRIMARY_CACHE
55

6+
from .constants import api_documentation, rest_filters
7+
68
#
79
# If you are adding a new dynamic setting:
810
# Please be sure to modify pyproject.toml with your new settings in tool.setuptools.dynamic
@@ -11,13 +13,6 @@
1113

1214

1315
DEFAULT_AUTH_GROUP = 'auth.Group'
14-
DEFAULT_SPECTACULAR_SETTINGS = {
15-
'TITLE': 'Open API',
16-
'DESCRIPTION': 'Open API',
17-
'VERSION': 'v1',
18-
'SCHEMA_PATH_PREFIX': '/api/v1/',
19-
'COMPONENT_NO_READ_ONLY_REQUIRED': True,
20-
}
2116
DEFAULT_ANSIBLE_BASE_AUTH = "ansible_base.authentication.backend.AnsibleBaseAuth"
2217
DEFAULT_ANSIBLE_BASE_JWT_CONSUMER_APP_NAME = "ansible_base.jwt_consumer"
2318
DEFAULT_ANSIBLE_BASE_RBAC_APP_NAME = "ansible_base.rbac"
@@ -92,51 +87,22 @@ def get_mergeable_dab_settings(settings: dict) -> dict: # NOSONAR
9287
# without enabling the ansible_base.rest_filters app explicitly
9388
# we also apply this to views from other apps so we should always define it
9489
if settings.get('ANSIBLE_BASE_REST_FILTERS_RESERVED_NAMES') is None:
95-
dab_data['ANSIBLE_BASE_REST_FILTERS_RESERVED_NAMES'] = (
96-
'page',
97-
'page_size',
98-
'format',
99-
'order',
100-
'order_by',
101-
'search',
102-
'type',
103-
'host_filter',
104-
'count_disabled',
105-
'no_truncate',
106-
'limit',
107-
'validate',
108-
)
90+
dab_data['ANSIBLE_BASE_REST_FILTERS_RESERVED_NAMES'] = rest_filters.reserved_names
10991

11092
# SPECTACULAR SETTINGS
111-
if 'ansible_base.api_documentation' in installed_apps:
112-
rest_framework.setdefault('DEFAULT_SCHEMA_CLASS', 'drf_spectacular.openapi.AutoSchema')
113-
114-
if 'drf_spectacular' not in installed_apps:
115-
installed_apps.append('drf_spectacular')
116-
117-
for key, value in DEFAULT_SPECTACULAR_SETTINGS.items():
118-
if key not in spectacular_settings:
119-
spectacular_settings[key] = value
93+
# NOTE: validators check default schema class and that drf_spectacular is installed
12094

12195
# General, factual, constant of all filters that ansible_base.rest_filters ships
122-
dab_data['ANSIBLE_BASE_ALL_REST_FILTERS'] = (
123-
'ansible_base.rest_filters.rest_framework.type_filter_backend.TypeFilterBackend',
124-
'ansible_base.rest_filters.rest_framework.field_lookup_backend.FieldLookupBackend',
125-
'rest_framework.filters.SearchFilter',
126-
'ansible_base.rest_filters.rest_framework.order_backend.OrderByBackend',
127-
)
128-
if 'ansible_base.rest_filters' in installed_apps:
129-
rest_framework['DEFAULT_FILTER_BACKENDS'] = dab_data['ANSIBLE_BASE_ALL_REST_FILTERS']
130-
else:
96+
dab_data['ANSIBLE_BASE_ALL_REST_FILTERS'] = rest_filters.dab_rest_filters
97+
if 'ansible_base.rest_filters' not in installed_apps:
13198
# Explanation - these are the filters for views provided by DAB like /authenticators/
13299
# we want them to be enabled by default _even if_ the rest_filters app is not used
133100
# so that clients have consistency, but if an app wants to turn them off, they can.
134101
# these will be combined with the actual DRF defaults in our base view
135102
dab_data['ANSIBLE_BASE_CUSTOM_VIEW_FILTERS'] = dab_data['ANSIBLE_BASE_ALL_REST_FILTERS']
136103

137104
if 'ansible_base.authentication' in installed_apps:
138-
if 'social_django' not in installed_apps:
139-
installed_apps.append('social_django')
105+
# TODO: stop point for migrating modifications to validations
140106
if DEFAULT_ANSIBLE_BASE_AUTH not in authentication_backends:
141107
authentication_backends.append(DEFAULT_ANSIBLE_BASE_AUTH)
142108

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from dynaconf import Validator
2+
3+
from .constants import api_documentation, rest_filters
4+
5+
6+
def has_spectacular(v):
7+
return bool('drf_spectacular' in v)
8+
9+
10+
def has_drf_spectacular_settings(v):
11+
if 'DEFAULT_SCHEMA_CLASS' not in v:
12+
return False
13+
if v['DEFAULT_SCHEMA_CLASS'] != api_documentation.auto_schema:
14+
return False
15+
16+
spectacular_settings = v['SPECTACULAR_SETTINGS']
17+
for key, value in api_documentation.dab_spectacular_settings.items():
18+
user_value = spectacular_settings.get(key)
19+
if user_value != value:
20+
return False
21+
return True
22+
23+
24+
def has_all_rest_filters(v):
25+
user_filters = v.get('DEFAULT_FILTER_BACKENDS', [])
26+
for cls_name in rest_filters.dab_rest_filters:
27+
if cls_name not in user_filters:
28+
return False
29+
return True
30+
31+
32+
def has_social_django(v):
33+
return bool('social_django' in v)
34+
35+
36+
dab_validators = {
37+
"ansible_base.api_documentation": [
38+
Validator("INSTALLED_APPS", condition=has_spectacular),
39+
Validator("REST_FRAMEWORK", condition=has_drf_spectacular_settings),
40+
],
41+
"ansible_base.rest_filters": [
42+
Validator(
43+
"REST_FRAMEWORK",
44+
condition=has_all_rest_filters,
45+
messages={
46+
"condition": "{name} lacks required rest_filters entries in DEFAULT_FILTER_BACKENDS value=({value}) in env {env}, required: "
47+
+ str(rest_filters.dab_rest_filters)
48+
},
49+
)
50+
],
51+
"ansible_base.authentication": [Validator("INSTALLED_APPS", condition=has_social_django)],
52+
}

docs/apps/api_documentation.md

Lines changed: 10 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,47 +4,31 @@ django-ansible-base uses django-spectacular to auto-generate both Open API and S
44

55
## Settings
66

7-
Add `ansible_base.api_documentation` to your installed apps:
7+
Add `ansible_base.api_documentation` to your installed apps.
8+
Plus do what we tell you here.
89

910
```
10-
INSTALLED_APPS = [
11-
...
12-
'ansible_base.api_documentation',
13-
]
14-
```
11+
from ansible_base.lib.dynamic_config.constants import api_documentation
1512
16-
### Additional Settings
17-
Additional settings are required to enable api_documentation.
18-
This will happen automatically if using [dynamic_settings](../Installation.md)
1913
20-
First, you need to add `drf_spectacular` to your `INSTALLED_APPS`:
21-
```
2214
INSTALLED_APPS = [
2315
...
16+
'ansible_base.api_documentation',
2417
'drf_spectacular',
25-
...
2618
]
27-
```
28-
29-
Additionally, we create a `SPECTACULAR_SETTINGS` entry if its not already present:
30-
```
31-
SPECTACULAR_SETTINGS = {
32-
'TITLE': 'Open API',
33-
'DESCRIPTION': 'Open API',
34-
'VERSION': 'v1',
35-
'SCHEMA_PATH_PREFIX': '/api/v1/',
36-
}
37-
```
3819
39-
Finally, add a `DEFAULT_SCHEMA_CLASS` to your `REST_FRAMEWORK` setting:
40-
```
4120
REST_FRAMEWORK = {
4221
...
43-
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
22+
'SPECTACULAR_SETTINGS': api_documentation.dab_spectacular_settings,
23+
'DEFAULT_SCHEMA_CLASS': api_documentation.auto_schema,
4424
...
4525
}
4626
```
4727

28+
If you do not set these, our validators will let you know by throwing an error.
29+
See [dynamic_settings](../Installation.md) for more information.
30+
31+
4832
## URLS
4933

5034
This feature includes URLs which you will get if you are using [dynamic urls](../..//Installation.md)

docs/apps/rest_filters.md

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,24 @@ django-ansible-base has a built in mechanism for filtering and sorting query set
77
Add `ansible_base.rest_filters` to your installed apps:
88

99
```
10+
from ansible_base.lib.dynamic_config.constants import rest_filters
11+
12+
1013
INSTALLED_APPS = [
1114
...
1215
'ansible_base.rest_filters',
1316
]
14-
```
1517
16-
### Additional Settings
17-
Additional settings are required to enable filtering on your rest endpoints.
18-
This will happen automatically if using [dynamic_settings](../Installation.md)
19-
20-
To manually enable filtering without dynamic settings the following items need to be included in your settings:
21-
```
2218
REST_FRAMEWORK = {
2319
...
24-
'DEFAULT_FILTER_BACKENDS': (
25-
'ansible_base.rest_filters.rest_framework.type_filter_backend.TypeFilterBackend',
26-
'ansible_base.rest_filters.rest_framework.field_lookup_backend.FieldLookupBackend',
27-
'rest_framework.filters.SearchFilter',
28-
'ansible_base.rest_filters.rest_framework.order_backend.OrderByBackend',
29-
),
20+
'DEFAULT_FILTER_BACKENDS': rest_filters.dab_rest_filters,
3021
...
3122
}
3223
```
3324

25+
If you do not follow these instructions, the system will throw a validation error.
26+
Read more in [dynamic_settings](../Installation.md).
27+
3428
## Letting Extra Query Params Through
3529

3630
Sometimes you may have a view that needs to use a query param for a reason unrelated to filtering.

test_app/defaults.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
import os
66

7+
from ansible_base.lib.dynamic_config.constants import api_documentation, rest_filters
8+
79
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
810
DEBUG = True
911
ALLOWED_HOSTS = ["*"]
@@ -52,6 +54,7 @@
5254
'django.contrib.staticfiles',
5355
'rest_framework',
5456
'social_django',
57+
'drf_spectacular',
5558
'ansible_base.api_documentation',
5659
'ansible_base.authentication',
5760
'ansible_base.rest_filters',
@@ -98,6 +101,9 @@
98101
'ansible_base.oauth2_provider.permissions.OAuth2ScopePermission',
99102
'ansible_base.rbac.api.permissions.AnsibleBaseObjectPermissions',
100103
],
104+
'SPECTACULAR_SETTINGS': api_documentation.dab_spectacular_settings,
105+
'DEFAULT_SCHEMA_CLASS': api_documentation.auto_schema,
106+
'DEFAULT_FILTER_BACKENDS': rest_filters.dab_rest_filters,
101107
}
102108

103109
DATABASES = {

0 commit comments

Comments
 (0)