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.
Files changed (49) hide show
  1. django_content_studio-1.0.0b2.post3/LICENSE +21 -0
  2. django_content_studio-1.0.0b2.post3/PKG-INFO +86 -0
  3. django_content_studio-1.0.0b2.post3/README.md +70 -0
  4. django_content_studio-1.0.0b2.post3/content_studio/__init__.py +7 -0
  5. django_content_studio-1.0.0b2.post3/content_studio/admin.py +298 -0
  6. django_content_studio-1.0.0b2.post3/content_studio/apps.py +100 -0
  7. django_content_studio-1.0.0b2.post3/content_studio/dashboard/__init__.py +64 -0
  8. django_content_studio-1.0.0b2.post3/content_studio/dashboard/activity_log.py +34 -0
  9. django_content_studio-1.0.0b2.post3/content_studio/filters.py +124 -0
  10. django_content_studio-1.0.0b2.post3/content_studio/form.py +97 -0
  11. django_content_studio-1.0.0b2.post3/content_studio/formats.py +59 -0
  12. django_content_studio-1.0.0b2.post3/content_studio/login_backends/__init__.py +21 -0
  13. django_content_studio-1.0.0b2.post3/content_studio/login_backends/username_password.py +82 -0
  14. django_content_studio-1.0.0b2.post3/content_studio/media_library/serializers.py +25 -0
  15. django_content_studio-1.0.0b2.post3/content_studio/media_library/viewsets.py +132 -0
  16. django_content_studio-1.0.0b2.post3/content_studio/models.py +73 -0
  17. django_content_studio-1.0.0b2.post3/content_studio/paginators.py +20 -0
  18. django_content_studio-1.0.0b2.post3/content_studio/router.py +14 -0
  19. django_content_studio-1.0.0b2.post3/content_studio/serializers.py +118 -0
  20. django_content_studio-1.0.0b2.post3/content_studio/settings.py +152 -0
  21. django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/assets/browser-ponyfill-Ct7s-5jI.js +2 -0
  22. django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/assets/index.css +1 -0
  23. django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/assets/index.js +228 -0
  24. django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/assets/inter-cyrillic-ext-wght-normal.woff2 +0 -0
  25. django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/assets/inter-cyrillic-wght-normal.woff2 +0 -0
  26. django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/assets/inter-greek-ext-wght-normal.woff2 +0 -0
  27. django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/assets/inter-greek-wght-normal.woff2 +0 -0
  28. django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/assets/inter-latin-ext-wght-normal.woff2 +0 -0
  29. django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/assets/inter-latin-wght-normal.woff2 +0 -0
  30. django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/assets/inter-vietnamese-wght-normal.woff2 +0 -0
  31. django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/icons/pi/Phosphor-Bold.svg +3057 -0
  32. django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/icons/pi/Phosphor-Bold.ttf +0 -0
  33. django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/icons/pi/Phosphor-Bold.woff +0 -0
  34. django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/icons/pi/Phosphor-Bold.woff2 +0 -0
  35. django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/icons/pi/style.css +4627 -0
  36. django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/img/media_placeholder.svg +1 -0
  37. django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/index.html +19 -0
  38. django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/locales/en/translation.json +82 -0
  39. django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/locales/nl/translation.json +87 -0
  40. django_content_studio-1.0.0b2.post3/content_studio/static/content_studio/vite.svg +1 -0
  41. django_content_studio-1.0.0b2.post3/content_studio/templates/content_studio/index.html +19 -0
  42. django_content_studio-1.0.0b2.post3/content_studio/token_backends/__init__.py +39 -0
  43. django_content_studio-1.0.0b2.post3/content_studio/token_backends/jwt.py +56 -0
  44. django_content_studio-1.0.0b2.post3/content_studio/urls.py +21 -0
  45. django_content_studio-1.0.0b2.post3/content_studio/utils.py +62 -0
  46. django_content_studio-1.0.0b2.post3/content_studio/views.py +181 -0
  47. django_content_studio-1.0.0b2.post3/content_studio/viewsets.py +100 -0
  48. django_content_studio-1.0.0b2.post3/content_studio/widgets.py +83 -0
  49. 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
+ [![PyPI version](https://badge.fury.io/py/django-content-studio.svg)](https://badge.fury.io/py/django-content-studio)
20
+ [![Python versions](https://img.shields.io/pypi/pyversions/django-content-studio.svg)](https://pypi.org/project/django-content-studio/)
21
+ [![Django versions](https://img.shields.io/badge/django-5.0%2B-blue.svg)](https://www.djangoproject.com/)
22
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](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
+ [![PyPI version](https://badge.fury.io/py/django-content-studio.svg)](https://badge.fury.io/py/django-content-studio)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/django-content-studio.svg)](https://pypi.org/project/django-content-studio/)
5
+ [![Django versions](https://img.shields.io/badge/django-5.0%2B-blue.svg)](https://www.djangoproject.com/)
6
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](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,7 @@
1
+ __title__ = "Django Content Studio"
2
+ __version__ = "1.0.0-beta.2"
3
+ __author__ = "Leon van der Grient"
4
+ __license__ = "MIT"
5
+
6
+ # Version synonym
7
+ VERSION = __version__
@@ -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