django-content-studio 1.0.0b5__py3-none-any.whl

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. content_studio/__init__.py +7 -0
  2. content_studio/admin.py +307 -0
  3. content_studio/apps.py +100 -0
  4. content_studio/dashboard/__init__.py +64 -0
  5. content_studio/dashboard/activity_log.py +35 -0
  6. content_studio/filters.py +124 -0
  7. content_studio/form.py +178 -0
  8. content_studio/formats.py +59 -0
  9. content_studio/login_backends/__init__.py +21 -0
  10. content_studio/login_backends/username_password.py +82 -0
  11. content_studio/media_library/serializers.py +25 -0
  12. content_studio/media_library/viewsets.py +132 -0
  13. content_studio/models.py +73 -0
  14. content_studio/paginators.py +20 -0
  15. content_studio/router.py +14 -0
  16. content_studio/serializers.py +118 -0
  17. content_studio/settings.py +152 -0
  18. content_studio/static/content_studio/assets/browser-ponyfill-Ct7s-5jI.js +2 -0
  19. content_studio/static/content_studio/assets/browser-ponyfill-TyWUZ1Oq.js +2 -0
  20. content_studio/static/content_studio/assets/index.css +1 -0
  21. content_studio/static/content_studio/assets/index.js +249 -0
  22. content_studio/static/content_studio/assets/inter-cyrillic-ext-wght-normal.woff2 +0 -0
  23. content_studio/static/content_studio/assets/inter-cyrillic-wght-normal.woff2 +0 -0
  24. content_studio/static/content_studio/assets/inter-greek-ext-wght-normal.woff2 +0 -0
  25. content_studio/static/content_studio/assets/inter-greek-wght-normal.woff2 +0 -0
  26. content_studio/static/content_studio/assets/inter-latin-ext-wght-normal.woff2 +0 -0
  27. content_studio/static/content_studio/assets/inter-latin-wght-normal.woff2 +0 -0
  28. content_studio/static/content_studio/assets/inter-vietnamese-wght-normal.woff2 +0 -0
  29. content_studio/static/content_studio/icons/pi/Phosphor-Bold.svg +3057 -0
  30. content_studio/static/content_studio/icons/pi/Phosphor-Bold.ttf +0 -0
  31. content_studio/static/content_studio/icons/pi/Phosphor-Bold.woff +0 -0
  32. content_studio/static/content_studio/icons/pi/Phosphor-Bold.woff2 +0 -0
  33. content_studio/static/content_studio/icons/pi/style.css +4627 -0
  34. content_studio/static/content_studio/img/media_placeholder.svg +1 -0
  35. content_studio/static/content_studio/locales/en/translation.json +85 -0
  36. content_studio/static/content_studio/locales/nl/translation.json +90 -0
  37. content_studio/static/content_studio/vite.svg +1 -0
  38. content_studio/templates/content_studio/index.html +24 -0
  39. content_studio/token_backends/__init__.py +39 -0
  40. content_studio/token_backends/jwt.py +56 -0
  41. content_studio/urls.py +21 -0
  42. content_studio/utils.py +62 -0
  43. content_studio/views.py +181 -0
  44. content_studio/viewsets.py +114 -0
  45. content_studio/widgets.py +83 -0
  46. django_content_studio-1.0.0b5.dist-info/LICENSE +21 -0
  47. django_content_studio-1.0.0b5.dist-info/METADATA +86 -0
  48. django_content_studio-1.0.0b5.dist-info/RECORD +49 -0
  49. django_content_studio-1.0.0b5.dist-info/WHEEL +4 -0
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" fill="none"><rect width="1200" height="1200" fill="#EAEAEA" rx="3"/><g opacity=".5"><g opacity=".5"><path fill="#FAFAFA" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/></g><path stroke="url(#a)" stroke-width="2.418" d="M0-1.209h553.581" transform="scale(1 -1) rotate(45 1163.11 91.165)"/><path stroke="url(#b)" stroke-width="2.418" d="M404.846 598.671h391.726"/><path stroke="url(#c)" stroke-width="2.418" d="M599.5 795.742V404.017"/><path stroke="url(#d)" stroke-width="2.418" d="m795.717 796.597-391.441-391.44"/><path fill="#fff" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/><g clip-path="url(#e)"><path fill="#666" fill-rule="evenodd" d="M616.426 586.58h-31.434v16.176l3.553-3.554.531-.531h9.068l.074-.074 8.463-8.463h2.565l7.18 7.181V586.58Zm-15.715 14.654 3.698 3.699 1.283 1.282-2.565 2.565-1.282-1.283-5.2-5.199h-6.066l-5.514 5.514-.073.073v2.876a2.418 2.418 0 0 0 2.418 2.418h26.598a2.418 2.418 0 0 0 2.418-2.418v-8.317l-8.463-8.463-7.181 7.181-.071.072Zm-19.347 5.442v4.085a6.045 6.045 0 0 0 6.046 6.045h26.598a6.044 6.044 0 0 0 6.045-6.045v-7.108l1.356-1.355-1.282-1.283-.074-.073v-17.989h-38.689v23.43l-.146.146.146.147Z" clip-rule="evenodd"/></g><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/></g><defs><linearGradient id="a" x1="554.061" x2="-.48" y1=".083" y2=".087" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="b" x1="796.912" x2="404.507" y1="599.963" y2="599.965" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="c" x1="600.792" x2="600.794" y1="403.677" y2="796.082" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="d" x1="404.85" x2="796.972" y1="403.903" y2="796.02" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><clipPath id="e"><path fill="#fff" d="M581.364 580.535h38.689v38.689h-38.689z"/></clipPath></defs></svg>
@@ -0,0 +1,85 @@
1
+ {
2
+ "common": {
3
+ "search": "Search",
4
+ "save": "Save",
5
+ "create": "Create",
6
+ "cancel": "Cancel",
7
+ "general": "General",
8
+ "delete": "Delete",
9
+ "deleting": "Deleting...",
10
+ "deleted": "Deleted",
11
+ "edit": "Edit",
12
+ "continue": "Continue",
13
+ "delete_confirm_title": "Are you sure?",
14
+ "delete_confirm_description": "This action cannot be undone. Are you sure you want to delete?"
15
+ },
16
+ "app": {
17
+ "copied_to_clipboard": "Copied to clipboard!"
18
+ },
19
+ "login": {
20
+ "title": "Welcome back",
21
+ "subtitle": "Sign in to your account",
22
+ "submit": "Sign in",
23
+ "email_placeholder": "Enter your email",
24
+ "username_placeholder": "Enter your username",
25
+ "password_placeholder": "Enter your password",
26
+ "remember_me": "Remember me",
27
+ "forgot_password": "Forgot password?",
28
+ "no_account": "Don't have an account? Contact your administrator"
29
+ },
30
+ "main_menu": {
31
+ "dashboard": "Dashboard",
32
+ "media_library": "Media library",
33
+ "settings": "Settings",
34
+ "log_out": "Log out"
35
+ },
36
+ "editor": {
37
+ "title_create": "Create {{modelName}}",
38
+ "title_edit": "Edit {{modelName}}",
39
+ "last_edited": "Last edited",
40
+ "unsaved_alert": "There are unsaved changes. Are you sure you want to leave?"
41
+ },
42
+ "media-library": {
43
+ "title": "Media library",
44
+ "description": "Manage your media files here.",
45
+ "new_folder": "New folder",
46
+ "file_types": {
47
+ "file": "File",
48
+ "image": "Image",
49
+ "video": "Video",
50
+ "audio": "Audio"
51
+ }
52
+ },
53
+ "widgets": {
54
+ "rich_text_widget": {
55
+ "heading1": "Heading 1",
56
+ "heading2": "Heading 2",
57
+ "heading3": "Heading 3",
58
+ "heading4": "Heading 4",
59
+ "heading5": "Heading 5",
60
+ "heading6": "Heading 6",
61
+ "paragraph": "Paragraph",
62
+ "blockquote": "Blockquote",
63
+ "codeBlock": "Code block",
64
+ "set_link": "Add link",
65
+ "update_link": "Edit link",
66
+ "remove_link": "Remove link"
67
+ },
68
+ "media_widget": {
69
+ "select_media": "Select media",
70
+ "dialog_title": "Select media",
71
+ "upload_label": "Upload media"
72
+ },
73
+ "date_picker": {
74
+ "placeholder": "Select a date"
75
+ }
76
+ },
77
+ "dashboard": {
78
+ "widgets": {
79
+ "activity_log": {
80
+ "title": "Activity log",
81
+ "subtitle": "Latest updates from your team"
82
+ }
83
+ }
84
+ }
85
+ }
@@ -0,0 +1,90 @@
1
+ {
2
+ "common": {
3
+ "search": "Zoeken",
4
+ "save": "Opslaan",
5
+ "create": "Maak aan",
6
+ "cancel": "Annuleren",
7
+ "general": "Algemeen",
8
+ "delete": "Verwijderen",
9
+ "deleting": "Verwijderen...",
10
+ "deleted": "Verwijderd",
11
+ "edit": "Bewerken",
12
+ "continue": "Doorgaan",
13
+ "delete_confirm_title": "Weet je het zeker?",
14
+ "delete_confirm_description": "Deze actie kan niet ongedaan gemaakt worden. Weet je zeker dat je dit wilt verwijderen?"
15
+ },
16
+ "app": {
17
+ "copied_to_clipboard": "Gekopieerd naar klembord"
18
+ },
19
+ "login": {
20
+ "title": "Welkom terug",
21
+ "subtitle": "Log in op je account",
22
+ "submit": "Inloggen",
23
+ "email_placeholder": "Voer je e-mailadres in",
24
+ "username_placeholder": "Voer je gebruikersnaam in",
25
+ "password_placeholder": "Voer je wachtwoord in",
26
+ "remember_me": "Onthoud mij",
27
+ "forgot_password": "Wachtwoord vergeten?",
28
+ "no_account": "Geen account? Neem contact op met je administrator."
29
+ },
30
+ "main_menu": {
31
+ "dashboard": "Dashboard",
32
+ "media_library": "Mediabibliotheek",
33
+ "settings": "Instellingen",
34
+ "log_out": "Uitloggen"
35
+ },
36
+ "editor": {
37
+ "title_create": "Maak {{modelName}} aan",
38
+ "title_edit": "Bewerk {{modelName}}",
39
+ "last_edited": "Laatst bewerkt",
40
+ "unsaved_alert": "Je hebt niet-opgeslagen wijzigen. Weet je zeker dat je weg wilt gaan?"
41
+ },
42
+ "media-library": {
43
+ "title": "Mediabibliotheek",
44
+ "description": "Beheer je media-bestanden hier.",
45
+ "new_folder": "Nieuwe map",
46
+ "file_types": {
47
+ "file": "Bestand",
48
+ "image": "Afbeelding",
49
+ "video": "Video",
50
+ "audio": "Audio"
51
+ }
52
+ },
53
+ "widgets": {
54
+ "rich_text_widget": {
55
+ "heading1": "Kop 1",
56
+ "heading2": "Kop 2",
57
+ "heading3": "Kop 3",
58
+ "heading4": "Kop 4",
59
+ "heading5": "Kop 5",
60
+ "heading6": "Kop 6",
61
+ "paragraph": "Paragraaf",
62
+ "blockquote": "Citaat",
63
+ "codeBlock": "Codeblok",
64
+ "set_link": "Voeg link toe",
65
+ "update_link": "Bewerk link",
66
+ "remove_link": "Verwijder link"
67
+ },
68
+ "media_widget": {
69
+ "select_media": "Selecteer media",
70
+ "dialog_title": "Selecteer media",
71
+ "upload_label": "Upload media"
72
+ },
73
+ "date_picker": {
74
+ "placeholder": "Selecteer een datum"
75
+ }
76
+ },
77
+ "dashboard": {
78
+ "widgets": {
79
+ "activity_log": {
80
+ "title": "Activiteitenlogboek",
81
+ "subtitle": "Recente updates van je team",
82
+ "action_flags": {
83
+ "1": "voegde toe",
84
+ "2": "bewerkte",
85
+ "3": "verwijderde"
86
+ }
87
+ }
88
+ }
89
+ }
90
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
@@ -0,0 +1,24 @@
1
+ {% load static %}
2
+ <!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <link rel="icon" href={% static 'content_studio/icon.png' %}>
8
+ <title>Django Content Studio</title>
9
+ <script>
10
+ window.DCS_STATIC_PREFIX = "{% static '' %}content_studio/";
11
+ window.DCS_BASENAME = "{% url 'content_studio_web' %}";
12
+ </script>
13
+ <script type="module" crossorigin src="{% static 'content_studio/assets/index.js' %}"></script>
14
+ <link rel="stylesheet" crossorigin href="{% static 'content_studio/assets/index.css' %}">
15
+ <link rel="stylesheet" crossorigin href="{% static 'content_studio/icons/pi/style.css' %}">
16
+
17
+ <link rel="preconnect" href="https://fonts.googleapis.com">
18
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
19
+ <link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&family=DM+Serif+Text:ital@0;1&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
20
+ </head>
21
+ <body>
22
+ <div id="root"></div>
23
+ </body>
24
+ </html>
@@ -0,0 +1,39 @@
1
+ from django.core.exceptions import ImproperlyConfigured
2
+
3
+ from .jwt import SimpleJwtBackend
4
+ from ..router import content_studio_router
5
+
6
+
7
+ class TokenBackendManager:
8
+ """
9
+ Manages different token authentication backends for use by
10
+ Content Studio.
11
+
12
+ While login backends are used to identify a user,
13
+ token backends determine are used to authenticate
14
+ communication between Content Studio and the admin API.
15
+ """
16
+
17
+ available_backends = [SimpleJwtBackend]
18
+ _active_backend = None
19
+
20
+ @property
21
+ def active_backend(self):
22
+ if self._active_backend:
23
+ return self._active_backend
24
+
25
+ for backend in self.available_backends:
26
+ if backend.is_available:
27
+ self._active_backend = backend
28
+ return backend
29
+
30
+ raise ImproperlyConfigured(
31
+ "You need to install at least one of the support authentication backends."
32
+ )
33
+
34
+ def set_up_router(self):
35
+ content_studio_router.register(
36
+ f"api/tokens/{self.active_backend.__name__.lower().replace('backend', '')}",
37
+ self.active_backend.view_set,
38
+ basename="content_studio_token_backend",
39
+ )
@@ -0,0 +1,56 @@
1
+ from rest_framework.decorators import action
2
+ from rest_framework.response import Response
3
+ from rest_framework.viewsets import ViewSet
4
+ from rest_framework_simplejwt.authentication import JWTAuthentication
5
+ from rest_framework_simplejwt.settings import api_settings as simplejwt_settings
6
+ from rest_framework_simplejwt.tokens import RefreshToken
7
+ from rest_framework_simplejwt.views import TokenRefreshView
8
+
9
+
10
+ class SimpleJwtViewSet(ViewSet):
11
+ @action(
12
+ detail=False, methods=["post"], permission_classes=[], authentication_classes=[]
13
+ )
14
+ def refresh(self, request):
15
+
16
+ view_instance = TokenRefreshView()
17
+ view_instance.request = request
18
+ view_instance.format_kwarg = None
19
+ return view_instance.post(request)
20
+
21
+
22
+ class SimpleJwtBackend:
23
+ name = "Simple JWT"
24
+ authentication_class = JWTAuthentication
25
+ view_set = SimpleJwtViewSet
26
+
27
+ @classmethod
28
+ def get_info(cls):
29
+
30
+ return {
31
+ "type": cls.__name__,
32
+ "config": {
33
+ "ACCESS_TOKEN_LIFETIME": simplejwt_settings.ACCESS_TOKEN_LIFETIME.total_seconds(),
34
+ },
35
+ }
36
+
37
+ @property
38
+ def is_available(self) -> bool:
39
+ try:
40
+ import rest_framework_simplejwt
41
+
42
+ return True
43
+ except ImportError:
44
+ return False
45
+
46
+ @classmethod
47
+ def get_response_for_user(cls, user):
48
+
49
+ refresh = RefreshToken.for_user(user)
50
+
51
+ return Response(
52
+ {
53
+ "refresh": str(refresh),
54
+ "access": str(refresh.access_token),
55
+ }
56
+ )
content_studio/urls.py ADDED
@@ -0,0 +1,21 @@
1
+ from django.urls import re_path
2
+
3
+ from .media_library.viewsets import MediaLibraryViewSet, MediaFolderViewSet
4
+ from .router import content_studio_router
5
+ from .views import ContentStudioWebAppView, AdminApiViewSet
6
+
7
+ content_studio_router.register("api", AdminApiViewSet, "content_studio_admin")
8
+ content_studio_router.register(
9
+ "api/media-library/items", MediaLibraryViewSet, "content_studio_media_library_items"
10
+ )
11
+ content_studio_router.register(
12
+ "api/media-library/folders",
13
+ MediaFolderViewSet,
14
+ "content_studio_media_library_folders",
15
+ )
16
+
17
+ urlpatterns = [
18
+ re_path(
19
+ "^(?!api).*$", ContentStudioWebAppView.as_view(), name="content_studio_web"
20
+ ),
21
+ ] + content_studio_router.urls
@@ -0,0 +1,62 @@
1
+ import json
2
+ import sys
3
+
4
+ from rich.console import Console
5
+
6
+ console = Console()
7
+
8
+
9
+ def log(*args, **kwargs):
10
+ console.print(*args, **kwargs)
11
+
12
+
13
+ def flatten(xss):
14
+ return [x for xs in xss for x in xs]
15
+
16
+
17
+ def is_runserver():
18
+ """
19
+ Checks if the Django application is started as a server.
20
+ We'll also assume it started if manage.py is not used (e.g. when Django is started using wsgi/asgi).
21
+ The main purpose of this check is to not run certain code on other management commands such
22
+ as `migrate`.
23
+ """
24
+ is_manage_cmd = sys.argv[0].endswith("/manage.py")
25
+
26
+ return not is_manage_cmd or sys.argv[1] == "runserver"
27
+
28
+
29
+ def is_jsonable(x):
30
+ try:
31
+ json.dumps(x)
32
+ return True
33
+ except (TypeError, OverflowError):
34
+ return False
35
+
36
+
37
+ def get_related_field_name(inline, parent_model):
38
+ """
39
+ Get the name of the foreign key field in the inline model.
40
+ """
41
+ if inline.fk_name:
42
+ return inline.fk_name
43
+
44
+ # Let Django figure it out
45
+
46
+ opts = inline.model._meta
47
+
48
+ # Find all foreign keys pointing to parent model
49
+ fks = [
50
+ f
51
+ for f in opts.get_fields()
52
+ if f.many_to_one and f.remote_field.model == parent_model
53
+ ]
54
+
55
+ if len(fks) == 1:
56
+ return fks[0].name
57
+ elif len(fks) == 0:
58
+ raise ValueError(
59
+ f"No foreign key found in {inline.model} pointing to {parent_model}"
60
+ )
61
+ else:
62
+ raise ValueError(f"Multiple foreign keys found. Specify fk_name on the inline.")
@@ -0,0 +1,181 @@
1
+ from django.conf import settings
2
+ from django.contrib import admin
3
+ from django.urls import reverse, NoReverseMatch
4
+ from django.utils.translation import gettext_lazy as _
5
+ from django.views.generic import TemplateView
6
+ from rest_framework.decorators import action
7
+ from rest_framework.permissions import IsAdminUser, AllowAny
8
+ from rest_framework.renderers import JSONRenderer
9
+ from rest_framework.response import Response
10
+ from rest_framework.viewsets import ViewSet
11
+
12
+ from . import __version__
13
+ from .admin import AdminSerializer, ModelGroup
14
+ from .models import ModelSerializer
15
+ from .serializers import SessionUserSerializer
16
+ from .settings import cs_settings
17
+
18
+
19
+ class ContentStudioWebAppView(TemplateView):
20
+ """
21
+ View for rendering the content studio web app.
22
+ """
23
+
24
+ template_name = "content_studio/index.html"
25
+
26
+
27
+ class AdminApiViewSet(ViewSet):
28
+ """
29
+ View set for content studio admin endpoints.
30
+ """
31
+
32
+ permission_classes = [IsAdminUser]
33
+ renderer_classes = [JSONRenderer]
34
+
35
+ @action(
36
+ methods=["get"],
37
+ detail=False,
38
+ url_path="info",
39
+ permission_classes=[AllowAny],
40
+ )
41
+ def info(self, request):
42
+ """
43
+ Returns public information about the Content Studio admin.
44
+ """
45
+ admin_site = cs_settings.ADMIN_SITE
46
+
47
+ data = {
48
+ "version": __version__,
49
+ "site_header": admin_site.site_header,
50
+ "site_title": admin_site.site_title,
51
+ "index_title": admin_site.index_title,
52
+ "site_url": admin_site.site_url,
53
+ "health_check": get_health_check_path(),
54
+ "login_backends": [
55
+ backend.get_info()
56
+ for backend in admin_site.login_backend.active_backends
57
+ ],
58
+ "token_backend": admin_site.token_backend.active_backend.get_info(),
59
+ "formats": {
60
+ model_class.__name__: frmt.serialize()
61
+ for model_class, frmt in admin_site.default_format_mapping.items()
62
+ },
63
+ "widgets": get_widgets(),
64
+ "settings": {
65
+ "created_by_attr": cs_settings.CREATED_BY_ATTR,
66
+ "created_at_attr": cs_settings.CREATED_AT_ATTR,
67
+ "edited_by_attr": cs_settings.EDITED_BY_ATTR,
68
+ "edited_at_attr": cs_settings.EDITED_AT_ATTR,
69
+ },
70
+ }
71
+
72
+ return Response(data=data)
73
+
74
+ @action(
75
+ methods=["get"],
76
+ detail=False,
77
+ url_path="discover",
78
+ )
79
+ def discover(
80
+ self,
81
+ request,
82
+ ):
83
+ """
84
+ Returns information about the Django app (models, admin models, admin site, settings, etc.).
85
+ """
86
+ admin_site = cs_settings.ADMIN_SITE
87
+ data = {
88
+ "models": get_models(request),
89
+ "model_groups": get_model_groups(),
90
+ "user_model": settings.AUTH_USER_MODEL,
91
+ }
92
+
93
+ media_model = cs_settings.MEDIA_LIBRARY_MODEL
94
+ folder_model = cs_settings.MEDIA_LIBRARY_FOLDER_MODEL
95
+
96
+ data["media_library"] = {
97
+ "enabled": media_model is not None,
98
+ "folders": folder_model is not None,
99
+ "models": {
100
+ "media_model": media_model._meta.label_lower,
101
+ "folder_model": folder_model._meta.label_lower,
102
+ },
103
+ }
104
+
105
+ if admin_site.dashboard:
106
+ data["dashboard"] = admin_site.dashboard.serialize()
107
+ else:
108
+ data["dashboard"] = {"widgets": []}
109
+
110
+ return Response(data=data)
111
+
112
+ @action(methods=["get"], detail=False, url_path="me")
113
+ def me(self, request):
114
+ """
115
+ Returns information about the current user.
116
+ """
117
+ return Response(SessionUserSerializer(request.user).data)
118
+
119
+
120
+ def get_models(request):
121
+ models = []
122
+ registered_models = admin.site._registry
123
+
124
+ for model, admin_class in registered_models.items():
125
+ models.append(
126
+ {
127
+ **ModelSerializer(model).serialize(),
128
+ "admin": AdminSerializer(admin_class).serialize(request),
129
+ }
130
+ )
131
+ for inline in admin_class.inlines:
132
+ if inline.model not in registered_models:
133
+ models.append(
134
+ {
135
+ **ModelSerializer(inline.model).serialize(),
136
+ }
137
+ )
138
+
139
+ return models
140
+
141
+
142
+ def get_model_groups():
143
+ admin_site = cs_settings.ADMIN_SITE
144
+
145
+ default_group = [
146
+ ModelGroup(
147
+ label=_("Content"),
148
+ name="default",
149
+ icon=None,
150
+ models=[model for model, admin_model in admin.site._registry.items()],
151
+ )
152
+ ]
153
+ # Get custom model groups or use the default one
154
+ model_groups = getattr(admin_site, "model_groups", None) or default_group
155
+
156
+ return [
157
+ {
158
+ "name": group.name,
159
+ "icon": group.icon,
160
+ "color": group.color,
161
+ "label": group.label,
162
+ "models": [m._meta.label_lower for m in group.models],
163
+ }
164
+ for group in model_groups
165
+ ]
166
+
167
+
168
+ def get_widgets():
169
+ admin_site = cs_settings.ADMIN_SITE
170
+
171
+ return {
172
+ (m if isinstance(m, str) else m.__name__): widget.serialize()
173
+ for m, widget in admin_site.default_widget_mapping.items()
174
+ }
175
+
176
+
177
+ def get_health_check_path():
178
+ try:
179
+ return reverse("healthcheck")
180
+ except NoReverseMatch:
181
+ return None