django-content-studio 1.0.0a1__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.
- django_content_studio-1.0.0a1/LICENSE +21 -0
- django_content_studio-1.0.0a1/PKG-INFO +88 -0
- django_content_studio-1.0.0a1/README.md +72 -0
- django_content_studio-1.0.0a1/content_studio/__init__.py +7 -0
- django_content_studio-1.0.0a1/content_studio/admin.py +154 -0
- django_content_studio-1.0.0a1/content_studio/apps.py +59 -0
- django_content_studio-1.0.0a1/content_studio/dashboard.py +7 -0
- django_content_studio-1.0.0a1/content_studio/form.py +76 -0
- django_content_studio-1.0.0a1/content_studio/login_backends/__init__.py +21 -0
- django_content_studio-1.0.0a1/content_studio/login_backends/username_password.py +79 -0
- django_content_studio-1.0.0a1/content_studio/models.py +86 -0
- django_content_studio-1.0.0a1/content_studio/router.py +14 -0
- django_content_studio-1.0.0a1/content_studio/serializers.py +16 -0
- django_content_studio-1.0.0a1/content_studio/settings.py +146 -0
- django_content_studio-1.0.0a1/content_studio/static/content_studio/assets/browser-ponyfill-Ct7s-5jI.js +2 -0
- django_content_studio-1.0.0a1/content_studio/static/content_studio/assets/index.css +1 -0
- django_content_studio-1.0.0a1/content_studio/static/content_studio/assets/index.js +106 -0
- django_content_studio-1.0.0a1/content_studio/static/content_studio/assets/inter-cyrillic-ext-wght-normal.woff2 +0 -0
- django_content_studio-1.0.0a1/content_studio/static/content_studio/assets/inter-cyrillic-wght-normal.woff2 +0 -0
- django_content_studio-1.0.0a1/content_studio/static/content_studio/assets/inter-greek-ext-wght-normal.woff2 +0 -0
- django_content_studio-1.0.0a1/content_studio/static/content_studio/assets/inter-greek-wght-normal.woff2 +0 -0
- django_content_studio-1.0.0a1/content_studio/static/content_studio/assets/inter-latin-ext-wght-normal.woff2 +0 -0
- django_content_studio-1.0.0a1/content_studio/static/content_studio/assets/inter-latin-wght-normal.woff2 +0 -0
- django_content_studio-1.0.0a1/content_studio/static/content_studio/assets/inter-vietnamese-wght-normal.woff2 +0 -0
- django_content_studio-1.0.0a1/content_studio/static/content_studio/index.html +14 -0
- django_content_studio-1.0.0a1/content_studio/static/content_studio/locales/en/translation.json +11 -0
- django_content_studio-1.0.0a1/content_studio/static/content_studio/locales/nl/translation.json +11 -0
- django_content_studio-1.0.0a1/content_studio/static/content_studio/vite.svg +1 -0
- django_content_studio-1.0.0a1/content_studio/templates/content_studio/index.html +20 -0
- django_content_studio-1.0.0a1/content_studio/token_backends/__init__.py +39 -0
- django_content_studio-1.0.0a1/content_studio/token_backends/jwt.py +61 -0
- django_content_studio-1.0.0a1/content_studio/urls.py +12 -0
- django_content_studio-1.0.0a1/content_studio/utils.py +30 -0
- django_content_studio-1.0.0a1/content_studio/views.py +94 -0
- django_content_studio-1.0.0a1/content_studio/viewsets.py +128 -0
- django_content_studio-1.0.0a1/content_studio/widgets.py +30 -0
- django_content_studio-1.0.0a1/pyproject.toml +24 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Leon van der Grient
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: django-content-studio
|
|
3
|
+
Version: 1.0.0a1
|
|
4
|
+
Summary: Modern & flexible Django admin
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Leon van der Grient
|
|
7
|
+
Author-email: leon@devtastic.io
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# Django Content Studio
|
|
18
|
+
|
|
19
|
+
[](https://badge.fury.io/py/django-content-studio)
|
|
20
|
+
[](https://pypi.org/project/django-content-studio/)
|
|
21
|
+
[](https://www.djangoproject.com/)
|
|
22
|
+
[](LICENSE)
|
|
23
|
+
|
|
24
|
+
Django Content Studio is a modern, flexible alternative to the Django admin.
|
|
25
|
+
|
|
26
|
+
> This package is still under development
|
|
27
|
+
|
|
28
|
+
## 🚀 Quick Start
|
|
29
|
+
|
|
30
|
+
### Installation
|
|
31
|
+
|
|
32
|
+
☝️ Django Content Studio depends on Django and Django Rest Framework.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install django-content-studio
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Add to Django Settings
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
# settings.py
|
|
42
|
+
INSTALLED_APPS = [
|
|
43
|
+
'django.contrib.admin',
|
|
44
|
+
'django.contrib.auth',
|
|
45
|
+
'django.contrib.contenttypes',
|
|
46
|
+
'django.contrib.sessions',
|
|
47
|
+
'django.contrib.messages',
|
|
48
|
+
'django.contrib.staticfiles',
|
|
49
|
+
'rest_framework',
|
|
50
|
+
'content_studio', # Add this
|
|
51
|
+
# ... your apps
|
|
52
|
+
]
|
|
53
|
+
```
|
|
54
|
+
### Add URLs
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
# urls.py
|
|
58
|
+
urlpatterns = [
|
|
59
|
+
path("admin/", include("content_studio.urls")),
|
|
60
|
+
# ... your urls
|
|
61
|
+
]
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## 🐛 Issues & Support
|
|
65
|
+
|
|
66
|
+
- 🐛 **Bug Reports**: [GitHub Issues](https://github.com/StructuralRealist/django-content-studio/issues)
|
|
67
|
+
- 💬 **Discussions**: [GitHub Discussions](https://github.com/StructuralRealist/django-content-studio/discussions)
|
|
68
|
+
- 📧 **Email**: leon@devtastic.io
|
|
69
|
+
|
|
70
|
+
## 📄 License
|
|
71
|
+
|
|
72
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
73
|
+
|
|
74
|
+
## 🙏 Acknowledgments
|
|
75
|
+
|
|
76
|
+
- Built with React and Tailwind CSS
|
|
77
|
+
- Inspired by the original Django admin
|
|
78
|
+
- Thanks to all contributors and the Django community
|
|
79
|
+
|
|
80
|
+
## 🔗 Links
|
|
81
|
+
|
|
82
|
+
- [PyPI Package](https://pypi.org/project/django-content-studio/)
|
|
83
|
+
- [GitHub Repository](https://github.com/StructuralRealist/django-content-studio)
|
|
84
|
+
- [Changelog](CHANGELOG.md)
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
Made in Europe 🇪🇺 with 💚 for Django
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Django Content Studio
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/py/django-content-studio)
|
|
4
|
+
[](https://pypi.org/project/django-content-studio/)
|
|
5
|
+
[](https://www.djangoproject.com/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
Django Content Studio is a modern, flexible alternative to the Django admin.
|
|
9
|
+
|
|
10
|
+
> This package is still under development
|
|
11
|
+
|
|
12
|
+
## 🚀 Quick Start
|
|
13
|
+
|
|
14
|
+
### Installation
|
|
15
|
+
|
|
16
|
+
☝️ Django Content Studio depends on Django and Django Rest Framework.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install django-content-studio
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Add to Django Settings
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
# settings.py
|
|
26
|
+
INSTALLED_APPS = [
|
|
27
|
+
'django.contrib.admin',
|
|
28
|
+
'django.contrib.auth',
|
|
29
|
+
'django.contrib.contenttypes',
|
|
30
|
+
'django.contrib.sessions',
|
|
31
|
+
'django.contrib.messages',
|
|
32
|
+
'django.contrib.staticfiles',
|
|
33
|
+
'rest_framework',
|
|
34
|
+
'content_studio', # Add this
|
|
35
|
+
# ... your apps
|
|
36
|
+
]
|
|
37
|
+
```
|
|
38
|
+
### Add URLs
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
# urls.py
|
|
42
|
+
urlpatterns = [
|
|
43
|
+
path("admin/", include("content_studio.urls")),
|
|
44
|
+
# ... your urls
|
|
45
|
+
]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 🐛 Issues & Support
|
|
49
|
+
|
|
50
|
+
- 🐛 **Bug Reports**: [GitHub Issues](https://github.com/StructuralRealist/django-content-studio/issues)
|
|
51
|
+
- 💬 **Discussions**: [GitHub Discussions](https://github.com/StructuralRealist/django-content-studio/discussions)
|
|
52
|
+
- 📧 **Email**: leon@devtastic.io
|
|
53
|
+
|
|
54
|
+
## 📄 License
|
|
55
|
+
|
|
56
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
57
|
+
|
|
58
|
+
## 🙏 Acknowledgments
|
|
59
|
+
|
|
60
|
+
- Built with React and Tailwind CSS
|
|
61
|
+
- Inspired by the original Django admin
|
|
62
|
+
- Thanks to all contributors and the Django community
|
|
63
|
+
|
|
64
|
+
## 🔗 Links
|
|
65
|
+
|
|
66
|
+
- [PyPI Package](https://pypi.org/project/django-content-studio/)
|
|
67
|
+
- [GitHub Repository](https://github.com/StructuralRealist/django-content-studio)
|
|
68
|
+
- [Changelog](CHANGELOG.md)
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
Made in Europe 🇪🇺 with 💚 for Django
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
from django.contrib.admin import *
|
|
2
|
+
from rest_framework.request import HttpRequest
|
|
3
|
+
|
|
4
|
+
from .dashboard import Dashboard
|
|
5
|
+
from .form import FormSet, FormSetGroup
|
|
6
|
+
from .login_backends import LoginBackendManager
|
|
7
|
+
from .token_backends import TokenBackendManager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AdminSite(AdminSite):
|
|
11
|
+
"""
|
|
12
|
+
Enhanced admin site for Django Content Studio and integration with
|
|
13
|
+
Django Content Framework.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
token_backend = TokenBackendManager()
|
|
17
|
+
|
|
18
|
+
login_backend = LoginBackendManager()
|
|
19
|
+
|
|
20
|
+
dashboard = Dashboard()
|
|
21
|
+
|
|
22
|
+
def __init__(self, *args, **kwargs):
|
|
23
|
+
super().__init__(*args, **kwargs)
|
|
24
|
+
# Add token backend's view set to the
|
|
25
|
+
# Content Studio router.
|
|
26
|
+
self.token_backend.set_up_router()
|
|
27
|
+
# Add login backend's view set to the
|
|
28
|
+
# Content Studio router.
|
|
29
|
+
self.login_backend.set_up_router()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
admin_site = AdminSite()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ModelAdmin(ModelAdmin):
|
|
36
|
+
"""
|
|
37
|
+
Enhanced model admin for Django Content Studio and integration with
|
|
38
|
+
Django Content Framework. Although it's relatively backwards compatible,
|
|
39
|
+
some default behavior has been changed.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
# We set a lower limit than Django's default of 100
|
|
43
|
+
list_per_page = 20
|
|
44
|
+
|
|
45
|
+
# Configure the main section in the edit-view.
|
|
46
|
+
edit_main: list[type[FormSetGroup | FormSet | str]] = []
|
|
47
|
+
|
|
48
|
+
# Configure the sidebar in the edit-view.
|
|
49
|
+
edit_sidebar: list[type[FormSet | str]] = []
|
|
50
|
+
|
|
51
|
+
def save_model(self, request, obj, form, change):
|
|
52
|
+
if hasattr(obj, "edited_by"):
|
|
53
|
+
obj.edited_by = request.user
|
|
54
|
+
super().save_model(request, obj, form, change)
|
|
55
|
+
|
|
56
|
+
def has_add_permission(self, request):
|
|
57
|
+
is_singleton = getattr(self.model, "is_singleton", False)
|
|
58
|
+
|
|
59
|
+
# Don't allow to add more than one singleton object.
|
|
60
|
+
if is_singleton and self.model.objects.get():
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
return super().has_add_permission(request)
|
|
64
|
+
|
|
65
|
+
def has_delete_permission(self, request, obj=None):
|
|
66
|
+
is_singleton = getattr(self.model, "is_singleton", False)
|
|
67
|
+
|
|
68
|
+
if is_singleton:
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
return super().has_delete_permission(request, obj)
|
|
72
|
+
|
|
73
|
+
def render_change_form(self, request, context, *args, **kwargs):
|
|
74
|
+
is_singleton = getattr(self.model, "is_singleton", False)
|
|
75
|
+
|
|
76
|
+
context["show_save_and_add_another"] = not is_singleton
|
|
77
|
+
|
|
78
|
+
return super().render_change_form(request, context, *args, **kwargs)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class AdminSerializer:
|
|
82
|
+
"""
|
|
83
|
+
Class for serializing Django admin classes.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def __init__(self, admin_class: ModelAdmin):
|
|
87
|
+
self.admin_class = admin_class
|
|
88
|
+
|
|
89
|
+
def serialize(self, request: HttpRequest):
|
|
90
|
+
admin_class = self.admin_class
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
"edit": {
|
|
94
|
+
"main": self.serialize_edit_main(request),
|
|
95
|
+
"sidebar": self.serialize_edit_sidebar(request),
|
|
96
|
+
},
|
|
97
|
+
"list": {
|
|
98
|
+
"per_page": admin_class.list_per_page,
|
|
99
|
+
},
|
|
100
|
+
"permissions": {
|
|
101
|
+
"add_permission": admin_class.has_add_permission(request),
|
|
102
|
+
"delete_permission": admin_class.has_delete_permission(request),
|
|
103
|
+
"change_permission": admin_class.has_change_permission(request),
|
|
104
|
+
"view_permission": admin_class.has_view_permission(request),
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
def serialize_edit_main(self, request):
|
|
109
|
+
admin_class = self.admin_class
|
|
110
|
+
|
|
111
|
+
return [
|
|
112
|
+
i.serialize()
|
|
113
|
+
for i in self.get_edit_main(
|
|
114
|
+
getattr(admin_class, "edit_main", admin_class.get_fields(request))
|
|
115
|
+
)
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
def serialize_edit_sidebar(self, request):
|
|
119
|
+
admin_class = self.admin_class
|
|
120
|
+
|
|
121
|
+
return [
|
|
122
|
+
i.serialize()
|
|
123
|
+
for i in self.get_edit_sidebar(getattr(admin_class, "edit_sidebar", None))
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
def get_edit_main(self, edit_main):
|
|
127
|
+
"""
|
|
128
|
+
Returns a normalized list of form set groups.
|
|
129
|
+
|
|
130
|
+
Form sets will be wrapped in a form set group. If the edit_main attribute is a list of fields,
|
|
131
|
+
they are wrapped in a form set and a form set group.
|
|
132
|
+
"""
|
|
133
|
+
if not edit_main:
|
|
134
|
+
return []
|
|
135
|
+
if isinstance(edit_main[0], FormSetGroup):
|
|
136
|
+
return edit_main
|
|
137
|
+
if isinstance(edit_main[0], FormSet):
|
|
138
|
+
return [FormSetGroup(formsets=edit_main)]
|
|
139
|
+
|
|
140
|
+
return [FormSetGroup(formsets=[FormSet(fields=edit_main)])]
|
|
141
|
+
|
|
142
|
+
def get_edit_sidebar(self, edit_sidebar):
|
|
143
|
+
"""
|
|
144
|
+
Returns a normalized list of form sets for the edit_sidebar.
|
|
145
|
+
|
|
146
|
+
If the edit_sidebar attribute is a list of fields,
|
|
147
|
+
they are wrapped in a form set.
|
|
148
|
+
"""
|
|
149
|
+
if not edit_sidebar:
|
|
150
|
+
return []
|
|
151
|
+
if isinstance(edit_sidebar[0], FormSet):
|
|
152
|
+
return edit_sidebar
|
|
153
|
+
|
|
154
|
+
return [FormSet(fields=edit_sidebar)]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from django.apps import AppConfig
|
|
2
|
+
from django.contrib import admin
|
|
3
|
+
from rest_framework import serializers
|
|
4
|
+
|
|
5
|
+
from headless.utils import is_runserver
|
|
6
|
+
from . import VERSION
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DjangoContentStudioConfig(AppConfig):
|
|
10
|
+
name = "content_studio"
|
|
11
|
+
label = "content_studio"
|
|
12
|
+
initialized = False
|
|
13
|
+
|
|
14
|
+
def ready(self):
|
|
15
|
+
from .utils import log
|
|
16
|
+
|
|
17
|
+
if is_runserver() and not self.initialized:
|
|
18
|
+
self.initialized = True
|
|
19
|
+
|
|
20
|
+
log("\n")
|
|
21
|
+
log("----------------------------------------")
|
|
22
|
+
log("Django Content Studio")
|
|
23
|
+
log(f"Version {VERSION}")
|
|
24
|
+
log("----------------------------------------")
|
|
25
|
+
log(":rocket:", "Starting Django Content Studio")
|
|
26
|
+
log(":mag:", "Discovering admin models...")
|
|
27
|
+
registered_models = len(admin.site._registry)
|
|
28
|
+
log(
|
|
29
|
+
":white_check_mark:",
|
|
30
|
+
f"[green]Found {registered_models} admin models[/green]",
|
|
31
|
+
)
|
|
32
|
+
self._create_crud_api()
|
|
33
|
+
log("\n")
|
|
34
|
+
|
|
35
|
+
def _create_crud_api(self):
|
|
36
|
+
from .viewsets import BaseModelViewSet
|
|
37
|
+
from .router import content_studio_router
|
|
38
|
+
from .utils import log
|
|
39
|
+
|
|
40
|
+
for _model, admin_model in admin.site._registry.items():
|
|
41
|
+
|
|
42
|
+
class Serializer(serializers.ModelSerializer):
|
|
43
|
+
class Meta:
|
|
44
|
+
model = _model
|
|
45
|
+
fields = "__all__"
|
|
46
|
+
|
|
47
|
+
class ViewSet(BaseModelViewSet):
|
|
48
|
+
serializer_class = Serializer
|
|
49
|
+
queryset = _model.objects.all()
|
|
50
|
+
|
|
51
|
+
content_studio_router.register(
|
|
52
|
+
f"api/content/{_model._meta.label_lower}",
|
|
53
|
+
ViewSet,
|
|
54
|
+
f"content_studio_api-{_model._meta.label_lower}",
|
|
55
|
+
)
|
|
56
|
+
log(
|
|
57
|
+
":white_check_mark:",
|
|
58
|
+
f"[green]Created CRUD API[/green]",
|
|
59
|
+
)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
class Field:
|
|
2
|
+
"""
|
|
3
|
+
Field class for configuring the fields in content edit views in Django Content Studio.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
def __init__(self, name: str, col_span: int = 1):
|
|
7
|
+
self.name = name
|
|
8
|
+
self.col_span = col_span
|
|
9
|
+
|
|
10
|
+
def serialize(self):
|
|
11
|
+
return {
|
|
12
|
+
"name": self.name,
|
|
13
|
+
"col_span": self.col_span,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FieldLayout:
|
|
18
|
+
"""
|
|
19
|
+
Field layout class for configuring the layout of fields in content edit views in Django Content Studio.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, fields: list[str | Field] = None, columns: int = 1):
|
|
23
|
+
self.fields = fields or []
|
|
24
|
+
self.columns = columns
|
|
25
|
+
|
|
26
|
+
def serialize(self):
|
|
27
|
+
return {
|
|
28
|
+
"fields": [
|
|
29
|
+
field.serialize() if isinstance(field, Field) else field
|
|
30
|
+
for field in self.fields
|
|
31
|
+
],
|
|
32
|
+
"columns": self.columns,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class FormSet:
|
|
37
|
+
"""
|
|
38
|
+
Formset class for configuring the blocks of fields in content edit views
|
|
39
|
+
in Django Content Studio.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
title: str = "",
|
|
45
|
+
description: str = "",
|
|
46
|
+
fields: list[str | Field | FieldLayout] = None,
|
|
47
|
+
):
|
|
48
|
+
self.title = title
|
|
49
|
+
self.description = description
|
|
50
|
+
self.fields = fields or []
|
|
51
|
+
|
|
52
|
+
def serialize(self):
|
|
53
|
+
return {
|
|
54
|
+
"title": self.title,
|
|
55
|
+
"description": self.description,
|
|
56
|
+
"fields": [
|
|
57
|
+
field.serialize() if isinstance(field, (Field, FieldLayout)) else field
|
|
58
|
+
for field in self.fields
|
|
59
|
+
],
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class FormSetGroup:
|
|
64
|
+
"""
|
|
65
|
+
Formset group class for configuring the groups of form sets in content edit views.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, label: str = "", formsets: list[FormSet] = None):
|
|
69
|
+
self.label = label
|
|
70
|
+
self.formsets = formsets or []
|
|
71
|
+
|
|
72
|
+
def serialize(self):
|
|
73
|
+
return {
|
|
74
|
+
"label": self.label,
|
|
75
|
+
"formsets": [formset.serialize() for formset in self.formsets],
|
|
76
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from .username_password import UsernamePasswordBackend
|
|
2
|
+
from ..router import content_studio_router
|
|
3
|
+
from ..settings import cs_settings
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LoginBackendManager:
|
|
7
|
+
"""
|
|
8
|
+
Manages different login backends for use by
|
|
9
|
+
Content Studio.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, **kwargs):
|
|
13
|
+
self.active_backends = cs_settings.LOGIN_BACKENDS
|
|
14
|
+
|
|
15
|
+
def set_up_router(self):
|
|
16
|
+
for backend in self.active_backends:
|
|
17
|
+
content_studio_router.register(
|
|
18
|
+
f"api/login/{backend.__name__.lower().replace('backend', '')}",
|
|
19
|
+
backend.view_set,
|
|
20
|
+
basename="content_studio_login_backend",
|
|
21
|
+
)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from django.contrib.auth import get_user_model, authenticate
|
|
2
|
+
from rest_framework import serializers
|
|
3
|
+
from rest_framework.exceptions import PermissionDenied
|
|
4
|
+
from rest_framework.permissions import AllowAny
|
|
5
|
+
from rest_framework.renderers import JSONRenderer
|
|
6
|
+
from rest_framework.viewsets import ViewSet
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class UsernamePasswordSerializer(serializers.Serializer):
|
|
10
|
+
username = serializers.CharField(required=True)
|
|
11
|
+
password = serializers.CharField(required=True)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class UsernamePasswordViewSet(ViewSet):
|
|
15
|
+
"""
|
|
16
|
+
View set for username and password endpoints.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
permission_classes = [AllowAny]
|
|
20
|
+
renderer_classes = [JSONRenderer]
|
|
21
|
+
|
|
22
|
+
def create(self, request):
|
|
23
|
+
serializer = UsernamePasswordSerializer(data=request.data)
|
|
24
|
+
|
|
25
|
+
serializer.is_valid(raise_exception=True)
|
|
26
|
+
|
|
27
|
+
user = UsernamePasswordBackend.login(
|
|
28
|
+
username=serializer.validated_data["username"],
|
|
29
|
+
password=serializer.validated_data["password"],
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if user is None or not user.is_active:
|
|
33
|
+
raise PermissionDenied()
|
|
34
|
+
|
|
35
|
+
from ..admin import admin_site
|
|
36
|
+
|
|
37
|
+
return admin_site.auth_backend.active_backend.get_response_for_user(user)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class UsernamePasswordBackend:
|
|
41
|
+
name = "Username password"
|
|
42
|
+
view_set = UsernamePasswordViewSet
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def get_info(cls):
|
|
46
|
+
"""
|
|
47
|
+
Returns information about the backend.
|
|
48
|
+
"""
|
|
49
|
+
user_model = get_user_model()
|
|
50
|
+
username_field = getattr(user_model, user_model.USERNAME_FIELD, None)
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
"type": cls.__name__,
|
|
54
|
+
"config": {"username_field_type": username_field.field.__class__.__name__},
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def login(cls, username, password):
|
|
59
|
+
"""
|
|
60
|
+
Authenticates user using username and password.
|
|
61
|
+
Returns the user if successful, None otherwise.
|
|
62
|
+
"""
|
|
63
|
+
return authenticate(username=username, password=password)
|
|
64
|
+
|
|
65
|
+
def request_password_reset(self, username):
|
|
66
|
+
"""
|
|
67
|
+
Sends a password reset email.
|
|
68
|
+
"""
|
|
69
|
+
raise NotImplemented(
|
|
70
|
+
"You need to implement a method for sending a password reset token."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def complete_password_reset(self, reset_token, new_password):
|
|
74
|
+
"""
|
|
75
|
+
Sets the new password based on the reset token.
|
|
76
|
+
"""
|
|
77
|
+
raise NotImplemented(
|
|
78
|
+
"You need to implement a method for validating a reset token and setting a new password."
|
|
79
|
+
)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
|
|
3
|
+
from content_framework import fields as cf_fields
|
|
4
|
+
from . import widgets
|
|
5
|
+
from .utils import is_jsonable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ModelSerializer:
|
|
9
|
+
def __init__(self, model: type[models.Model]):
|
|
10
|
+
self.model = model
|
|
11
|
+
|
|
12
|
+
widgets = {
|
|
13
|
+
models.CharField: widgets.InputWidget,
|
|
14
|
+
models.IntegerField: widgets.InputWidget,
|
|
15
|
+
models.SmallIntegerField: widgets.InputWidget,
|
|
16
|
+
models.BigIntegerField: widgets.InputWidget,
|
|
17
|
+
models.PositiveIntegerField: widgets.InputWidget,
|
|
18
|
+
models.PositiveSmallIntegerField: widgets.InputWidget,
|
|
19
|
+
models.PositiveBigIntegerField: widgets.InputWidget,
|
|
20
|
+
models.FloatField: widgets.InputWidget,
|
|
21
|
+
models.DecimalField: widgets.InputWidget,
|
|
22
|
+
models.SlugField: widgets.SlugWidget,
|
|
23
|
+
models.TextField: widgets.TextAreaWidget,
|
|
24
|
+
models.BooleanField: widgets.BooleanWidget,
|
|
25
|
+
models.NullBooleanField: widgets.BooleanWidget,
|
|
26
|
+
cf_fields.MultipleChoiceField: widgets.MultipleChoiceWidget,
|
|
27
|
+
cf_fields.TagField: widgets.TagWidget,
|
|
28
|
+
cf_fields.HTMLField: widgets.RichTextWidget,
|
|
29
|
+
cf_fields.URLPathField: widgets.URLPathWidget,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
def serialize(self):
|
|
33
|
+
model = self.model
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
"label": model._meta.label,
|
|
37
|
+
"verbose_name": model._meta.verbose_name,
|
|
38
|
+
"verbose_name_plural": model._meta.verbose_name_plural,
|
|
39
|
+
"fields": self.get_fields(),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
def get_fields(self):
|
|
43
|
+
fields = {}
|
|
44
|
+
|
|
45
|
+
for field in self.model._meta.fields:
|
|
46
|
+
fields[field.name] = self.get_field(field)
|
|
47
|
+
|
|
48
|
+
return fields
|
|
49
|
+
|
|
50
|
+
def get_field(self, field):
|
|
51
|
+
widget = self.get_widget(field)
|
|
52
|
+
|
|
53
|
+
data = {
|
|
54
|
+
"verbose_name": field.verbose_name,
|
|
55
|
+
"required": not field.null or not field.blank,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if field.help_text:
|
|
59
|
+
data["help_text"] = field.help_text
|
|
60
|
+
|
|
61
|
+
if is_jsonable(field.default):
|
|
62
|
+
data["default"] = field.default
|
|
63
|
+
|
|
64
|
+
if widget:
|
|
65
|
+
data["widget"] = widget
|
|
66
|
+
|
|
67
|
+
if not field.editable:
|
|
68
|
+
data["readonly"] = True
|
|
69
|
+
|
|
70
|
+
if field.primary_key:
|
|
71
|
+
data["primary_key"] = True
|
|
72
|
+
data["readonly"] = True
|
|
73
|
+
|
|
74
|
+
if getattr(field, "choices", None) is not None:
|
|
75
|
+
data["choices"] = field.choices
|
|
76
|
+
|
|
77
|
+
if getattr(field, "max_length", None) is not None:
|
|
78
|
+
data["max_length"] = field.max_length
|
|
79
|
+
|
|
80
|
+
return data
|
|
81
|
+
|
|
82
|
+
def get_widget(self, field):
|
|
83
|
+
try:
|
|
84
|
+
return self.widgets[field.__class__].__name__
|
|
85
|
+
except KeyError:
|
|
86
|
+
return None
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from rest_framework.routers import DefaultRouter
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ExtendedRouter(DefaultRouter):
|
|
5
|
+
def get_method_map(self, viewset, method_map):
|
|
6
|
+
_method_map = super().get_method_map(viewset, method_map)
|
|
7
|
+
|
|
8
|
+
if getattr(viewset, "is_singleton", False):
|
|
9
|
+
_method_map["patch"] = "update"
|
|
10
|
+
|
|
11
|
+
return _method_map
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
content_studio_router = ExtendedRouter(trailing_slash=False)
|