django-resonant-settings 0.26__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. django_resonant_settings-0.26/.gitignore +176 -0
  2. django_resonant_settings-0.26/LICENSE +201 -0
  3. django_resonant_settings-0.26/PKG-INFO +36 -0
  4. django_resonant_settings-0.26/README.md +9 -0
  5. django_resonant_settings-0.26/pyproject.toml +77 -0
  6. django_resonant_settings-0.26/resonant_settings/__init__.py +0 -0
  7. django_resonant_settings-0.26/resonant_settings/_env.py +3 -0
  8. django_resonant_settings-0.26/resonant_settings/allauth.py +48 -0
  9. django_resonant_settings-0.26/resonant_settings/allauth_support/__init__.py +0 -0
  10. django_resonant_settings-0.26/resonant_settings/allauth_support/adapter.py +10 -0
  11. django_resonant_settings-0.26/resonant_settings/allauth_support/apps.py +6 -0
  12. django_resonant_settings-0.26/resonant_settings/allauth_support/createsuperuser.py +66 -0
  13. django_resonant_settings-0.26/resonant_settings/allauth_support/management/commands/createsuperuser.py +30 -0
  14. django_resonant_settings-0.26/resonant_settings/allauth_support/receiver.py +37 -0
  15. django_resonant_settings-0.26/resonant_settings/allauth_support/utils.py +14 -0
  16. django_resonant_settings-0.26/resonant_settings/celery.py +64 -0
  17. django_resonant_settings-0.26/resonant_settings/debug_toolbar.py +13 -0
  18. django_resonant_settings-0.26/resonant_settings/development/__init__.py +0 -0
  19. django_resonant_settings-0.26/resonant_settings/development/celery.py +7 -0
  20. django_resonant_settings-0.26/resonant_settings/development/extensions.py +9 -0
  21. django_resonant_settings-0.26/resonant_settings/django.py +37 -0
  22. django_resonant_settings-0.26/resonant_settings/logging.py +89 -0
  23. django_resonant_settings-0.26/resonant_settings/oauth_toolkit.py +32 -0
  24. django_resonant_settings-0.26/resonant_settings/production/__init__.py +0 -0
  25. django_resonant_settings-0.26/resonant_settings/production/email.py +16 -0
  26. django_resonant_settings-0.26/resonant_settings/production/https.py +22 -0
  27. django_resonant_settings-0.26/resonant_settings/production/s3_storage.py +31 -0
  28. django_resonant_settings-0.26/resonant_settings/py.typed +0 -0
  29. django_resonant_settings-0.26/resonant_settings/rest_framework.py +75 -0
  30. django_resonant_settings-0.26/resonant_settings/testing/__init__.py +0 -0
  31. django_resonant_settings-0.26/resonant_settings/testing/minio_storage.py +23 -0
@@ -0,0 +1,176 @@
1
+ # Created by https://www.toptal.com/developers/gitignore/api/python
2
+ # Edit at https://www.toptal.com/developers/gitignore?templates=python
3
+
4
+ ### Python ###
5
+ # Byte-compiled / optimized / DLL files
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+
10
+ # C extensions
11
+ *.so
12
+
13
+ # Distribution / packaging
14
+ .Python
15
+ build/
16
+ develop-eggs/
17
+ dist/
18
+ downloads/
19
+ eggs/
20
+ .eggs/
21
+ lib/
22
+ lib64/
23
+ parts/
24
+ sdist/
25
+ var/
26
+ wheels/
27
+ share/python-wheels/
28
+ *.egg-info/
29
+ .installed.cfg
30
+ *.egg
31
+ MANIFEST
32
+
33
+ # PyInstaller
34
+ # Usually these files are written by a python script from a template
35
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
36
+ *.manifest
37
+ *.spec
38
+
39
+ # Installer logs
40
+ pip-log.txt
41
+ pip-delete-this-directory.txt
42
+
43
+ # Unit test / coverage reports
44
+ htmlcov/
45
+ .tox/
46
+ .nox/
47
+ .coverage
48
+ .coverage.*
49
+ .cache
50
+ nosetests.xml
51
+ coverage.xml
52
+ *.cover
53
+ *.py,cover
54
+ .hypothesis/
55
+ .pytest_cache/
56
+ cover/
57
+
58
+ # Translations
59
+ *.mo
60
+ *.pot
61
+
62
+ # Django stuff:
63
+ *.log
64
+ local_settings.py
65
+ db.sqlite3
66
+ db.sqlite3-journal
67
+
68
+ # Flask stuff:
69
+ instance/
70
+ .webassets-cache
71
+
72
+ # Scrapy stuff:
73
+ .scrapy
74
+
75
+ # Sphinx documentation
76
+ docs/_build/
77
+
78
+ # PyBuilder
79
+ .pybuilder/
80
+ target/
81
+
82
+ # Jupyter Notebook
83
+ .ipynb_checkpoints
84
+
85
+ # IPython
86
+ profile_default/
87
+ ipython_config.py
88
+
89
+ # pyenv
90
+ # For a library or package, you might want to ignore these files since the code is
91
+ # intended to run in multiple environments; otherwise, check them in:
92
+ # .python-version
93
+
94
+ # pipenv
95
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
96
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
97
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
98
+ # install all needed dependencies.
99
+ #Pipfile.lock
100
+
101
+ # poetry
102
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
103
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
104
+ # commonly ignored for libraries.
105
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
106
+ #poetry.lock
107
+
108
+ # pdm
109
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
110
+ #pdm.lock
111
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
112
+ # in version control.
113
+ # https://pdm.fming.dev/#use-with-ide
114
+ .pdm.toml
115
+
116
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
117
+ __pypackages__/
118
+
119
+ # Celery stuff
120
+ celerybeat-schedule
121
+ celerybeat.pid
122
+
123
+ # SageMath parsed files
124
+ *.sage.py
125
+
126
+ # Environments
127
+ .env
128
+ .venv
129
+ env/
130
+ venv/
131
+ ENV/
132
+ env.bak/
133
+ venv.bak/
134
+
135
+ # Spyder project settings
136
+ .spyderproject
137
+ .spyproject
138
+
139
+ # Rope project settings
140
+ .ropeproject
141
+
142
+ # mkdocs documentation
143
+ /site
144
+
145
+ # mypy
146
+ .mypy_cache/
147
+ .dmypy.json
148
+ dmypy.json
149
+
150
+ # Pyre type checker
151
+ .pyre/
152
+
153
+ # pytype static type analyzer
154
+ .pytype/
155
+
156
+ # Cython debug symbols
157
+ cython_debug/
158
+
159
+ # PyCharm
160
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
161
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
162
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
163
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
164
+ #.idea/
165
+
166
+ ### Python Patch ###
167
+ # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
168
+ poetry.toml
169
+
170
+ # ruff
171
+ .ruff_cache/
172
+
173
+ # LSP config files
174
+ pyrightconfig.json
175
+
176
+ # End of https://www.toptal.com/developers/gitignore/api/python
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
@@ -0,0 +1,36 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-resonant-settings
3
+ Version: 0.26
4
+ Summary: Shared Django settings for Resonant applications.
5
+ Project-URL: Repository, https://github.com/kitware-resonant/cookiecutter-resonant
6
+ Project-URL: Bug Reports, https://github.com/kitware-resonant/cookiecutter-resonant/issues
7
+ Maintainer-email: "Kitware, Inc." <kitware@kitware.com>
8
+ License: Apache 2.0
9
+ License-File: LICENSE
10
+ Keywords: django,resonant,setting,settings
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Web Environment
13
+ Classifier: Framework :: Django
14
+ Classifier: Framework :: Django :: 5
15
+ Classifier: Framework :: Django :: 5.1
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: License :: OSI Approved :: Apache Software License
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Requires-Python: >=3.13
23
+ Requires-Dist: django-environ
24
+ Provides-Extra: allauth
25
+ Requires-Dist: django-allauth; extra == 'allauth'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # django-resonant-settings
29
+ [![PyPI](https://img.shields.io/pypi/v/django-resonant-settings)](https://pypi.org/project/django-resonant-settings/)
30
+
31
+ Shared Django settings for Resonant applications.
32
+
33
+ # Installation and Usage
34
+ This package is tightly coupled to
35
+ [`cookiecutter-resonant`](https://github.com/kitware-resonant/cookiecutter-resonant) and should
36
+ be used within a cookiecutter-derived Resonant application.
@@ -0,0 +1,9 @@
1
+ # django-resonant-settings
2
+ [![PyPI](https://img.shields.io/pypi/v/django-resonant-settings)](https://pypi.org/project/django-resonant-settings/)
3
+
4
+ Shared Django settings for Resonant applications.
5
+
6
+ # Installation and Usage
7
+ This package is tightly coupled to
8
+ [`cookiecutter-resonant`](https://github.com/kitware-resonant/cookiecutter-resonant) and should
9
+ be used within a cookiecutter-derived Resonant application.
@@ -0,0 +1,77 @@
1
+ [build-system]
2
+ requires = ["hatchling", "hatch-vcs"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "django-resonant-settings"
7
+ description = "Shared Django settings for Resonant applications."
8
+ readme = "README.md"
9
+ requires-python = ">=3.13"
10
+ license = { text = "Apache 2.0" }
11
+ maintainers = [{ name = "Kitware, Inc.", email = "kitware@kitware.com" }]
12
+ keywords = [
13
+ "django",
14
+ "resonant",
15
+ "setting",
16
+ "settings",
17
+ ]
18
+ classifiers = [
19
+ "Development Status :: 3 - Alpha",
20
+ "Environment :: Web Environment",
21
+ "Framework :: Django :: 5",
22
+ "Framework :: Django :: 5.1",
23
+ "Framework :: Django",
24
+ "Intended Audience :: Developers",
25
+ "License :: OSI Approved :: Apache Software License",
26
+ "Operating System :: OS Independent",
27
+ "Programming Language :: Python :: 3",
28
+ "Programming Language :: Python :: 3.13",
29
+ "Programming Language :: Python",
30
+ ]
31
+ dependencies = [
32
+ "django-environ",
33
+ ]
34
+ dynamic = ["version"]
35
+
36
+ [project.optional-dependencies]
37
+ allauth = [
38
+ "django-allauth",
39
+ ]
40
+
41
+ [project.urls]
42
+ Repository = "https://github.com/kitware-resonant/cookiecutter-resonant"
43
+ "Bug Reports" = "https://github.com/kitware-resonant/cookiecutter-resonant/issues"
44
+
45
+ [tool.hatch.build]
46
+ packages = [
47
+ "resonant_settings",
48
+ ]
49
+
50
+ [tool.hatch.version]
51
+ source = "vcs"
52
+ raw-options = { root = ".." }
53
+
54
+ [tool.black]
55
+ line-length = 100
56
+ target-version = ["py313"]
57
+
58
+ [tool.isort]
59
+ profile = "black"
60
+ line_length = 100
61
+ # Sort by name, don't cluster "from" vs "import"
62
+ force_sort_within_sections = true
63
+ # Combines "as" imports on the same line
64
+ combine_as_imports = true
65
+
66
+ [tool.mypy]
67
+ files = [
68
+ "resonant_settings",
69
+ ]
70
+ show_error_codes = true
71
+
72
+ [[tool.mypy.overrides]]
73
+ module = [
74
+ "allauth.*",
75
+ "environ.*",
76
+ ]
77
+ ignore_missing_imports = true
@@ -0,0 +1,3 @@
1
+ import environ
2
+
3
+ env = environ.Env()
@@ -0,0 +1,48 @@
1
+ """
2
+ Configure django-allauth with the following features:
3
+ * Disable usernames for end users, using exclusively email addresses for login
4
+ * Require email verification
5
+ * Quality of life improvements for users
6
+
7
+ This requires the `django-allauth` package to be installed and requires
8
+ `resonant_settings.allauth_support` to be added to INSTALLED_APPS.
9
+ """
10
+
11
+ # The sites framework requires this to be set.
12
+ # In the unlikely case where a database's pk sequence for the django_site table is not reset,
13
+ # the default site object could have a different pk. Then this will need to be overridden
14
+ # downstream.
15
+ SITE_ID = 1
16
+
17
+ AUTHENTICATION_BACKENDS = [
18
+ # Django's built-in ModelBackend is not necessary, since all users will be
19
+ # authenticated by their email address
20
+ "allauth.account.auth_backends.AuthenticationBackend",
21
+ ]
22
+
23
+ # see configuration documentation at
24
+ # https://django-allauth.readthedocs.io/en/latest/configuration.html
25
+
26
+ # Require email verification, but this can be overridden
27
+ ACCOUNT_EMAIL_VERIFICATION = "mandatory"
28
+
29
+ # Use email as the identifier for login
30
+ ACCOUNT_AUTHENTICATION_METHOD = "email"
31
+ ACCOUNT_EMAIL_REQUIRED = True
32
+ ACCOUNT_USERNAME_REQUIRED = False
33
+
34
+ # Set the username as the email
35
+ ACCOUNT_ADAPTER = "resonant_settings.allauth_support.adapter.EmailAsUsernameAccountAdapter"
36
+ ACCOUNT_USER_MODEL_USERNAME_FIELD = None
37
+
38
+ # Quality of life improvements, but may not work if the browser is closed
39
+ ACCOUNT_SESSION_REMEMBER = True
40
+ ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True
41
+ ACCOUNT_LOGIN_ON_PASSWORD_RESET = True
42
+
43
+ # These will permit GET requests to mutate the user state, but significantly improve usability
44
+ ACCOUNT_LOGOUT_ON_GET = True
45
+ ACCOUNT_CONFIRM_EMAIL_ON_GET = True
46
+
47
+ # This will likely become the default in the future, but enable it now
48
+ ACCOUNT_PRESERVE_USERNAME_CASING = False
@@ -0,0 +1,10 @@
1
+ from allauth.account.adapter import DefaultAccountAdapter
2
+ from django.contrib.auth.models import AbstractUser
3
+ from django.http import HttpRequest
4
+
5
+
6
+ class EmailAsUsernameAccountAdapter(DefaultAccountAdapter):
7
+ """Automatically populate the username as the email address."""
8
+
9
+ def populate_username(self, request: HttpRequest, user: AbstractUser) -> None:
10
+ user.username = user.email
@@ -0,0 +1,6 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class AllauthSupportConfig(AppConfig):
5
+ name = "resonant_settings.allauth_support"
6
+ verbose_name = "Resonant settings django-allauth support"
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import ClassVar
4
+
5
+ from django.contrib.auth.management.commands import createsuperuser
6
+ from django.contrib.auth.models import User, UserManager
7
+
8
+ from resonant_settings.allauth_support.utils import temporarily_change_attributes
9
+
10
+
11
+ class Command(createsuperuser.Command):
12
+ def __init__(self, *args, **kwargs):
13
+ super().__init__(*args, **kwargs)
14
+ self.UserModel = EmailAsUsernameProxyUser
15
+ self.username_field = self.UserModel._meta.get_field(self.UserModel.USERNAME_FIELD)
16
+
17
+ def _validate_username(self, username, verbose_field_name, database):
18
+ # Since "username" is actually unique, "email" (i.e. "self.username_field") is logically
19
+ # unique too. Explicitly setting the "_unique" attribute ensures that app-level duplicate
20
+ # checking is done by "_validate_username", which produces better, earlier error messages.
21
+ # Out of an abundance of caution, set the "_unique" attribute back to its original value
22
+ # when this is done.
23
+ with temporarily_change_attributes(self.username_field, _unique=True):
24
+ # Normalize (as it would be done before saving) for better duplicate detection
25
+ username = self.UserModel.normalize_username(username)
26
+ return super()._validate_username(username, verbose_field_name, database)
27
+
28
+
29
+ class EmailAsUsernameProxyUserManager(UserManager):
30
+ # This version of "create_superuser" makes the "username" argument optional
31
+ def create_superuser(
32
+ self,
33
+ username: str | None = None,
34
+ email: str | None = None,
35
+ password: str | None = None,
36
+ **extra_fields,
37
+ ) -> EmailAsUsernameProxyUser:
38
+ # Practically, email will always be provided
39
+ assert email
40
+ user = super().create_superuser(
41
+ username=email, email=email, password=password, **extra_fields
42
+ )
43
+ return user
44
+
45
+
46
+ class EmailAsUsernameProxyUser(User):
47
+ # https://github.com/typeddjango/django-stubs/issues/2112
48
+ class Meta(User.Meta): # type: ignore[name-defined]
49
+ proxy = True
50
+
51
+ objects = EmailAsUsernameProxyUserManager()
52
+
53
+ # "createsuperuser.Command" automatically includes the referent of "USERNAME_FIELD", and we want
54
+ # to apply username labeling, help text, and validation rules from the actual "email" field
55
+ USERNAME_FIELD = "email"
56
+
57
+ # Don't include "email" in "REQUIRED_FIELDS", to prevent adding that field twice to the
58
+ # "createsuperuser.Command" argument parser
59
+ REQUIRED_FIELDS: ClassVar[list[str]] = []
60
+
61
+ @classmethod
62
+ def normalize_username(cls, username: str) -> str:
63
+ # This method is called from "UserManager._create_user" with the actual "username" field.
64
+ # To ensure that the saved value of the "username" field exactly matches the "email" field,
65
+ # apply the same normalization process.
66
+ return EmailAsUsernameProxyUserManager.normalize_email(username)
@@ -0,0 +1,30 @@
1
+ from typing import cast
2
+
3
+ from allauth.account import app_settings as allauth_settings
4
+ from django.contrib.auth import get_user_model
5
+ from django.contrib.auth.management.commands import createsuperuser as django_createsuperuser
6
+ from django.contrib.auth.models import AbstractUser
7
+ from django.core.management import BaseCommand
8
+ from django.db.models.signals import post_save
9
+
10
+ from resonant_settings.allauth_support import createsuperuser as allauth_support_createsuperuser
11
+ from resonant_settings.allauth_support.receiver import verify_email_address_on_user_post_save
12
+
13
+ """
14
+ When Allauth is configured to use a User's `email` as the `username`, override the `createsuperuser`
15
+ management command to only prompt for an email address.
16
+ """
17
+
18
+ # If using email as username
19
+ if not allauth_settings.USERNAME_REQUIRED:
20
+ # Expose the modified command
21
+ Command: type[BaseCommand] = allauth_support_createsuperuser.Command
22
+ user_model: type[AbstractUser] = allauth_support_createsuperuser.EmailAsUsernameProxyUser
23
+
24
+ else:
25
+ # Expose the pristine upstream version of the command
26
+ Command = django_createsuperuser.Command
27
+ user_model = cast(type[AbstractUser], get_user_model())
28
+
29
+ # Always automatically verify email addresses of newly created superusers
30
+ post_save.connect(verify_email_address_on_user_post_save, sender=user_model)
@@ -0,0 +1,37 @@
1
+ import logging
2
+
3
+ from allauth.account.models import EmailAddress
4
+ from django.contrib.auth.models import AbstractUser
5
+
6
+ logger = logging.getLogger(__file__)
7
+
8
+
9
+ def verify_email_address_on_user_post_save(
10
+ sender: type[AbstractUser],
11
+ instance: AbstractUser,
12
+ created: bool,
13
+ **kwargs,
14
+ ):
15
+ """Automatically verify email addresses of newly created superusers."""
16
+ # These should always be true, but it's a final sanity check
17
+ if created and instance.is_superuser:
18
+ # Django is less strict than Allauth about duplicate email addresses (Django lowercases
19
+ # the domain portion or an address, Allauth lowercases the whole address). It's possible
20
+ # an acceptable email address via createsuperuser would be refused as a duplicate via
21
+ # Allauth.
22
+ # It's also possible that in an mature database where users have multiple non-primary
23
+ # emails, the new user's email matches an existing user's non-primary email, violating
24
+ # uniqueness.
25
+ # So, make a conservative effort at setting the email address as verified, since failure is
26
+ # not critical and can be resolved by the user.
27
+ email_address, created = EmailAddress.objects.get_or_create(
28
+ email__iexact=instance.email,
29
+ defaults={
30
+ "user": instance,
31
+ "email": instance.email,
32
+ "verified": True,
33
+ "primary": True,
34
+ },
35
+ )
36
+ if not created:
37
+ logger.warning(f'Could not automatically verify email address "{instance.email}".')
@@ -0,0 +1,14 @@
1
+ from contextlib import contextmanager
2
+
3
+
4
+ # From https://stackoverflow.com/a/38532086
5
+ @contextmanager
6
+ def temporarily_change_attributes(something, **kwargs):
7
+ previous_values = {k: getattr(something, k) for k in kwargs}
8
+ for k, v in kwargs.items():
9
+ setattr(something, k, v)
10
+ try:
11
+ yield
12
+ finally:
13
+ for k, v in previous_values.items():
14
+ setattr(something, k, v)
@@ -0,0 +1,64 @@
1
+ """
2
+ Configure Celery with the following features:
3
+ * Disable the results backend
4
+ * Ensure that tasks will never be lost, but tasks themselves must be idempotent
5
+ * Optimize the network connection to CloudAMQP
6
+
7
+ This requires the `celery` package to be installed.
8
+ """
9
+
10
+ from resonant_settings._env import env
11
+
12
+ # Assume AMQP.
13
+ CELERY_BROKER_URL: str = env.str("DJANGO_CELERY_BROKER_URL")
14
+
15
+ # Disable results backend, as this feature has too many weaknesses.
16
+ # The database should be used to communicate results of completed tasks.
17
+ CELERY_RESULT_BACKEND = None
18
+
19
+ # Only acknowledge a task being done after the function finishes.
20
+ # This provides safety against worker crashes, but adds the requirement
21
+ # that tasks must be idempotent (which is a best practice anyway).
22
+ # See: https://docs.celeryproject.org/en/stable/faq.html#should-i-use-retry-or-acks-late
23
+ CELERY_TASK_ACKS_LATE = True
24
+
25
+ # When a worker subprocess abruptly exists, assume it was is killed by the operating system for
26
+ # a cause which is intrinsic (e.g. a segfault or OOM) to the task it was running, so do not
27
+ # requeue. It's expected that the task wouldn't succeed if run again.
28
+ # This should not impact cases where the task fails due to extrinsic causes (e.g. the process
29
+ # supervisor sends a SIGKILL or the machine loses power), as we assume that the parent worker
30
+ # process will immediately die too (and not have a chance to requeue the task).
31
+ # See: https://docs.celeryproject.org/en/stable/userguide/tasks.html#tasks for more explanation
32
+ # of these tradeoffs.
33
+ # None of this affects warm shutdowns from a SIGTERM (which the process supervisor ought to
34
+ # send), as this just allows Celery to complete running tasks; see:
35
+ # https://docs.celeryproject.org/en/stable/userguide/workers.html#process-signals for reference.
36
+ # This is Celery's default.
37
+ CELERY_TASK_REJECT_ON_WORKER_LOST = False
38
+
39
+ # When a task fails due to an internally-raised exception or due to a timeout, do not requeue.
40
+ # It's expected that the task wouldn't succeed if run again.
41
+ # This is Celery's default.
42
+ CELERY_TASK_ACKS_ON_FAILURE_OR_TIMEOUT = True
43
+
44
+ # This is sensible behavior with TASKS_ACKS_LATE, this must be enabled to prevent warnings,
45
+ # and this will be Celery's default in 6.0.
46
+ CELERY_WORKER_CANCEL_LONG_RUNNING_TASKS_ON_CONNECTION_LOSS = True
47
+
48
+ # CloudAMQP-suggested settings
49
+ # https://www.cloudamqp.com/docs/celery.html
50
+ CELERY_BROKER_POOL_LIMIT = 1
51
+ CELERY_BROKER_HEARTBEAT = None
52
+ CELERY_BROKER_CONNECTION_TIMEOUT = 30
53
+ CELERY_EVENT_QUEUE_EXPIRES = 60
54
+
55
+ # Note, CELERY_WORKER settings could be different on each running worker.
56
+
57
+ # Do not prefetch, as the speed benefit for fast-running tasks may not be
58
+ # worth a potentially unfair allocation with slow-running tasks and
59
+ # multiple workers.
60
+ CELERY_WORKER_PREFETCH_MULTIPLIER = 1
61
+
62
+ # Accept the default of the number of CPU cores.
63
+ # Workers running memory-intensive tasks may need to decrease this.
64
+ CELERY_WORKER_CONCURRENCY: int | None = None
@@ -0,0 +1,13 @@
1
+ """
2
+ Configure Django Debug Toolbar with the following features:
3
+ * Improve performance with large queries
4
+
5
+ This requires the `django-debug-toolbar` package to be installed.
6
+ """
7
+
8
+ DEBUG_TOOLBAR_CONFIG = {
9
+ # The default size often is too small, causing an inability to view queries
10
+ "RESULTS_CACHE_SIZE": 250,
11
+ # If this setting is True, large sql queries can cause the page to render slowly
12
+ "PRETTIFY_SQL": False,
13
+ }
@@ -0,0 +1,7 @@
1
+ # Acknowledge early in development, which will help prevent failing or
2
+ # long-running tasks from being started automatically every time the worker
3
+ # process restarts; this more aggressively flushes the task queue.
4
+ CELERY_TASK_ACKS_LATE = False
5
+
6
+ # In development, run without concurrency.
7
+ CELERY_WORKER_CONCURRENCY: int | None = 1
@@ -0,0 +1,9 @@
1
+ """
2
+ Configure Django Extensions.
3
+
4
+ This requires the `django-extensions` package to be installed.
5
+ """
6
+
7
+ SHELL_PLUS_PRINT_SQL = True
8
+ SHELL_PLUS_PRINT_SQL_TRUNCATE = None
9
+ RUNSERVER_PLUS_PRINT_SQL_TRUNCATE = None
@@ -0,0 +1,37 @@
1
+ """
2
+ Configure a basic Django project.
3
+ """
4
+
5
+ TEMPLATES = [
6
+ {
7
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
8
+ "DIRS": [],
9
+ "APP_DIRS": True,
10
+ "OPTIONS": {
11
+ "context_processors": [
12
+ "django.template.context_processors.debug",
13
+ "django.template.context_processors.request",
14
+ "django.contrib.auth.context_processors.auth",
15
+ "django.contrib.messages.context_processors.messages",
16
+ ],
17
+ },
18
+ },
19
+ ]
20
+
21
+ PASSWORD_HASHERS = [
22
+ # Argon2 is recommended by OWASP, so make it the default for new passwords
23
+ # https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
24
+ "django.contrib.auth.hashers.Argon2PasswordHasher",
25
+ # scrypt was the default hasher in older versions of Resonant,
26
+ # so it must be enabled to read old passwords
27
+ "django.contrib.auth.hashers.ScryptPasswordHasher",
28
+ # Support for other hashers isn't needed,
29
+ # since databases shouldn't have entries with other algorithms
30
+ ]
31
+ # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
32
+ AUTH_PASSWORD_VALIDATORS = [
33
+ {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
34
+ {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
35
+ {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
36
+ {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
37
+ ]
@@ -0,0 +1,89 @@
1
+ """
2
+ Configure Django logging with the following features:
3
+ * Emit all logs to stdout
4
+ * Exclude favicons and static files from request and server logs
5
+ * Improve log formatting and add colorization
6
+
7
+ This requires the `rich` package to be installed.
8
+ """
9
+
10
+ import logging
11
+
12
+ from django.http import HttpRequest
13
+
14
+
15
+ def _filter_favicon_requests(record: logging.LogRecord) -> bool:
16
+ if record.name == "django.request":
17
+ request: HttpRequest | None = getattr(record, "request", None)
18
+ if request and request.path == "/favicon.ico":
19
+ return False
20
+
21
+ if (
22
+ record.name == "django.server"
23
+ and isinstance(record.args, tuple)
24
+ and len(record.args) >= 1
25
+ and str(record.args[0]).startswith("GET /favicon.ico ")
26
+ ):
27
+ return False
28
+
29
+ return True
30
+
31
+
32
+ def _filter_static_requests(record: logging.LogRecord) -> bool:
33
+ if (
34
+ record.name == "django.server"
35
+ and isinstance(record.args, tuple)
36
+ and len(record.args) >= 1
37
+ and str(record.args[0]).startswith("GET /static/")
38
+ ):
39
+ return False
40
+
41
+ return True
42
+
43
+
44
+ LOGGING = {
45
+ "version": 1,
46
+ # Replace existing logging configuration
47
+ "incremental": False,
48
+ # This redefines all of Django's declared loggers, but most loggers are implicitly
49
+ # declared on usage, and should not be disabled. They often propagate their output
50
+ # to the root anyway.
51
+ "disable_existing_loggers": False,
52
+ "formatters": {"rich": {"datefmt": "[%X]"}},
53
+ "filters": {
54
+ "filter_favicon_requests": {
55
+ "()": "django.utils.log.CallbackFilter",
56
+ "callback": _filter_favicon_requests,
57
+ },
58
+ "filter_static_requests": {
59
+ "()": "django.utils.log.CallbackFilter",
60
+ "callback": _filter_static_requests,
61
+ },
62
+ },
63
+ "handlers": {
64
+ "console": {
65
+ "class": "rich.logging.RichHandler",
66
+ "formatter": "rich",
67
+ "filters": ["filter_favicon_requests", "filter_static_requests"],
68
+ },
69
+ },
70
+ # Existing loggers actually contain direct (non-string) references to existing handlers,
71
+ # so after redefining handlers, all existing loggers must be redefined too
72
+ "loggers": {
73
+ # Configure the root logger to output to the console
74
+ "": {"level": "INFO", "handlers": ["console"], "propagate": False},
75
+ # Django defines special configurations for the "django" and "django.server" loggers,
76
+ # but we will manage all content at the root logger instead, so reset those
77
+ # configurations.
78
+ "django": {
79
+ "handlers": [],
80
+ "level": "NOTSET",
81
+ "propagate": True,
82
+ },
83
+ "django.server": {
84
+ "handlers": [],
85
+ "level": "NOTSET",
86
+ "propagate": True,
87
+ },
88
+ },
89
+ }
@@ -0,0 +1,32 @@
1
+ """
2
+ Configure Django OAuth Toolkit with the following features:
3
+ * Harden security
4
+ * Improve usability of token scopes
5
+ * Improve quality of live for out of band flows and non-refreshing clients
6
+
7
+ This requires the `django-oauth-toolkit` package to be installed.
8
+ """
9
+
10
+ from datetime import timedelta
11
+
12
+ OAUTH2_PROVIDER = {
13
+ "ALLOWED_REDIRECT_URI_SCHEMES": ["https"],
14
+ # Don't require users to re-approve scopes each time
15
+ "REQUEST_APPROVAL_PROMPT": "auto",
16
+ # ERROR_RESPONSE_WITH_SCOPES is only used with the "permission_classes" helpers for scopes.
17
+ # If the scope itself is confidential, this could leak information. but the usability
18
+ # benefit is probably worth it.
19
+ "ERROR_RESPONSE_WITH_SCOPES": True,
20
+ # Allow 5 minutes for a flow to exchange an auth code for a token. This is typically
21
+ # 60 seconds but out-of-band flows may take a bit longer. A maximum of 10 minutes is
22
+ # recommended: https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.
23
+ "AUTHORIZATION_CODE_EXPIRE_SECONDS": timedelta(minutes=5).total_seconds(),
24
+ # Django can persist logins for longer than this via cookies,
25
+ # but non-refreshing clients will need to redirect to Django's auth every 24 hours.
26
+ "ACCESS_TOKEN_EXPIRE_SECONDS": timedelta(days=1).total_seconds(),
27
+ # This allows refresh tokens to eventually be removed from the database by
28
+ # "manage.py cleartokens". This value is not actually enforced when refresh tokens are
29
+ # checked, but it can be assumed that all clients will need to redirect to Django's auth
30
+ # every 30 days.
31
+ "REFRESH_TOKEN_EXPIRE_SECONDS": timedelta(days=30).total_seconds(),
32
+ }
@@ -0,0 +1,16 @@
1
+ """
2
+ Configure Django's email sending.
3
+
4
+ The following environment variables must be externally set:
5
+ * `DJANGO_EMAIL_URL`, as a URL for login to an STMP server, as parsed by `dj-email-url`. This
6
+ typically will start with `submission:`. Special characters in passwords must be URL-encoded.
7
+ See https://pypi.org/project/dj-email-url/ for full details.
8
+ * `DJANGO_DEFAULT_FROM_EMAIL`, as the default From address for outgoing email.
9
+ """
10
+
11
+ from resonant_settings._env import env
12
+
13
+ vars().update(env.email_url("DJANGO_EMAIL_URL"))
14
+
15
+ DEFAULT_FROM_EMAIL: str = env.str("DJANGO_DEFAULT_FROM_EMAIL")
16
+ SERVER_EMAIL = DEFAULT_FROM_EMAIL
@@ -0,0 +1,22 @@
1
+ """Configure Django's security middleware to use and require HTTPS."""
2
+
3
+ from datetime import timedelta
4
+
5
+ SECURE_SSL_REDIRECT = True
6
+
7
+ # This needs to be set by the HTTPS terminating reverse proxy.
8
+ # Heroku and Render automatically set this.
9
+ SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
10
+
11
+ SESSION_COOKIE_SECURE = True
12
+ CSRF_COOKIE_SECURE = True
13
+
14
+ # Enable HSTS
15
+ SECURE_HSTS_SECONDS = timedelta(days=365).total_seconds()
16
+ # This is already False by default, but it's important to ensure HSTS is not forced on other
17
+ # subdomains which may have different HTTPS practices.
18
+ SECURE_HSTS_INCLUDE_SUBDOMAINS = False
19
+ # This is already False by default, but per https://hstspreload.org/#opt-in, projects should
20
+ # opt-in to preload by overriding this setting. Additionally, all subdomains must have HSTS to
21
+ # register for preloading.
22
+ SECURE_HSTS_PRELOAD = False
@@ -0,0 +1,31 @@
1
+ """
2
+ Configure S3Storage.
3
+
4
+ The following environment variables must be externally set:
5
+ * AWS_DEFAULT_REGION
6
+ * AWS_ACCESS_KEY_ID
7
+ * AWS_SECRET_ACCESS_KEY
8
+ * DJANGO_STORAGE_BUCKET_NAME
9
+
10
+ This requires the `django-storages[s3]` package to be installed.
11
+ """
12
+
13
+ from datetime import timedelta
14
+
15
+ from resonant_settings._env import env
16
+
17
+ # These exact environment variable names are important,
18
+ # as direct instantiations of Boto will also respect them.
19
+ AWS_S3_REGION_NAME: str = env.str("AWS_DEFAULT_REGION")
20
+ AWS_S3_ACCESS_KEY_ID: str = env.str("AWS_ACCESS_KEY_ID")
21
+ AWS_S3_SECRET_ACCESS_KEY: str = env.str("AWS_SECRET_ACCESS_KEY")
22
+
23
+ AWS_STORAGE_BUCKET_NAME: str = env.str("DJANGO_STORAGE_BUCKET_NAME")
24
+
25
+ # It's critical to use the v4 signature;
26
+ # it isn't the upstream default only for backwards compatability reasons.
27
+ AWS_S3_SIGNATURE_VERSION = "s3v4"
28
+
29
+ AWS_S3_MAX_MEMORY_SIZE = 5 * 1024 * 1024
30
+ AWS_S3_FILE_OVERWRITE = False
31
+ AWS_QUERYSTRING_EXPIRE = timedelta(hours=6).total_seconds()
@@ -0,0 +1,75 @@
1
+ """
2
+ Configure Django REST framework and drf-yasg.
3
+
4
+ This requires the `django-oauth-toolkit` and `drf-yasg` packages to be installed.
5
+ """
6
+
7
+ from typing import Any
8
+
9
+ # When SessionAuthentication is allowed, it's critical that the following settings
10
+ # (respectively part of Django and django-cors-headers) are set to these values (although those
11
+ # are the also the default values).
12
+ SESSION_COOKIE_SAMESITE = "Lax"
13
+ CORS_ALLOW_CREDENTIALS = False
14
+
15
+ REST_FRAMEWORK = {
16
+ "DEFAULT_AUTHENTICATION_CLASSES": [
17
+ "oauth2_provider.contrib.rest_framework.OAuth2Authentication",
18
+ # Allow SessionAuthentication, as this is much more convenient for Ajax requests
19
+ # from server-rendered pages, including:
20
+ # * YASG (Swagger / ReDoc)
21
+ # * The Admin interface, when using interactive fields like S3-file-field
22
+ # * Augmentation of server-rendered views with background Javascript
23
+ # (see https://docs.djangoproject.com/en/3.1/ref/csrf/#ajax )
24
+ # It's important that true SPAs and other clients be forced to go though
25
+ # OAuth2Authentication instead, as this is the only supported auth mechanism which
26
+ # robustly works across origins; however, it turns out that this can only be enforced
27
+ # partially.
28
+ # To understand why, first read https://web.dev/same-site-same-origin/ to understand
29
+ # that even with "SameSite=Lax" (or "SameSite=Strict"), cookies are only technically
30
+ # limited to same-site requests, and do not have the stronger same-origin limitation.
31
+ # If a naive SPA developer configures their client to include credentials
32
+ # ("{withCredentials: true}" in XHR, jQuery, and Axios, or "{credentials: 'include'}"
33
+ # in Fetch; this configuration is often suggested as an "easy" fix for authentication
34
+ # problems by StackOverflow), then the session cookie will be sent with any cross-site
35
+ # requests where the user has logged into the Django server. Note, cross-site requests
36
+ # include origins with a different port (typical in local development) and origins with
37
+ # a different subdomain (common in many deployments). From DRF's perspective, as long
38
+ # as the request uses a safe verb (more on this below), the request will be
39
+ # authenticated transparently. However, since Django is configured to not set
40
+ # "Access-Control-Allow-Credentials", the SPA client will not be able to read the
41
+ # response and get a CORS error; this is the right outcome (client cannot effectively
42
+ # make the request), but with a confusing and hard-to-debug reason (CORS error,
43
+ # instead of a 401/403) and developers may be tempted to "fix" it by enabling
44
+ # "Access-Control-Allow-Credentials" instead of fixing their client to use OAuth
45
+ # correctly, which will likely lead to further bugs when the SPA is deployed to a
46
+ # non-same-site environment. Alternatively, if the request does not use a safe verb
47
+ # (and the request is not preflighted by the browser, which is permitted in some
48
+ # cases), DRF will enforce CSRF protection and the request will 403 fail with a
49
+ # "CSRF Failed: CSRF token missing or incorrect" message, which is also confusing and
50
+ # may lead developers to incorrect fixes.
51
+ # TL;DR: Developers of SPAs may encounter misleading error messages when making Ajax
52
+ # requests "withCredentials", but security is still maintained.
53
+ "rest_framework.authentication.SessionAuthentication",
54
+ ],
55
+ # This is a much more sensible degree of basic security
56
+ "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticatedOrReadOnly"],
57
+ # BoundedLimitOffsetPagination provides LimitOffsetPagination with a maximum page size
58
+ "DEFAULT_PAGINATION_CLASS": "resonant_utils.rest_framework.BoundedLimitOffsetPagination",
59
+ # This provides a sane default for requests that do not specify a page size.
60
+ # This also ensures that endpoints with pagination will always return a
61
+ # pagination-structured response.
62
+ "PAGE_SIZE": 100,
63
+ # Real clients typically JSON-encode their request bodies, so the test client should too
64
+ "TEST_REQUEST_DEFAULT_FORMAT": "json",
65
+ }
66
+
67
+ SWAGGER_SETTINGS: dict[str, Any] = {
68
+ # The default security definition ("basic") is not supported by this DRF configuration,
69
+ # so expect all logins to come via the Django session, which there's no OpenAPI
70
+ # security definition for.
71
+ "SECURITY_DEFINITIONS": None,
72
+ "USE_SESSION_AUTH": True,
73
+ }
74
+
75
+ REDOC_SETTINGS: dict[str, Any] = {}
@@ -0,0 +1,23 @@
1
+ """
2
+ Configure MinioMediaStorage.
3
+
4
+ This requires the `django-minio-storage` package to be installed.
5
+ """
6
+ from urllib.parse import ParseResult
7
+
8
+ from resonant_settings._env import env
9
+
10
+ minio_url: ParseResult = env.url("DJANGO_MINIO_STORAGE_URL")
11
+ MINIO_STORAGE_USE_HTTPS = minio_url.scheme == "https"
12
+ MINIO_STORAGE_ENDPOINT = minio_url.hostname + (f":{minio_url.port}" if minio_url.port else "")
13
+ MINIO_STORAGE_ACCESS_KEY = minio_url.username
14
+ MINIO_STORAGE_SECRET_KEY = minio_url.password
15
+ MINIO_STORAGE_MEDIA_BUCKET_NAME = minio_url.path.lstrip("/")
16
+
17
+ # Setting this allows MinIO to work through network namespace partitions
18
+ # (e.g. when running within Docker Compose)
19
+ MINIO_STORAGE_MEDIA_URL: str | None = env.str("DJANGO_MINIO_STORAGE_MEDIA_URL", default=None)
20
+
21
+ MINIO_STORAGE_AUTO_CREATE_MEDIA_BUCKET = True
22
+ MINIO_STORAGE_AUTO_CREATE_MEDIA_POLICY = "READ_WRITE"
23
+ MINIO_STORAGE_MEDIA_USE_PRESIGNED = True