django-content-studio 1.0.0a1__py3-none-any.whl → 1.0.0b1.post1__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 +2 -2
- content_studio/admin.py +154 -10
- content_studio/apps.py +59 -18
- content_studio/dashboard/__init__.py +64 -0
- content_studio/dashboard/activity_log.py +34 -0
- content_studio/filters.py +124 -0
- content_studio/form.py +32 -11
- content_studio/formats.py +59 -0
- content_studio/login_backends/username_password.py +6 -3
- content_studio/media_library/serializers.py +25 -0
- content_studio/media_library/viewsets.py +132 -0
- content_studio/models.py +23 -36
- content_studio/paginators.py +20 -0
- content_studio/serializers.py +105 -3
- content_studio/settings.py +7 -1
- content_studio/static/__init__.py +0 -0
- content_studio/templates/content_studio/index.html +1 -2
- content_studio/token_backends/jwt.py +6 -11
- content_studio/urls.py +11 -2
- content_studio/utils.py +32 -0
- content_studio/views.py +100 -13
- content_studio/viewsets.py +16 -44
- content_studio/widgets.py +61 -8
- {django_content_studio-1.0.0a1.dist-info → django_content_studio-1.0.0b1.post1.dist-info}/METADATA +4 -6
- django_content_studio-1.0.0b1.post1.dist-info/RECORD +30 -0
- content_studio/dashboard.py +0 -7
- django_content_studio-1.0.0a1.dist-info/RECORD +0 -23
- {django_content_studio-1.0.0a1.dist-info → django_content_studio-1.0.0b1.post1.dist-info}/LICENSE +0 -0
- {django_content_studio-1.0.0a1.dist-info → django_content_studio-1.0.0b1.post1.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
###
|
|
2
|
+
# Formats determine how fields are displayed in Django Content Studio.
|
|
3
|
+
# Each Django model field has a default format, but this can be
|
|
4
|
+
# overridden in the admin model.
|
|
5
|
+
###
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BaseFormat:
|
|
9
|
+
@classmethod
|
|
10
|
+
def serialize(cls):
|
|
11
|
+
return {"name": cls.__name__}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ForeignKeyFormat(BaseFormat):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MediaFormat(BaseFormat):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TextFormat(BaseFormat):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class HtmlFormat(BaseFormat):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BooleanFormat(BaseFormat):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class DateFormat(BaseFormat):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class DateTimeFormat(BaseFormat):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TimeFormat(BaseFormat):
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class NumberFormat(BaseFormat):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class FileSizeFormat(BaseFormat):
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TagFormat(BaseFormat):
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class JSONFormat(BaseFormat):
|
|
59
|
+
pass
|
|
@@ -29,12 +29,15 @@ class UsernamePasswordViewSet(ViewSet):
|
|
|
29
29
|
password=serializer.validated_data["password"],
|
|
30
30
|
)
|
|
31
31
|
|
|
32
|
-
if user is None
|
|
33
|
-
raise PermissionDenied()
|
|
32
|
+
if user is None:
|
|
33
|
+
raise PermissionDenied(detail="Invalid username or password.")
|
|
34
|
+
|
|
35
|
+
if not user.is_active:
|
|
36
|
+
raise PermissionDenied(detail="User account is disabled.")
|
|
34
37
|
|
|
35
38
|
from ..admin import admin_site
|
|
36
39
|
|
|
37
|
-
return admin_site.
|
|
40
|
+
return admin_site.token_backend.active_backend.get_response_for_user(user)
|
|
38
41
|
|
|
39
42
|
|
|
40
43
|
class UsernamePasswordBackend:
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from rest_framework import serializers
|
|
2
|
+
|
|
3
|
+
from content_studio.settings import cs_settings
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MediaItemSerializer(serializers.ModelSerializer):
|
|
7
|
+
thumbnail = serializers.SerializerMethodField()
|
|
8
|
+
|
|
9
|
+
class Meta:
|
|
10
|
+
model = cs_settings.MEDIA_LIBRARY_MODEL
|
|
11
|
+
fields = "__all__"
|
|
12
|
+
|
|
13
|
+
def get_thumbnail(self, obj):
|
|
14
|
+
admin_site = cs_settings.ADMIN_SITE
|
|
15
|
+
|
|
16
|
+
if admin_site and obj.type == "image":
|
|
17
|
+
return admin_site.get_thumbnail(obj)
|
|
18
|
+
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MediaFolderSerializer(serializers.ModelSerializer):
|
|
23
|
+
class Meta:
|
|
24
|
+
model = cs_settings.MEDIA_LIBRARY_FOLDER_MODEL
|
|
25
|
+
fields = ["id", "name", "parent"]
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
from rest_framework import status, exceptions
|
|
2
|
+
from rest_framework.decorators import action
|
|
3
|
+
from rest_framework.filters import OrderingFilter, SearchFilter
|
|
4
|
+
from rest_framework.parsers import JSONParser, MultiPartParser
|
|
5
|
+
from rest_framework.permissions import DjangoModelPermissions
|
|
6
|
+
from rest_framework.renderers import JSONRenderer
|
|
7
|
+
from rest_framework.response import Response
|
|
8
|
+
from rest_framework.viewsets import ModelViewSet
|
|
9
|
+
|
|
10
|
+
from content_studio.paginators import ContentPagination
|
|
11
|
+
from content_studio.settings import cs_settings
|
|
12
|
+
from .serializers import MediaItemSerializer, MediaFolderSerializer
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MediaLibraryViewSet(ModelViewSet):
|
|
16
|
+
_media_model = None
|
|
17
|
+
lookup_field = "id"
|
|
18
|
+
parser_classes = [JSONParser, MultiPartParser]
|
|
19
|
+
renderer_classes = [JSONRenderer]
|
|
20
|
+
permission_classes = [DjangoModelPermissions]
|
|
21
|
+
filter_backends = [SearchFilter, OrderingFilter]
|
|
22
|
+
pagination_class = ContentPagination
|
|
23
|
+
|
|
24
|
+
def __init__(self, *args, **kwargs):
|
|
25
|
+
super(MediaLibraryViewSet, self).__init__()
|
|
26
|
+
|
|
27
|
+
admin_site = cs_settings.ADMIN_SITE
|
|
28
|
+
|
|
29
|
+
self.authentication_classes = [
|
|
30
|
+
admin_site.token_backend.active_backend.authentication_class
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
def get_queryset(self):
|
|
34
|
+
if not self._media_model:
|
|
35
|
+
self._media_model = cs_settings.MEDIA_LIBRARY_MODEL
|
|
36
|
+
|
|
37
|
+
if not self._media_model:
|
|
38
|
+
raise exceptions.MethodNotAllowed(
|
|
39
|
+
method="GET", detail="Media model not defined."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
folder = self.request.query_params.get("folder", None)
|
|
43
|
+
qs = self._media_model.objects.all()
|
|
44
|
+
|
|
45
|
+
if folder:
|
|
46
|
+
if folder == "root":
|
|
47
|
+
return qs.filter(folder__isnull=True)
|
|
48
|
+
return qs.filter(folder=folder)
|
|
49
|
+
|
|
50
|
+
return qs
|
|
51
|
+
|
|
52
|
+
def get_serializer_class(self):
|
|
53
|
+
if self._media_model:
|
|
54
|
+
return MediaItemSerializer
|
|
55
|
+
|
|
56
|
+
raise exceptions.MethodNotAllowed(
|
|
57
|
+
method="GET", detail="Media model not defined."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class MediaFolderViewSet(ModelViewSet):
|
|
62
|
+
_folder_model = None
|
|
63
|
+
lookup_field = "id"
|
|
64
|
+
parser_classes = [JSONParser]
|
|
65
|
+
renderer_classes = [JSONRenderer]
|
|
66
|
+
permission_classes = [DjangoModelPermissions]
|
|
67
|
+
filter_backends = [SearchFilter, OrderingFilter]
|
|
68
|
+
pagination_class = ContentPagination
|
|
69
|
+
|
|
70
|
+
def __init__(self, *args, **kwargs):
|
|
71
|
+
super(MediaFolderViewSet, self).__init__()
|
|
72
|
+
|
|
73
|
+
admin_site = cs_settings.ADMIN_SITE
|
|
74
|
+
|
|
75
|
+
self.authentication_classes = [
|
|
76
|
+
admin_site.token_backend.active_backend.authentication_class
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
def get_queryset(self):
|
|
80
|
+
if not self._folder_model:
|
|
81
|
+
self._folder_model = cs_settings.MEDIA_LIBRARY_FOLDER_MODEL
|
|
82
|
+
|
|
83
|
+
if not self._folder_model:
|
|
84
|
+
raise exceptions.MethodNotAllowed(
|
|
85
|
+
method="GET", detail="Media folder model not defined."
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
parent = self.request.query_params.get("parent", None)
|
|
89
|
+
qs = self._folder_model.objects.all()
|
|
90
|
+
|
|
91
|
+
if self.action != "list":
|
|
92
|
+
return qs
|
|
93
|
+
|
|
94
|
+
# The list endpoint is always within the scope of a folder
|
|
95
|
+
if not parent:
|
|
96
|
+
return qs.filter(parent__isnull=True)
|
|
97
|
+
return qs.filter(parent=parent)
|
|
98
|
+
|
|
99
|
+
def get_serializer_class(self):
|
|
100
|
+
if self._folder_model:
|
|
101
|
+
return MediaFolderSerializer
|
|
102
|
+
|
|
103
|
+
raise exceptions.MethodNotAllowed(
|
|
104
|
+
method="GET", detail="Media folder model not defined."
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
@action(methods=["get"], detail=False, url_path="path")
|
|
108
|
+
def get(self, request, *args, **kwargs):
|
|
109
|
+
|
|
110
|
+
if not self._folder_model:
|
|
111
|
+
self._folder_model = cs_settings.MEDIA_LIBRARY_FOLDER_MODEL
|
|
112
|
+
|
|
113
|
+
if not self._folder_model:
|
|
114
|
+
raise exceptions.MethodNotAllowed(
|
|
115
|
+
method="GET", detail="Folder model not defined."
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
folder_id = request.query_params.get("folder", None)
|
|
119
|
+
|
|
120
|
+
if not folder_id:
|
|
121
|
+
return Response(data=[])
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
folder = self._folder_model.objects.get(pk=folder_id)
|
|
125
|
+
path = []
|
|
126
|
+
while folder:
|
|
127
|
+
path.insert(0, folder)
|
|
128
|
+
folder = folder.parent
|
|
129
|
+
|
|
130
|
+
return Response(data=MediaFolderSerializer(path, many=True).data)
|
|
131
|
+
except self._folder_model.DoesNotExist:
|
|
132
|
+
return Response(status=status.HTTP_404_NOT_FOUND)
|
content_studio/models.py
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
from django.db import models
|
|
2
2
|
|
|
3
|
-
from content_framework import fields as cf_fields
|
|
4
|
-
from . import widgets
|
|
5
3
|
from .utils import is_jsonable
|
|
6
4
|
|
|
7
5
|
|
|
@@ -9,61 +7,50 @@ class ModelSerializer:
|
|
|
9
7
|
def __init__(self, model: type[models.Model]):
|
|
10
8
|
self.model = model
|
|
11
9
|
|
|
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
10
|
def serialize(self):
|
|
33
11
|
model = self.model
|
|
34
12
|
|
|
35
13
|
return {
|
|
36
|
-
"label": model._meta.
|
|
14
|
+
"label": model._meta.label_lower,
|
|
37
15
|
"verbose_name": model._meta.verbose_name,
|
|
38
16
|
"verbose_name_plural": model._meta.verbose_name_plural,
|
|
39
17
|
"fields": self.get_fields(),
|
|
40
18
|
}
|
|
41
19
|
|
|
42
20
|
def get_fields(self):
|
|
43
|
-
fields = {
|
|
21
|
+
fields = {
|
|
22
|
+
"__str__": {
|
|
23
|
+
"type": "CharField",
|
|
24
|
+
"readonly": True,
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
standard_fields = self.model._meta.fields
|
|
28
|
+
m2m_fields = self.model._meta.many_to_many
|
|
44
29
|
|
|
45
|
-
for field in
|
|
30
|
+
for field in standard_fields + m2m_fields:
|
|
46
31
|
fields[field.name] = self.get_field(field)
|
|
47
32
|
|
|
48
33
|
return fields
|
|
49
34
|
|
|
50
35
|
def get_field(self, field):
|
|
51
|
-
widget = self.get_widget(field)
|
|
52
|
-
|
|
53
36
|
data = {
|
|
54
37
|
"verbose_name": field.verbose_name,
|
|
55
38
|
"required": not field.null or not field.blank,
|
|
39
|
+
"type": field.__class__.__name__,
|
|
56
40
|
}
|
|
57
41
|
|
|
42
|
+
if hasattr(field, "widget_class"):
|
|
43
|
+
data["widget_class"] = field.widget_class.__name__
|
|
44
|
+
|
|
45
|
+
if hasattr(field, "format_class"):
|
|
46
|
+
data["format_class"] = field.format_class.__name__
|
|
47
|
+
|
|
58
48
|
if field.help_text:
|
|
59
49
|
data["help_text"] = field.help_text
|
|
60
50
|
|
|
61
51
|
if is_jsonable(field.default):
|
|
62
52
|
data["default"] = field.default
|
|
63
53
|
|
|
64
|
-
if widget:
|
|
65
|
-
data["widget"] = widget
|
|
66
|
-
|
|
67
54
|
if not field.editable:
|
|
68
55
|
data["readonly"] = True
|
|
69
56
|
|
|
@@ -71,16 +58,16 @@ class ModelSerializer:
|
|
|
71
58
|
data["primary_key"] = True
|
|
72
59
|
data["readonly"] = True
|
|
73
60
|
|
|
61
|
+
if field.is_relation:
|
|
62
|
+
data["related_model"] = field.related_model._meta.label_lower
|
|
63
|
+
|
|
74
64
|
if getattr(field, "choices", None) is not None:
|
|
75
65
|
data["choices"] = field.choices
|
|
76
66
|
|
|
77
67
|
if getattr(field, "max_length", None) is not None:
|
|
78
68
|
data["max_length"] = field.max_length
|
|
79
69
|
|
|
80
|
-
|
|
70
|
+
if getattr(field, "cs_get_field_attributes", None):
|
|
71
|
+
data.update(field.cs_get_field_attributes())
|
|
81
72
|
|
|
82
|
-
|
|
83
|
-
try:
|
|
84
|
-
return self.widgets[field.__class__].__name__
|
|
85
|
-
except KeyError:
|
|
86
|
-
return None
|
|
73
|
+
return data
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from rest_framework.pagination import PageNumberPagination
|
|
2
|
+
from rest_framework.response import Response
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ContentPagination(PageNumberPagination):
|
|
6
|
+
page_size = 20
|
|
7
|
+
page_size_query_param = "limit"
|
|
8
|
+
max_page_size = 100
|
|
9
|
+
|
|
10
|
+
def get_paginated_response(self, data):
|
|
11
|
+
return Response(
|
|
12
|
+
{
|
|
13
|
+
"pagination": {
|
|
14
|
+
"count": self.page.paginator.count,
|
|
15
|
+
"current": self.page.number,
|
|
16
|
+
"pages": self.page.paginator.num_pages,
|
|
17
|
+
},
|
|
18
|
+
"results": data,
|
|
19
|
+
}
|
|
20
|
+
)
|
content_studio/serializers.py
CHANGED
|
@@ -1,16 +1,118 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
|
|
1
3
|
from django.contrib.auth import get_user_model
|
|
2
4
|
from rest_framework import serializers
|
|
5
|
+
from rest_framework.relations import RelatedField
|
|
3
6
|
|
|
4
7
|
user_model = get_user_model()
|
|
5
8
|
|
|
6
9
|
|
|
7
|
-
class
|
|
10
|
+
class ContentRelatedField(RelatedField):
|
|
11
|
+
|
|
12
|
+
def to_representation(self, value):
|
|
13
|
+
from content_studio.settings import cs_settings
|
|
14
|
+
|
|
15
|
+
admin_site = cs_settings.ADMIN_SITE
|
|
16
|
+
data = {"id": value.id, "__str__": str(value)}
|
|
17
|
+
|
|
18
|
+
# Add file URL and media type if the model is a media library model.
|
|
19
|
+
if value.__class__ is cs_settings.MEDIA_LIBRARY_MODEL:
|
|
20
|
+
data["file"] = value.file.url
|
|
21
|
+
data["type"] = value.type
|
|
22
|
+
data["thumbnail"] = admin_site.get_thumbnail(value)
|
|
23
|
+
|
|
24
|
+
return data
|
|
25
|
+
|
|
26
|
+
def to_internal_value(self, data):
|
|
27
|
+
return self.get_queryset().get(id=data["id"])
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ContentSerializer(serializers.ModelSerializer):
|
|
31
|
+
__str__ = serializers.CharField(read_only=True)
|
|
32
|
+
serializer_related_field = ContentRelatedField
|
|
33
|
+
|
|
34
|
+
def get_field_names(self, declared_fields, info):
|
|
35
|
+
"""
|
|
36
|
+
Override to automatically add model properties to the fields list.
|
|
37
|
+
"""
|
|
38
|
+
# Get the default fields from parent class
|
|
39
|
+
fields = super().get_field_names(declared_fields, info)
|
|
40
|
+
|
|
41
|
+
# If fields is '__all__', we need to expand it
|
|
42
|
+
if fields == "__all__":
|
|
43
|
+
fields = list(super().get_field_names(declared_fields, info))
|
|
44
|
+
else:
|
|
45
|
+
fields = list(fields)
|
|
46
|
+
|
|
47
|
+
# Get all properties from the model
|
|
48
|
+
model_properties = self._get_model_properties()
|
|
49
|
+
|
|
50
|
+
# Add properties that aren't already in fields
|
|
51
|
+
for prop_name in model_properties:
|
|
52
|
+
if prop_name not in fields and prop_name not in declared_fields:
|
|
53
|
+
fields.append(prop_name)
|
|
54
|
+
|
|
55
|
+
return fields
|
|
56
|
+
|
|
57
|
+
def _get_model_properties(self):
|
|
58
|
+
"""
|
|
59
|
+
Extract all @property attributes from the model class.
|
|
60
|
+
"""
|
|
61
|
+
model = self.Meta.model
|
|
62
|
+
properties = []
|
|
63
|
+
|
|
64
|
+
# Inspect the model class and its parent classes
|
|
65
|
+
for klass in inspect.getmro(model):
|
|
66
|
+
if klass == object:
|
|
67
|
+
break
|
|
68
|
+
|
|
69
|
+
for name, obj in inspect.getmembers(klass):
|
|
70
|
+
# Check if it's a property and not private/protected
|
|
71
|
+
if isinstance(obj, property) and not name.startswith("_"):
|
|
72
|
+
if name not in properties:
|
|
73
|
+
properties.append(name)
|
|
74
|
+
|
|
75
|
+
return properties
|
|
76
|
+
|
|
77
|
+
def build_property_field(self, field_name, model_class):
|
|
78
|
+
"""
|
|
79
|
+
Create a ReadOnlyField for property attributes.
|
|
80
|
+
"""
|
|
81
|
+
return serializers.ReadOnlyField, {}
|
|
82
|
+
|
|
83
|
+
def build_field(self, field_name, info, model_class, nested_depth):
|
|
84
|
+
"""
|
|
85
|
+
Override to handle property fields.
|
|
86
|
+
"""
|
|
87
|
+
# Check if this is a property
|
|
88
|
+
if hasattr(model_class, field_name):
|
|
89
|
+
attr = getattr(model_class, field_name)
|
|
90
|
+
if isinstance(attr, property):
|
|
91
|
+
return self.build_property_field(field_name, model_class)
|
|
92
|
+
|
|
93
|
+
# Fall back to default behavior for regular fields
|
|
94
|
+
return super().build_field(field_name, info, model_class, nested_depth)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class SessionUserSerializer(serializers.ModelSerializer):
|
|
98
|
+
username = serializers.CharField(source=user_model.USERNAME_FIELD)
|
|
99
|
+
|
|
100
|
+
class Meta:
|
|
101
|
+
model = user_model
|
|
102
|
+
fields = (
|
|
103
|
+
"id",
|
|
104
|
+
"username",
|
|
105
|
+
"first_name",
|
|
106
|
+
"last_name",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class UserSerializer(serializers.ModelSerializer):
|
|
111
|
+
|
|
8
112
|
class Meta:
|
|
9
113
|
model = user_model
|
|
10
114
|
fields = (
|
|
11
115
|
"id",
|
|
12
|
-
user_model.USERNAME_FIELD,
|
|
13
|
-
user_model.EMAIL_FIELD,
|
|
14
116
|
"first_name",
|
|
15
117
|
"last_name",
|
|
16
118
|
)
|
content_studio/settings.py
CHANGED
|
@@ -21,19 +21,25 @@ from django.utils.module_loading import import_string
|
|
|
21
21
|
SETTINGS_NAMESPACE = "CONTENT_STUDIO"
|
|
22
22
|
|
|
23
23
|
DEFAULTS = {
|
|
24
|
+
"ADMIN_SITE": "content_studio.admin.admin_site",
|
|
24
25
|
"LOGIN_BACKENDS": [
|
|
25
26
|
"content_studio.login_backends.UsernamePasswordBackend",
|
|
26
27
|
],
|
|
27
|
-
"
|
|
28
|
+
"EDITED_BY_ATTR": "edited_by",
|
|
28
29
|
"EDITED_AT_ATTR": "edited_at",
|
|
29
30
|
"CREATED_BY_ATTR": "created_by",
|
|
30
31
|
"CREATED_AT_ATTR": "created_at",
|
|
32
|
+
"MEDIA_LIBRARY_MODEL": None,
|
|
33
|
+
"MEDIA_LIBRARY_FOLDER_MODEL": None,
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
|
|
34
37
|
# List of settings that may be in string import notation.
|
|
35
38
|
IMPORT_STRINGS = [
|
|
39
|
+
"ADMIN_SITE",
|
|
36
40
|
"LOGIN_BACKENDS",
|
|
41
|
+
"MEDIA_LIBRARY_MODEL",
|
|
42
|
+
"MEDIA_LIBRARY_FOLDER_MODEL",
|
|
37
43
|
]
|
|
38
44
|
|
|
39
45
|
|
|
File without changes
|
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
<title>Django Content Studio</title>
|
|
9
9
|
<script>
|
|
10
10
|
window.DCS_STATIC_PREFIX = "{% static '' %}content_studio/";
|
|
11
|
-
window.DCS_BASENAME = "{% url '
|
|
12
|
-
window.DCS_API_URL = "{% url 'headless_rest' %}";
|
|
11
|
+
window.DCS_BASENAME = "{% url 'content_studio_web' %}";
|
|
13
12
|
</script>
|
|
14
13
|
<script type="module" crossorigin src="{% static 'content_studio/assets/index.js' %}"></script>
|
|
15
14
|
<link rel="stylesheet" crossorigin href="{% static 'content_studio/assets/index.css' %}">
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
from rest_framework.decorators import action
|
|
2
2
|
from rest_framework.response import Response
|
|
3
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
|
|
4
8
|
|
|
5
9
|
|
|
6
10
|
class SimpleJwtViewSet(ViewSet):
|
|
@@ -8,7 +12,6 @@ class SimpleJwtViewSet(ViewSet):
|
|
|
8
12
|
detail=False, methods=["post"], permission_classes=[], authentication_classes=[]
|
|
9
13
|
)
|
|
10
14
|
def refresh(self, request):
|
|
11
|
-
from rest_framework_simplejwt.views import TokenRefreshView
|
|
12
15
|
|
|
13
16
|
view_instance = TokenRefreshView()
|
|
14
17
|
view_instance.request = request
|
|
@@ -18,22 +21,15 @@ class SimpleJwtViewSet(ViewSet):
|
|
|
18
21
|
|
|
19
22
|
class SimpleJwtBackend:
|
|
20
23
|
name = "Simple JWT"
|
|
21
|
-
authentication_class =
|
|
24
|
+
authentication_class = JWTAuthentication
|
|
22
25
|
view_set = SimpleJwtViewSet
|
|
23
26
|
|
|
24
|
-
def __init__(self):
|
|
25
|
-
from rest_framework_simplejwt.authentication import JWTAuthentication
|
|
26
|
-
|
|
27
|
-
self.authentication_class = JWTAuthentication
|
|
28
|
-
|
|
29
27
|
@classmethod
|
|
30
28
|
def get_info(cls):
|
|
31
29
|
|
|
32
|
-
from rest_framework_simplejwt.settings import api_settings as simplejwt_settings
|
|
33
|
-
|
|
34
30
|
return {
|
|
35
31
|
"type": cls.__name__,
|
|
36
|
-
"
|
|
32
|
+
"config": {
|
|
37
33
|
"ACCESS_TOKEN_LIFETIME": simplejwt_settings.ACCESS_TOKEN_LIFETIME.total_seconds(),
|
|
38
34
|
},
|
|
39
35
|
}
|
|
@@ -49,7 +45,6 @@ class SimpleJwtBackend:
|
|
|
49
45
|
|
|
50
46
|
@classmethod
|
|
51
47
|
def get_response_for_user(cls, user):
|
|
52
|
-
from rest_framework_simplejwt.tokens import RefreshToken
|
|
53
48
|
|
|
54
49
|
refresh = RefreshToken.for_user(user)
|
|
55
50
|
|
content_studio/urls.py
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
from django.urls import re_path
|
|
2
2
|
|
|
3
|
+
from .media_library.viewsets import MediaLibraryViewSet, MediaFolderViewSet
|
|
3
4
|
from .router import content_studio_router
|
|
4
5
|
from .views import ContentStudioWebAppView, AdminApiViewSet
|
|
5
6
|
|
|
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
|
+
)
|
|
7
16
|
|
|
8
|
-
urlpatterns =
|
|
17
|
+
urlpatterns = [
|
|
9
18
|
re_path(
|
|
10
19
|
"^(?!api).*$", ContentStudioWebAppView.as_view(), name="content_studio_web"
|
|
11
20
|
),
|
|
12
|
-
]
|
|
21
|
+
] + content_studio_router.urls
|
content_studio/utils.py
CHANGED
|
@@ -10,6 +10,10 @@ def log(*args, **kwargs):
|
|
|
10
10
|
console.print(*args, **kwargs)
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
def flatten(xss):
|
|
14
|
+
return [x for xs in xss for x in xs]
|
|
15
|
+
|
|
16
|
+
|
|
13
17
|
def is_runserver():
|
|
14
18
|
"""
|
|
15
19
|
Checks if the Django application is started as a server.
|
|
@@ -28,3 +32,31 @@ def is_jsonable(x):
|
|
|
28
32
|
return True
|
|
29
33
|
except (TypeError, OverflowError):
|
|
30
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.")
|