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.
- content_studio/__init__.py +7 -0
- content_studio/admin.py +307 -0
- content_studio/apps.py +100 -0
- content_studio/dashboard/__init__.py +64 -0
- content_studio/dashboard/activity_log.py +35 -0
- content_studio/filters.py +124 -0
- content_studio/form.py +178 -0
- content_studio/formats.py +59 -0
- content_studio/login_backends/__init__.py +21 -0
- content_studio/login_backends/username_password.py +82 -0
- content_studio/media_library/serializers.py +25 -0
- content_studio/media_library/viewsets.py +132 -0
- content_studio/models.py +73 -0
- content_studio/paginators.py +20 -0
- content_studio/router.py +14 -0
- content_studio/serializers.py +118 -0
- content_studio/settings.py +152 -0
- content_studio/static/content_studio/assets/browser-ponyfill-Ct7s-5jI.js +2 -0
- content_studio/static/content_studio/assets/browser-ponyfill-TyWUZ1Oq.js +2 -0
- content_studio/static/content_studio/assets/index.css +1 -0
- content_studio/static/content_studio/assets/index.js +249 -0
- content_studio/static/content_studio/assets/inter-cyrillic-ext-wght-normal.woff2 +0 -0
- content_studio/static/content_studio/assets/inter-cyrillic-wght-normal.woff2 +0 -0
- content_studio/static/content_studio/assets/inter-greek-ext-wght-normal.woff2 +0 -0
- content_studio/static/content_studio/assets/inter-greek-wght-normal.woff2 +0 -0
- content_studio/static/content_studio/assets/inter-latin-ext-wght-normal.woff2 +0 -0
- content_studio/static/content_studio/assets/inter-latin-wght-normal.woff2 +0 -0
- content_studio/static/content_studio/assets/inter-vietnamese-wght-normal.woff2 +0 -0
- content_studio/static/content_studio/icons/pi/Phosphor-Bold.svg +3057 -0
- content_studio/static/content_studio/icons/pi/Phosphor-Bold.ttf +0 -0
- content_studio/static/content_studio/icons/pi/Phosphor-Bold.woff +0 -0
- content_studio/static/content_studio/icons/pi/Phosphor-Bold.woff2 +0 -0
- content_studio/static/content_studio/icons/pi/style.css +4627 -0
- content_studio/static/content_studio/img/media_placeholder.svg +1 -0
- content_studio/static/content_studio/locales/en/translation.json +85 -0
- content_studio/static/content_studio/locales/nl/translation.json +90 -0
- content_studio/static/content_studio/vite.svg +1 -0
- content_studio/templates/content_studio/index.html +24 -0
- content_studio/token_backends/__init__.py +39 -0
- content_studio/token_backends/jwt.py +56 -0
- content_studio/urls.py +21 -0
- content_studio/utils.py +62 -0
- content_studio/views.py +181 -0
- content_studio/viewsets.py +114 -0
- content_studio/widgets.py +83 -0
- django_content_studio-1.0.0b5.dist-info/LICENSE +21 -0
- django_content_studio-1.0.0b5.dist-info/METADATA +86 -0
- django_content_studio-1.0.0b5.dist-info/RECORD +49 -0
- 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
|
content_studio/utils.py
ADDED
|
@@ -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.")
|
content_studio/views.py
ADDED
|
@@ -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
|