django-content-studio 1.0.0b2.post3__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.0b2.post3/LICENSE +21 -0
- django_content_studio-1.0.0b2.post3/PKG-INFO +86 -0
- django_content_studio-1.0.0b2.post3/README.md +70 -0
- django_content_studio-1.0.0b2.post3/content_studio/__init__.py +7 -0
- django_content_studio-1.0.0b2.post3/content_studio/admin.py +298 -0
- django_content_studio-1.0.0b2.post3/content_studio/apps.py +100 -0
- django_content_studio-1.0.0b2.post3/content_studio/dashboard/__init__.py +64 -0
- django_content_studio-1.0.0b2.post3/content_studio/dashboard/activity_log.py +34 -0
- django_content_studio-1.0.0b2.post3/content_studio/filters.py +124 -0
- django_content_studio-1.0.0b2.post3/content_studio/form.py +97 -0
- django_content_studio-1.0.0b2.post3/content_studio/formats.py +59 -0
- django_content_studio-1.0.0b2.post3/content_studio/login_backends/__init__.py +21 -0
- django_content_studio-1.0.0b2.post3/content_studio/login_backends/username_password.py +82 -0
- django_content_studio-1.0.0b2.post3/content_studio/media_library/serializers.py +25 -0
- django_content_studio-1.0.0b2.post3/content_studio/media_library/viewsets.py +132 -0
- django_content_studio-1.0.0b2.post3/content_studio/models.py +73 -0
- django_content_studio-1.0.0b2.post3/content_studio/paginators.py +20 -0
- django_content_studio-1.0.0b2.post3/content_studio/router.py +14 -0
- django_content_studio-1.0.0b2.post3/content_studio/serializers.py +118 -0
- django_content_studio-1.0.0b2.post3/content_studio/settings.py +152 -0
- django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/assets/browser-ponyfill-Ct7s-5jI.js +2 -0
- django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/assets/index.css +1 -0
- django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/assets/index.js +228 -0
- django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/assets/inter-cyrillic-ext-wght-normal.woff2 +0 -0
- django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/assets/inter-cyrillic-wght-normal.woff2 +0 -0
- django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/assets/inter-greek-ext-wght-normal.woff2 +0 -0
- django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/assets/inter-greek-wght-normal.woff2 +0 -0
- django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/assets/inter-latin-ext-wght-normal.woff2 +0 -0
- django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/assets/inter-latin-wght-normal.woff2 +0 -0
- django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/assets/inter-vietnamese-wght-normal.woff2 +0 -0
- django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/icons/pi/Phosphor-Bold.svg +3057 -0
- django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/icons/pi/Phosphor-Bold.ttf +0 -0
- django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/icons/pi/Phosphor-Bold.woff +0 -0
- django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/icons/pi/Phosphor-Bold.woff2 +0 -0
- django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/icons/pi/style.css +4627 -0
- django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/img/media_placeholder.svg +1 -0
- django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/index.html +19 -0
- django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/locales/en/translation.json +82 -0
- django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/locales/nl/translation.json +87 -0
- django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/vite.svg +1 -0
- django_content_studio-1.0.0b2.post3/content_studio/templates/content_studio/index.html +19 -0
- django_content_studio-1.0.0b2.post3/content_studio/token_backends/__init__.py +39 -0
- django_content_studio-1.0.0b2.post3/content_studio/token_backends/jwt.py +56 -0
- django_content_studio-1.0.0b2.post3/content_studio/urls.py +21 -0
- django_content_studio-1.0.0b2.post3/content_studio/utils.py +62 -0
- django_content_studio-1.0.0b2.post3/content_studio/views.py +181 -0
- django_content_studio-1.0.0b2.post3/content_studio/viewsets.py +100 -0
- django_content_studio-1.0.0b2.post3/content_studio/widgets.py +83 -0
- django_content_studio-1.0.0b2.post3/pyproject.toml +27 -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,86 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: django-content-studio
|
|
3
|
+
Version: 1.0.0b2.post3
|
|
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
|
+
## 🚀 Quick Start
|
|
27
|
+
|
|
28
|
+
### Installation
|
|
29
|
+
|
|
30
|
+
☝️ Django Content Studio depends on Django and Django Rest Framework.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install django-content-studio
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Add to Django Settings
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
# settings.py
|
|
40
|
+
INSTALLED_APPS = [
|
|
41
|
+
'django.contrib.admin',
|
|
42
|
+
'django.contrib.auth',
|
|
43
|
+
'django.contrib.contenttypes',
|
|
44
|
+
'django.contrib.sessions',
|
|
45
|
+
'django.contrib.messages',
|
|
46
|
+
'django.contrib.staticfiles',
|
|
47
|
+
'rest_framework',
|
|
48
|
+
'content_studio', # Add this
|
|
49
|
+
# ... your apps
|
|
50
|
+
]
|
|
51
|
+
```
|
|
52
|
+
### Add URLs
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
# urls.py
|
|
56
|
+
urlpatterns = [
|
|
57
|
+
path("admin/", include("content_studio.urls")),
|
|
58
|
+
# ... your urls
|
|
59
|
+
]
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## 🐛 Issues & Support
|
|
63
|
+
|
|
64
|
+
- 🐛 **Bug Reports**: [GitHub Issues](https://github.com/BitsOfAbstraction/django-content-studio/issues)
|
|
65
|
+
- 💬 **Discussions**: [GitHub Discussions](https://github.com/BitsOfAbstraction/django-content-studio/discussions)
|
|
66
|
+
- 📧 **Email**: leon@devtastic.io
|
|
67
|
+
|
|
68
|
+
## 📄 License
|
|
69
|
+
|
|
70
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
71
|
+
|
|
72
|
+
## 🙏 Acknowledgments
|
|
73
|
+
|
|
74
|
+
- Built with React and Tailwind CSS
|
|
75
|
+
- Inspired by the original Django admin
|
|
76
|
+
- Thanks to all contributors and the Django community
|
|
77
|
+
|
|
78
|
+
## 🔗 Links
|
|
79
|
+
|
|
80
|
+
- [PyPI Package](https://pypi.org/project/django-content-studio/)
|
|
81
|
+
- [GitHub Repository](https://github.com/BitsOfAbstraction/django-content-studio)
|
|
82
|
+
- [Changelog](CHANGELOG.md)
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
Made in Europe 🇪🇺 with 💚 for Django
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
## 🚀 Quick Start
|
|
11
|
+
|
|
12
|
+
### Installation
|
|
13
|
+
|
|
14
|
+
☝️ Django Content Studio depends on Django and Django Rest Framework.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install django-content-studio
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Add to Django Settings
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
# settings.py
|
|
24
|
+
INSTALLED_APPS = [
|
|
25
|
+
'django.contrib.admin',
|
|
26
|
+
'django.contrib.auth',
|
|
27
|
+
'django.contrib.contenttypes',
|
|
28
|
+
'django.contrib.sessions',
|
|
29
|
+
'django.contrib.messages',
|
|
30
|
+
'django.contrib.staticfiles',
|
|
31
|
+
'rest_framework',
|
|
32
|
+
'content_studio', # Add this
|
|
33
|
+
# ... your apps
|
|
34
|
+
]
|
|
35
|
+
```
|
|
36
|
+
### Add URLs
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
# urls.py
|
|
40
|
+
urlpatterns = [
|
|
41
|
+
path("admin/", include("content_studio.urls")),
|
|
42
|
+
# ... your urls
|
|
43
|
+
]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 🐛 Issues & Support
|
|
47
|
+
|
|
48
|
+
- 🐛 **Bug Reports**: [GitHub Issues](https://github.com/BitsOfAbstraction/django-content-studio/issues)
|
|
49
|
+
- 💬 **Discussions**: [GitHub Discussions](https://github.com/BitsOfAbstraction/django-content-studio/discussions)
|
|
50
|
+
- 📧 **Email**: leon@devtastic.io
|
|
51
|
+
|
|
52
|
+
## 📄 License
|
|
53
|
+
|
|
54
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
55
|
+
|
|
56
|
+
## 🙏 Acknowledgments
|
|
57
|
+
|
|
58
|
+
- Built with React and Tailwind CSS
|
|
59
|
+
- Inspired by the original Django admin
|
|
60
|
+
- Thanks to all contributors and the Django community
|
|
61
|
+
|
|
62
|
+
## 🔗 Links
|
|
63
|
+
|
|
64
|
+
- [PyPI Package](https://pypi.org/project/django-content-studio/)
|
|
65
|
+
- [GitHub Repository](https://github.com/BitsOfAbstraction/django-content-studio)
|
|
66
|
+
- [Changelog](CHANGELOG.md)
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
Made in Europe 🇪🇺 with 💚 for Django
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
from typing import Type
|
|
2
|
+
|
|
3
|
+
from django.contrib import admin
|
|
4
|
+
from django.db import models
|
|
5
|
+
from django.db.models import Model
|
|
6
|
+
from rest_framework.request import HttpRequest
|
|
7
|
+
|
|
8
|
+
from . import widgets, formats
|
|
9
|
+
from .form import FormSet, FormSetGroup
|
|
10
|
+
from .login_backends import LoginBackendManager
|
|
11
|
+
from .token_backends import TokenBackendManager
|
|
12
|
+
from .utils import get_related_field_name
|
|
13
|
+
|
|
14
|
+
register = admin.register
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class StackedInline(admin.StackedInline):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TabularInline(admin.TabularInline):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AdminSite(admin.AdminSite):
|
|
26
|
+
"""
|
|
27
|
+
Enhanced admin site for Django Content Studio.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
token_backend = TokenBackendManager()
|
|
31
|
+
|
|
32
|
+
login_backend = LoginBackendManager()
|
|
33
|
+
|
|
34
|
+
dashboard = None
|
|
35
|
+
|
|
36
|
+
model_groups = None
|
|
37
|
+
|
|
38
|
+
default_widget_mapping = {
|
|
39
|
+
models.CharField: widgets.InputWidget,
|
|
40
|
+
models.IntegerField: widgets.InputWidget,
|
|
41
|
+
models.SmallIntegerField: widgets.InputWidget,
|
|
42
|
+
models.BigIntegerField: widgets.InputWidget,
|
|
43
|
+
models.PositiveIntegerField: widgets.InputWidget,
|
|
44
|
+
models.PositiveSmallIntegerField: widgets.InputWidget,
|
|
45
|
+
models.PositiveBigIntegerField: widgets.InputWidget,
|
|
46
|
+
models.FloatField: widgets.InputWidget,
|
|
47
|
+
models.DecimalField: widgets.InputWidget,
|
|
48
|
+
models.SlugField: widgets.SlugWidget,
|
|
49
|
+
models.TextField: widgets.TextAreaWidget,
|
|
50
|
+
models.BooleanField: widgets.CheckboxWidget,
|
|
51
|
+
models.NullBooleanField: widgets.CheckboxWidget,
|
|
52
|
+
models.ForeignKey: widgets.ForeignKeyWidget,
|
|
53
|
+
models.ManyToManyField: widgets.ManyToManyWidget,
|
|
54
|
+
models.OneToOneField: widgets.ForeignKeyWidget,
|
|
55
|
+
models.DateField: widgets.DateWidget,
|
|
56
|
+
models.DateTimeField: widgets.DateTimeWidget,
|
|
57
|
+
models.TimeField: widgets.TimeWidget,
|
|
58
|
+
models.JSONField: widgets.JSONWidget,
|
|
59
|
+
# Common third-party fields
|
|
60
|
+
"AutoSlugField": widgets.SlugWidget,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
default_format_mapping = {
|
|
64
|
+
models.CharField: formats.TextFormat,
|
|
65
|
+
models.IntegerField: formats.NumberFormat,
|
|
66
|
+
models.SmallIntegerField: formats.NumberFormat,
|
|
67
|
+
models.BigIntegerField: formats.NumberFormat,
|
|
68
|
+
models.PositiveIntegerField: formats.NumberFormat,
|
|
69
|
+
models.PositiveSmallIntegerField: formats.NumberFormat,
|
|
70
|
+
models.PositiveBigIntegerField: formats.NumberFormat,
|
|
71
|
+
models.FloatField: formats.NumberFormat,
|
|
72
|
+
models.DecimalField: formats.NumberFormat,
|
|
73
|
+
models.SlugField: formats.TextFormat,
|
|
74
|
+
models.TextField: formats.TextFormat,
|
|
75
|
+
models.BooleanField: formats.BooleanFormat,
|
|
76
|
+
models.NullBooleanField: formats.BooleanFormat,
|
|
77
|
+
models.DateField: formats.DateFormat,
|
|
78
|
+
models.DateTimeField: formats.DateTimeFormat,
|
|
79
|
+
models.TimeField: formats.TimeFormat,
|
|
80
|
+
models.ForeignKey: formats.ForeignKeyFormat,
|
|
81
|
+
models.OneToOneField: formats.ForeignKeyFormat,
|
|
82
|
+
models.JSONField: formats.JSONFormat,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
def setup(self):
|
|
86
|
+
# Add token backend's view set to the
|
|
87
|
+
# Content Studio router.
|
|
88
|
+
self.token_backend.set_up_router()
|
|
89
|
+
# Add login backend's view set to the
|
|
90
|
+
# Content Studio router.
|
|
91
|
+
self.login_backend.set_up_router()
|
|
92
|
+
# Add dashboard's view set to the
|
|
93
|
+
# Content Studio router.
|
|
94
|
+
if self.dashboard:
|
|
95
|
+
self.dashboard.set_up_router()
|
|
96
|
+
|
|
97
|
+
def get_thumbnail(self, obj) -> str:
|
|
98
|
+
"""
|
|
99
|
+
Method for getting and manipulating the image path (or URL).
|
|
100
|
+
By default, this returns the image path as is.
|
|
101
|
+
"""
|
|
102
|
+
return obj.file.url
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
admin_site = AdminSite()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ModelAdmin(admin.ModelAdmin):
|
|
109
|
+
"""
|
|
110
|
+
Enhanced model admin for Django Content Studio and integration with
|
|
111
|
+
Django Content Framework. Although it's relatively backwards compatible,
|
|
112
|
+
some default behavior has been changed.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
# Whether the model is a singleton and should not show
|
|
116
|
+
# the list view.
|
|
117
|
+
is_singleton = False
|
|
118
|
+
|
|
119
|
+
# Override the widget used for certain fields by adding
|
|
120
|
+
# a map of field to widget. Fields that are not included
|
|
121
|
+
# will fall back to their default widget.
|
|
122
|
+
#
|
|
123
|
+
# @example
|
|
124
|
+
# widget_mapping = {'is_published': widgets.SwitchWidget}
|
|
125
|
+
widget_mapping = None
|
|
126
|
+
|
|
127
|
+
# Override the format used for certain fields by adding
|
|
128
|
+
# a map of field to format. Fields that are not included
|
|
129
|
+
# will fall back to their default format.
|
|
130
|
+
#
|
|
131
|
+
# @example
|
|
132
|
+
# format_mapping = {'file_size': widgets.FileSizeWidget}
|
|
133
|
+
format_mapping = None
|
|
134
|
+
|
|
135
|
+
# We set a lower limit than Django's default of 100
|
|
136
|
+
list_per_page = 20
|
|
137
|
+
|
|
138
|
+
# Description shown below model name on list pages
|
|
139
|
+
list_description = ""
|
|
140
|
+
|
|
141
|
+
# Configure the main section in the edit-view.
|
|
142
|
+
edit_main: list[type[FormSetGroup | FormSet | str]] = []
|
|
143
|
+
|
|
144
|
+
# Configure the sidebar in the edit-view.
|
|
145
|
+
edit_sidebar: list[type[FormSet | str]] = []
|
|
146
|
+
|
|
147
|
+
icon = None
|
|
148
|
+
|
|
149
|
+
def save_model(self, request, obj, form, change):
|
|
150
|
+
if hasattr(obj, "edited_by"):
|
|
151
|
+
obj.edited_by = request.user
|
|
152
|
+
super().save_model(request, obj, form, change)
|
|
153
|
+
|
|
154
|
+
def has_add_permission(self, request):
|
|
155
|
+
is_singleton = getattr(self.model, "is_singleton", False)
|
|
156
|
+
|
|
157
|
+
# Don't allow to add more than one singleton object.
|
|
158
|
+
if is_singleton and self.model.objects.get():
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
return super().has_add_permission(request)
|
|
162
|
+
|
|
163
|
+
def has_delete_permission(self, request, obj=None):
|
|
164
|
+
is_singleton = getattr(self.model, "is_singleton", False)
|
|
165
|
+
|
|
166
|
+
if is_singleton:
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
return super().has_delete_permission(request, obj)
|
|
170
|
+
|
|
171
|
+
def render_change_form(self, request, context, *args, **kwargs):
|
|
172
|
+
is_singleton = getattr(self.model, "is_singleton", False)
|
|
173
|
+
|
|
174
|
+
context["show_save_and_add_another"] = not is_singleton
|
|
175
|
+
|
|
176
|
+
return super().render_change_form(request, context, *args, **kwargs)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class AdminSerializer:
|
|
180
|
+
"""
|
|
181
|
+
Class for serializing Django admin classes.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
def __init__(self, admin_class: ModelAdmin):
|
|
185
|
+
self.admin_class = admin_class
|
|
186
|
+
|
|
187
|
+
def serialize(self, request: HttpRequest):
|
|
188
|
+
admin_class = self.admin_class
|
|
189
|
+
format_mapping = getattr(admin_class, "format_mapping", None) or {}
|
|
190
|
+
widget_mapping = getattr(admin_class, "widget_mapping", None) or {}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
"icon": getattr(admin_class, "icon", None),
|
|
194
|
+
"is_singleton": getattr(admin_class, "is_singleton", False),
|
|
195
|
+
"edit": {
|
|
196
|
+
"main": self.serialize_edit_main(request),
|
|
197
|
+
"sidebar": self.serialize_edit_sidebar(request),
|
|
198
|
+
"inlines": [
|
|
199
|
+
{
|
|
200
|
+
"model": inline.model._meta.label_lower,
|
|
201
|
+
"fk_name": get_related_field_name(inline, admin_class.model),
|
|
202
|
+
"list_display": getattr(inline, "list_display", None)
|
|
203
|
+
or ["__str__"],
|
|
204
|
+
}
|
|
205
|
+
for inline in admin_class.inlines
|
|
206
|
+
],
|
|
207
|
+
},
|
|
208
|
+
"list": {
|
|
209
|
+
"per_page": admin_class.list_per_page,
|
|
210
|
+
"description": getattr(admin_class, "list_description", ""),
|
|
211
|
+
"display": admin_class.list_display,
|
|
212
|
+
"search": len(admin_class.search_fields) > 0,
|
|
213
|
+
"filter": admin_class.list_filter,
|
|
214
|
+
"sortable_by": admin_class.sortable_by,
|
|
215
|
+
},
|
|
216
|
+
"widget_mapping": {
|
|
217
|
+
field: widget.serialize() for field, widget in widget_mapping.items()
|
|
218
|
+
},
|
|
219
|
+
"format_mapping": {
|
|
220
|
+
field: format.serialize() for field, format in format_mapping.items()
|
|
221
|
+
},
|
|
222
|
+
"permissions": {
|
|
223
|
+
"add_permission": admin_class.has_add_permission(request),
|
|
224
|
+
"delete_permission": admin_class.has_delete_permission(request),
|
|
225
|
+
"change_permission": admin_class.has_change_permission(request),
|
|
226
|
+
"view_permission": admin_class.has_view_permission(request),
|
|
227
|
+
},
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
def serialize_edit_main(self, request):
|
|
231
|
+
admin_class = self.admin_class
|
|
232
|
+
|
|
233
|
+
return [
|
|
234
|
+
i.serialize()
|
|
235
|
+
for i in self.get_edit_main(
|
|
236
|
+
getattr(admin_class, "edit_main", admin_class.get_fields(request))
|
|
237
|
+
)
|
|
238
|
+
]
|
|
239
|
+
|
|
240
|
+
def serialize_edit_sidebar(self, request):
|
|
241
|
+
admin_class = self.admin_class
|
|
242
|
+
|
|
243
|
+
return [
|
|
244
|
+
i.serialize()
|
|
245
|
+
for i in self.get_edit_sidebar(getattr(admin_class, "edit_sidebar", None))
|
|
246
|
+
]
|
|
247
|
+
|
|
248
|
+
def get_edit_main(self, edit_main):
|
|
249
|
+
"""
|
|
250
|
+
Returns a normalized list of form set groups.
|
|
251
|
+
|
|
252
|
+
Form sets will be wrapped in a form set group. If the edit_main attribute is a list of fields,
|
|
253
|
+
they are wrapped in a form set and a form set group.
|
|
254
|
+
"""
|
|
255
|
+
if not edit_main:
|
|
256
|
+
return []
|
|
257
|
+
if isinstance(edit_main[0], FormSetGroup):
|
|
258
|
+
return edit_main
|
|
259
|
+
if isinstance(edit_main[0], FormSet):
|
|
260
|
+
return [FormSetGroup(formsets=edit_main)]
|
|
261
|
+
|
|
262
|
+
return [FormSetGroup(formsets=[FormSet(fields=edit_main)])]
|
|
263
|
+
|
|
264
|
+
def get_edit_sidebar(self, edit_sidebar):
|
|
265
|
+
"""
|
|
266
|
+
Returns a normalized list of form sets for the edit_sidebar.
|
|
267
|
+
|
|
268
|
+
If the edit_sidebar attribute is a list of fields,
|
|
269
|
+
they are wrapped in a form set.
|
|
270
|
+
"""
|
|
271
|
+
if not edit_sidebar:
|
|
272
|
+
return []
|
|
273
|
+
if isinstance(edit_sidebar[0], FormSet):
|
|
274
|
+
return edit_sidebar
|
|
275
|
+
|
|
276
|
+
return [FormSet(fields=edit_sidebar)]
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class ModelGroup:
|
|
280
|
+
name = None
|
|
281
|
+
label = None
|
|
282
|
+
icon = None
|
|
283
|
+
color = None
|
|
284
|
+
models = None
|
|
285
|
+
|
|
286
|
+
def __init__(
|
|
287
|
+
self,
|
|
288
|
+
name: str,
|
|
289
|
+
label: str = None,
|
|
290
|
+
icon: str = None,
|
|
291
|
+
color: str = None,
|
|
292
|
+
models: list[Type[Model]] = None,
|
|
293
|
+
):
|
|
294
|
+
self.name = name
|
|
295
|
+
self.label = label or name.capitalize()
|
|
296
|
+
self.icon = icon
|
|
297
|
+
self.color = color
|
|
298
|
+
self.models = models or []
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from django.apps import AppConfig
|
|
2
|
+
from django.contrib import admin
|
|
3
|
+
|
|
4
|
+
from . import VERSION
|
|
5
|
+
from .paginators import ContentPagination
|
|
6
|
+
from .settings import cs_settings
|
|
7
|
+
from .utils import is_runserver
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DjangoContentStudioConfig(AppConfig):
|
|
11
|
+
name = "content_studio"
|
|
12
|
+
label = "content_studio"
|
|
13
|
+
initialized = False
|
|
14
|
+
|
|
15
|
+
def ready(self):
|
|
16
|
+
from .utils import log
|
|
17
|
+
|
|
18
|
+
if is_runserver() and not self.initialized:
|
|
19
|
+
self.initialized = True
|
|
20
|
+
|
|
21
|
+
log("\n")
|
|
22
|
+
log("----------------------------------------")
|
|
23
|
+
log("Django Content Studio")
|
|
24
|
+
log(f"Version {VERSION}")
|
|
25
|
+
log("----------------------------------------")
|
|
26
|
+
log(":rocket:", "Starting Django Content Studio")
|
|
27
|
+
log(":mag:", "Discovering admin models...")
|
|
28
|
+
registered_models = len(admin.site._registry)
|
|
29
|
+
log(
|
|
30
|
+
":white_check_mark:",
|
|
31
|
+
f"[green]Found {registered_models} admin models[/green]",
|
|
32
|
+
)
|
|
33
|
+
# Set up admin site routes
|
|
34
|
+
admin_site = cs_settings.ADMIN_SITE
|
|
35
|
+
admin_site.setup()
|
|
36
|
+
|
|
37
|
+
# Set up content CRUD APIs
|
|
38
|
+
self._create_crud_api()
|
|
39
|
+
|
|
40
|
+
log("\n")
|
|
41
|
+
|
|
42
|
+
def _create_crud_api(self):
|
|
43
|
+
from .utils import log
|
|
44
|
+
|
|
45
|
+
for model, admin_model in admin.site._registry.items():
|
|
46
|
+
self._create_view_set(model, admin_model)
|
|
47
|
+
|
|
48
|
+
for inline in admin_model.inlines:
|
|
49
|
+
self._create_view_set(
|
|
50
|
+
parent=model, model=inline.model, admin_model=inline
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
log(
|
|
54
|
+
":white_check_mark:",
|
|
55
|
+
f"[green]Created CRUD API[/green]",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def _create_view_set(self, model, admin_model, parent=None):
|
|
59
|
+
from .viewsets import BaseModelViewSet
|
|
60
|
+
from .router import content_studio_router
|
|
61
|
+
from .serializers import ContentSerializer
|
|
62
|
+
|
|
63
|
+
class Pagination(ContentPagination):
|
|
64
|
+
page_size = getattr(admin_model, "list_per_page", 10)
|
|
65
|
+
|
|
66
|
+
class ViewSet(BaseModelViewSet):
|
|
67
|
+
_model = model
|
|
68
|
+
_admin_model = admin_model
|
|
69
|
+
is_singleton = getattr(admin_model, "is_singleton", False)
|
|
70
|
+
pagination_class = Pagination
|
|
71
|
+
queryset = _model.objects.all()
|
|
72
|
+
search_fields = list(getattr(_admin_model, "search_fields", []))
|
|
73
|
+
|
|
74
|
+
def get_serializer_class(self):
|
|
75
|
+
# For list views we include the specified list_display fields.
|
|
76
|
+
if self.action == "list" and not self.is_singleton:
|
|
77
|
+
available_fields = [
|
|
78
|
+
"id",
|
|
79
|
+
"__str__",
|
|
80
|
+
] + list(getattr(self._admin_model, "list_display", []))
|
|
81
|
+
# In all other cases we include all fields.
|
|
82
|
+
else:
|
|
83
|
+
available_fields = "__all__"
|
|
84
|
+
|
|
85
|
+
class Serializer(ContentSerializer):
|
|
86
|
+
|
|
87
|
+
class Meta:
|
|
88
|
+
model = self._model
|
|
89
|
+
fields = available_fields
|
|
90
|
+
|
|
91
|
+
return Serializer
|
|
92
|
+
|
|
93
|
+
if parent:
|
|
94
|
+
prefix = f"api/inlines/{parent._meta.label_lower}/{model._meta.label_lower}"
|
|
95
|
+
basename = f"content_studio_api-{parent._meta.label_lower}-{model._meta.label_lower}"
|
|
96
|
+
else:
|
|
97
|
+
prefix = f"api/content/{model._meta.label_lower}"
|
|
98
|
+
basename = f"content_studio_api-{model._meta.label_lower}"
|
|
99
|
+
|
|
100
|
+
content_studio_router.register(prefix, ViewSet, basename)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from rest_framework.decorators import action
|
|
2
|
+
from rest_framework.exceptions import NotFound
|
|
3
|
+
from rest_framework.parsers import JSONParser
|
|
4
|
+
from rest_framework.renderers import JSONRenderer
|
|
5
|
+
from rest_framework.response import Response
|
|
6
|
+
from rest_framework.viewsets import ViewSet
|
|
7
|
+
|
|
8
|
+
from content_studio.settings import cs_settings
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Dashboard:
|
|
12
|
+
"""
|
|
13
|
+
The Dashboard class is used to define the structure of the dashboard
|
|
14
|
+
in Django Content Studio.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
widgets = None
|
|
18
|
+
|
|
19
|
+
def __init__(self, **kwargs):
|
|
20
|
+
self.widgets = kwargs.get("widgets", [])
|
|
21
|
+
|
|
22
|
+
def set_up_router(self):
|
|
23
|
+
from content_studio.router import content_studio_router
|
|
24
|
+
|
|
25
|
+
content_studio_router.register(
|
|
26
|
+
"api/dashboard",
|
|
27
|
+
DashboardViewSet,
|
|
28
|
+
basename="content_studio_dashboard",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def serialize(self):
|
|
32
|
+
return {
|
|
33
|
+
"widgets": [
|
|
34
|
+
{"name": w.__class__.__name__, "col_span": getattr(w, "col_span", 1)}
|
|
35
|
+
for w in self.widgets
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class DashboardViewSet(ViewSet):
|
|
41
|
+
parser_classes = [JSONParser]
|
|
42
|
+
renderer_classes = [JSONRenderer]
|
|
43
|
+
|
|
44
|
+
def __init__(self, *args, **kwargs):
|
|
45
|
+
super(ViewSet, self).__init__()
|
|
46
|
+
admin_site = cs_settings.ADMIN_SITE
|
|
47
|
+
|
|
48
|
+
self.dashboard = admin_site.dashboard
|
|
49
|
+
self.authentication_classes = [
|
|
50
|
+
admin_site.token_backend.active_backend.authentication_class
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
@action(detail=False, url_path="(?P<name>[^/.]+)")
|
|
54
|
+
def get(self, request, name=None):
|
|
55
|
+
widget = None
|
|
56
|
+
|
|
57
|
+
for w in self.dashboard.widgets:
|
|
58
|
+
if name == w.__class__.__name__.lower():
|
|
59
|
+
widget = w
|
|
60
|
+
|
|
61
|
+
if not widget:
|
|
62
|
+
raise NotFound()
|
|
63
|
+
|
|
64
|
+
return Response(data=widget.get_data(request))
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from django.contrib.admin.models import LogEntry
|
|
2
|
+
from rest_framework import serializers
|
|
3
|
+
|
|
4
|
+
from content_studio.serializers import ContentSerializer
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LogEntrySerializer(ContentSerializer):
|
|
8
|
+
object_model = serializers.SerializerMethodField()
|
|
9
|
+
|
|
10
|
+
class Meta:
|
|
11
|
+
model = LogEntry
|
|
12
|
+
fields = [
|
|
13
|
+
"id",
|
|
14
|
+
"action_flag",
|
|
15
|
+
"action_time",
|
|
16
|
+
"user",
|
|
17
|
+
"object_id",
|
|
18
|
+
"object_repr",
|
|
19
|
+
"object_model",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
def get_object_model(self, obj):
|
|
23
|
+
return f"{obj.content_type.app_label}.{obj.content_type.model}"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ActivityLogWidget:
|
|
27
|
+
"""
|
|
28
|
+
Widget for showing activity logs.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
col_span = 2
|
|
32
|
+
|
|
33
|
+
def get_data(self, request):
|
|
34
|
+
return LogEntrySerializer(LogEntry.objects.all()[0:5], many=True).data
|