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.
Files changed (37) hide show
  1. django_content_studio-1.0.0a1/LICENSE +21 -0
  2. django_content_studio-1.0.0a1/PKG-INFO +88 -0
  3. django_content_studio-1.0.0a1/README.md +72 -0
  4. django_content_studio-1.0.0a1/content_studio/__init__.py +7 -0
  5. django_content_studio-1.0.0a1/content_studio/admin.py +154 -0
  6. django_content_studio-1.0.0a1/content_studio/apps.py +59 -0
  7. django_content_studio-1.0.0a1/content_studio/dashboard.py +7 -0
  8. django_content_studio-1.0.0a1/content_studio/form.py +76 -0
  9. django_content_studio-1.0.0a1/content_studio/login_backends/__init__.py +21 -0
  10. django_content_studio-1.0.0a1/content_studio/login_backends/username_password.py +79 -0
  11. django_content_studio-1.0.0a1/content_studio/models.py +86 -0
  12. django_content_studio-1.0.0a1/content_studio/router.py +14 -0
  13. django_content_studio-1.0.0a1/content_studio/serializers.py +16 -0
  14. django_content_studio-1.0.0a1/content_studio/settings.py +146 -0
  15. django_content_studio-1.0.0a1/content_studio/static/content_studio/assets/browser-ponyfill-Ct7s-5jI.js +2 -0
  16. django_content_studio-1.0.0a1/content_studio/static/content_studio/assets/index.css +1 -0
  17. django_content_studio-1.0.0a1/content_studio/static/content_studio/assets/index.js +106 -0
  18. django_content_studio-1.0.0a1/content_studio/static/content_studio/assets/inter-cyrillic-ext-wght-normal.woff2 +0 -0
  19. django_content_studio-1.0.0a1/content_studio/static/content_studio/assets/inter-cyrillic-wght-normal.woff2 +0 -0
  20. django_content_studio-1.0.0a1/content_studio/static/content_studio/assets/inter-greek-ext-wght-normal.woff2 +0 -0
  21. django_content_studio-1.0.0a1/content_studio/static/content_studio/assets/inter-greek-wght-normal.woff2 +0 -0
  22. django_content_studio-1.0.0a1/content_studio/static/content_studio/assets/inter-latin-ext-wght-normal.woff2 +0 -0
  23. django_content_studio-1.0.0a1/content_studio/static/content_studio/assets/inter-latin-wght-normal.woff2 +0 -0
  24. django_content_studio-1.0.0a1/content_studio/static/content_studio/assets/inter-vietnamese-wght-normal.woff2 +0 -0
  25. django_content_studio-1.0.0a1/content_studio/static/content_studio/index.html +14 -0
  26. django_content_studio-1.0.0a1/content_studio/static/content_studio/locales/en/translation.json +11 -0
  27. django_content_studio-1.0.0a1/content_studio/static/content_studio/locales/nl/translation.json +11 -0
  28. django_content_studio-1.0.0a1/content_studio/static/content_studio/vite.svg +1 -0
  29. django_content_studio-1.0.0a1/content_studio/templates/content_studio/index.html +20 -0
  30. django_content_studio-1.0.0a1/content_studio/token_backends/__init__.py +39 -0
  31. django_content_studio-1.0.0a1/content_studio/token_backends/jwt.py +61 -0
  32. django_content_studio-1.0.0a1/content_studio/urls.py +12 -0
  33. django_content_studio-1.0.0a1/content_studio/utils.py +30 -0
  34. django_content_studio-1.0.0a1/content_studio/views.py +94 -0
  35. django_content_studio-1.0.0a1/content_studio/viewsets.py +128 -0
  36. django_content_studio-1.0.0a1/content_studio/widgets.py +30 -0
  37. 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
+ [![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
+ > 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
+ [![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
+ > 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,7 @@
1
+ __title__ = "Django Content Studio"
2
+ __version__ = "1.0.0-canary.1"
3
+ __author__ = "Leon van der Grient"
4
+ __license__ = "BSD 3-Clause"
5
+
6
+ # Version synonym
7
+ VERSION = __version__
@@ -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,7 @@
1
+ class Dashboard:
2
+ """
3
+ The Dashboard class is used to define the structure of the dashboard
4
+ in Django Content Studio.
5
+ """
6
+
7
+ pass
@@ -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)