django-content-studio 1.0.0a1__py3-none-any.whl → 1.0.0b1__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/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.dist-info}/METADATA +4 -6
- django_content_studio-1.0.0b1.dist-info/RECORD +29 -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.dist-info}/LICENSE +0 -0
- {django_content_studio-1.0.0a1.dist-info → django_content_studio-1.0.0b1.dist-info}/WHEEL +0 -0
content_studio/__init__.py
CHANGED
content_studio/admin.py
CHANGED
|
@@ -1,53 +1,151 @@
|
|
|
1
|
-
from
|
|
1
|
+
from typing import Type
|
|
2
|
+
|
|
3
|
+
from django.contrib import admin
|
|
4
|
+
from django.db import models
|
|
5
|
+
from django.db.models import Model
|
|
2
6
|
from rest_framework.request import HttpRequest
|
|
3
7
|
|
|
4
|
-
from .
|
|
8
|
+
from . import widgets, formats
|
|
5
9
|
from .form import FormSet, FormSetGroup
|
|
6
10
|
from .login_backends import LoginBackendManager
|
|
7
11
|
from .token_backends import TokenBackendManager
|
|
12
|
+
from .utils import get_related_field_name
|
|
13
|
+
|
|
14
|
+
register = admin.register
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class StackedInline(admin.StackedInline):
|
|
18
|
+
pass
|
|
19
|
+
|
|
8
20
|
|
|
21
|
+
class TabularInline(admin.TabularInline):
|
|
22
|
+
pass
|
|
9
23
|
|
|
10
|
-
|
|
24
|
+
|
|
25
|
+
class AdminSite(admin.AdminSite):
|
|
11
26
|
"""
|
|
12
|
-
Enhanced admin site for Django Content Studio
|
|
13
|
-
Django Content Framework.
|
|
27
|
+
Enhanced admin site for Django Content Studio.
|
|
14
28
|
"""
|
|
15
29
|
|
|
16
30
|
token_backend = TokenBackendManager()
|
|
17
31
|
|
|
18
32
|
login_backend = LoginBackendManager()
|
|
19
33
|
|
|
20
|
-
dashboard =
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
34
|
+
dashboard = None
|
|
35
|
+
|
|
36
|
+
model_groups = None
|
|
37
|
+
|
|
38
|
+
default_widget_mapping = {
|
|
39
|
+
models.CharField: widgets.InputWidget,
|
|
40
|
+
models.IntegerField: widgets.InputWidget,
|
|
41
|
+
models.SmallIntegerField: widgets.InputWidget,
|
|
42
|
+
models.BigIntegerField: widgets.InputWidget,
|
|
43
|
+
models.PositiveIntegerField: widgets.InputWidget,
|
|
44
|
+
models.PositiveSmallIntegerField: widgets.InputWidget,
|
|
45
|
+
models.PositiveBigIntegerField: widgets.InputWidget,
|
|
46
|
+
models.FloatField: widgets.InputWidget,
|
|
47
|
+
models.DecimalField: widgets.InputWidget,
|
|
48
|
+
models.SlugField: widgets.SlugWidget,
|
|
49
|
+
models.TextField: widgets.TextAreaWidget,
|
|
50
|
+
models.BooleanField: widgets.CheckboxWidget,
|
|
51
|
+
models.NullBooleanField: widgets.CheckboxWidget,
|
|
52
|
+
models.ForeignKey: widgets.ForeignKeyWidget,
|
|
53
|
+
models.ManyToManyField: widgets.ManyToManyWidget,
|
|
54
|
+
models.OneToOneField: widgets.ForeignKeyWidget,
|
|
55
|
+
models.DateField: widgets.DateWidget,
|
|
56
|
+
models.DateTimeField: widgets.DateTimeWidget,
|
|
57
|
+
models.TimeField: widgets.TimeWidget,
|
|
58
|
+
models.JSONField: widgets.JSONWidget,
|
|
59
|
+
# Common third-party fields
|
|
60
|
+
"AutoSlugField": widgets.SlugWidget,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
default_format_mapping = {
|
|
64
|
+
models.CharField: formats.TextFormat,
|
|
65
|
+
models.IntegerField: formats.NumberFormat,
|
|
66
|
+
models.SmallIntegerField: formats.NumberFormat,
|
|
67
|
+
models.BigIntegerField: formats.NumberFormat,
|
|
68
|
+
models.PositiveIntegerField: formats.NumberFormat,
|
|
69
|
+
models.PositiveSmallIntegerField: formats.NumberFormat,
|
|
70
|
+
models.PositiveBigIntegerField: formats.NumberFormat,
|
|
71
|
+
models.FloatField: formats.NumberFormat,
|
|
72
|
+
models.DecimalField: formats.NumberFormat,
|
|
73
|
+
models.SlugField: formats.TextFormat,
|
|
74
|
+
models.TextField: formats.TextFormat,
|
|
75
|
+
models.BooleanField: formats.BooleanFormat,
|
|
76
|
+
models.NullBooleanField: formats.BooleanFormat,
|
|
77
|
+
models.DateField: formats.DateFormat,
|
|
78
|
+
models.DateTimeField: formats.DateTimeFormat,
|
|
79
|
+
models.TimeField: formats.TimeFormat,
|
|
80
|
+
models.ForeignKey: formats.ForeignKeyFormat,
|
|
81
|
+
models.OneToOneField: formats.ForeignKeyFormat,
|
|
82
|
+
models.JSONField: formats.JSONFormat,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
def setup(self):
|
|
24
86
|
# Add token backend's view set to the
|
|
25
87
|
# Content Studio router.
|
|
26
88
|
self.token_backend.set_up_router()
|
|
27
89
|
# Add login backend's view set to the
|
|
28
90
|
# Content Studio router.
|
|
29
91
|
self.login_backend.set_up_router()
|
|
92
|
+
# Add dashboard's view set to the
|
|
93
|
+
# Content Studio router.
|
|
94
|
+
if self.dashboard:
|
|
95
|
+
self.dashboard.set_up_router()
|
|
96
|
+
|
|
97
|
+
def get_thumbnail(self, obj) -> str:
|
|
98
|
+
"""
|
|
99
|
+
Method for getting and manipulating the image path (or URL).
|
|
100
|
+
By default, this returns the image path as is.
|
|
101
|
+
"""
|
|
102
|
+
return obj.file.url
|
|
30
103
|
|
|
31
104
|
|
|
32
105
|
admin_site = AdminSite()
|
|
33
106
|
|
|
34
107
|
|
|
35
|
-
class ModelAdmin(ModelAdmin):
|
|
108
|
+
class ModelAdmin(admin.ModelAdmin):
|
|
36
109
|
"""
|
|
37
110
|
Enhanced model admin for Django Content Studio and integration with
|
|
38
111
|
Django Content Framework. Although it's relatively backwards compatible,
|
|
39
112
|
some default behavior has been changed.
|
|
40
113
|
"""
|
|
41
114
|
|
|
115
|
+
# Whether the model is a singleton and should not show
|
|
116
|
+
# the list view.
|
|
117
|
+
is_singleton = False
|
|
118
|
+
|
|
119
|
+
# Override the widget used for certain fields by adding
|
|
120
|
+
# a map of field to widget. Fields that are not included
|
|
121
|
+
# will fall back to their default widget.
|
|
122
|
+
#
|
|
123
|
+
# @example
|
|
124
|
+
# widget_mapping = {'is_published': widgets.SwitchWidget}
|
|
125
|
+
widget_mapping = None
|
|
126
|
+
|
|
127
|
+
# Override the format used for certain fields by adding
|
|
128
|
+
# a map of field to format. Fields that are not included
|
|
129
|
+
# will fall back to their default format.
|
|
130
|
+
#
|
|
131
|
+
# @example
|
|
132
|
+
# format_mapping = {'file_size': widgets.FileSizeWidget}
|
|
133
|
+
format_mapping = None
|
|
134
|
+
|
|
42
135
|
# We set a lower limit than Django's default of 100
|
|
43
136
|
list_per_page = 20
|
|
44
137
|
|
|
138
|
+
# Description shown below model name on list pages
|
|
139
|
+
list_description = ""
|
|
140
|
+
|
|
45
141
|
# Configure the main section in the edit-view.
|
|
46
142
|
edit_main: list[type[FormSetGroup | FormSet | str]] = []
|
|
47
143
|
|
|
48
144
|
# Configure the sidebar in the edit-view.
|
|
49
145
|
edit_sidebar: list[type[FormSet | str]] = []
|
|
50
146
|
|
|
147
|
+
icon = None
|
|
148
|
+
|
|
51
149
|
def save_model(self, request, obj, form, change):
|
|
52
150
|
if hasattr(obj, "edited_by"):
|
|
53
151
|
obj.edited_by = request.user
|
|
@@ -88,14 +186,38 @@ class AdminSerializer:
|
|
|
88
186
|
|
|
89
187
|
def serialize(self, request: HttpRequest):
|
|
90
188
|
admin_class = self.admin_class
|
|
189
|
+
format_mapping = getattr(admin_class, "format_mapping", None) or {}
|
|
190
|
+
widget_mapping = getattr(admin_class, "widget_mapping", None) or {}
|
|
91
191
|
|
|
92
192
|
return {
|
|
193
|
+
"icon": getattr(admin_class, "icon", None),
|
|
194
|
+
"is_singleton": getattr(admin_class, "is_singleton", False),
|
|
93
195
|
"edit": {
|
|
94
196
|
"main": self.serialize_edit_main(request),
|
|
95
197
|
"sidebar": self.serialize_edit_sidebar(request),
|
|
198
|
+
"inlines": [
|
|
199
|
+
{
|
|
200
|
+
"model": inline.model._meta.label_lower,
|
|
201
|
+
"fk_name": get_related_field_name(inline, admin_class.model),
|
|
202
|
+
"list_display": getattr(inline, "list_display", None)
|
|
203
|
+
or ["__str__"],
|
|
204
|
+
}
|
|
205
|
+
for inline in admin_class.inlines
|
|
206
|
+
],
|
|
96
207
|
},
|
|
97
208
|
"list": {
|
|
98
209
|
"per_page": admin_class.list_per_page,
|
|
210
|
+
"description": getattr(admin_class, "list_description", ""),
|
|
211
|
+
"display": admin_class.list_display,
|
|
212
|
+
"search": len(admin_class.search_fields) > 0,
|
|
213
|
+
"filter": admin_class.list_filter,
|
|
214
|
+
"sortable_by": admin_class.sortable_by,
|
|
215
|
+
},
|
|
216
|
+
"widget_mapping": {
|
|
217
|
+
field: widget.serialize() for field, widget in widget_mapping.items()
|
|
218
|
+
},
|
|
219
|
+
"format_mapping": {
|
|
220
|
+
field: format.serialize() for field, format in format_mapping.items()
|
|
99
221
|
},
|
|
100
222
|
"permissions": {
|
|
101
223
|
"add_permission": admin_class.has_add_permission(request),
|
|
@@ -152,3 +274,25 @@ class AdminSerializer:
|
|
|
152
274
|
return edit_sidebar
|
|
153
275
|
|
|
154
276
|
return [FormSet(fields=edit_sidebar)]
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class ModelGroup:
|
|
280
|
+
name = None
|
|
281
|
+
label = None
|
|
282
|
+
icon = None
|
|
283
|
+
color = None
|
|
284
|
+
models = None
|
|
285
|
+
|
|
286
|
+
def __init__(
|
|
287
|
+
self,
|
|
288
|
+
name: str,
|
|
289
|
+
label: str = None,
|
|
290
|
+
icon: str = None,
|
|
291
|
+
color: str = None,
|
|
292
|
+
models: list[Type[Model]] = None,
|
|
293
|
+
):
|
|
294
|
+
self.name = name
|
|
295
|
+
self.label = label or name.capitalize()
|
|
296
|
+
self.icon = icon
|
|
297
|
+
self.color = color
|
|
298
|
+
self.models = models or []
|
content_studio/apps.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from django.apps import AppConfig
|
|
2
2
|
from django.contrib import admin
|
|
3
|
-
from rest_framework import serializers
|
|
4
3
|
|
|
5
|
-
from headless.utils import is_runserver
|
|
6
4
|
from . import VERSION
|
|
5
|
+
from .paginators import ContentPagination
|
|
6
|
+
from .settings import cs_settings
|
|
7
|
+
from .utils import is_runserver
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class DjangoContentStudioConfig(AppConfig):
|
|
@@ -29,31 +30,71 @@ class DjangoContentStudioConfig(AppConfig):
|
|
|
29
30
|
":white_check_mark:",
|
|
30
31
|
f"[green]Found {registered_models} admin models[/green]",
|
|
31
32
|
)
|
|
33
|
+
# Set up admin site routes
|
|
34
|
+
admin_site = cs_settings.ADMIN_SITE
|
|
35
|
+
admin_site.setup()
|
|
36
|
+
|
|
37
|
+
# Set up content CRUD APIs
|
|
32
38
|
self._create_crud_api()
|
|
39
|
+
|
|
33
40
|
log("\n")
|
|
34
41
|
|
|
35
42
|
def _create_crud_api(self):
|
|
36
|
-
from .viewsets import BaseModelViewSet
|
|
37
|
-
from .router import content_studio_router
|
|
38
43
|
from .utils import log
|
|
39
44
|
|
|
40
|
-
for
|
|
41
|
-
|
|
42
|
-
class Serializer(serializers.ModelSerializer):
|
|
43
|
-
class Meta:
|
|
44
|
-
model = _model
|
|
45
|
-
fields = "__all__"
|
|
45
|
+
for model, admin_model in admin.site._registry.items():
|
|
46
|
+
self._create_view_set(model, admin_model)
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
for inline in admin_model.inlines:
|
|
49
|
+
self._create_view_set(
|
|
50
|
+
parent=model, model=inline.model, admin_model=inline
|
|
51
|
+
)
|
|
50
52
|
|
|
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
53
|
log(
|
|
57
54
|
":white_check_mark:",
|
|
58
55
|
f"[green]Created CRUD API[/green]",
|
|
59
56
|
)
|
|
57
|
+
|
|
58
|
+
def _create_view_set(self, model, admin_model, parent=None):
|
|
59
|
+
from .viewsets import BaseModelViewSet
|
|
60
|
+
from .router import content_studio_router
|
|
61
|
+
from .serializers import ContentSerializer
|
|
62
|
+
|
|
63
|
+
class Pagination(ContentPagination):
|
|
64
|
+
page_size = getattr(admin_model, "list_per_page", 10)
|
|
65
|
+
|
|
66
|
+
class ViewSet(BaseModelViewSet):
|
|
67
|
+
_model = model
|
|
68
|
+
_admin_model = admin_model
|
|
69
|
+
is_singleton = getattr(admin_model, "is_singleton", False)
|
|
70
|
+
pagination_class = Pagination
|
|
71
|
+
queryset = _model.objects.all()
|
|
72
|
+
search_fields = list(getattr(_admin_model, "search_fields", []))
|
|
73
|
+
|
|
74
|
+
def get_serializer_class(self):
|
|
75
|
+
# For list views we include the specified list_display fields.
|
|
76
|
+
if self.action == "list" and not self.is_singleton:
|
|
77
|
+
available_fields = [
|
|
78
|
+
"id",
|
|
79
|
+
"__str__",
|
|
80
|
+
] + list(getattr(self._admin_model, "list_display", []))
|
|
81
|
+
# In all other cases we include all fields.
|
|
82
|
+
else:
|
|
83
|
+
available_fields = "__all__"
|
|
84
|
+
|
|
85
|
+
class Serializer(ContentSerializer):
|
|
86
|
+
|
|
87
|
+
class Meta:
|
|
88
|
+
model = self._model
|
|
89
|
+
fields = available_fields
|
|
90
|
+
|
|
91
|
+
return Serializer
|
|
92
|
+
|
|
93
|
+
if parent:
|
|
94
|
+
prefix = f"api/inlines/{parent._meta.label_lower}/{model._meta.label_lower}"
|
|
95
|
+
basename = f"content_studio_api-{parent._meta.label_lower}-{model._meta.label_lower}"
|
|
96
|
+
else:
|
|
97
|
+
prefix = f"api/content/{model._meta.label_lower}"
|
|
98
|
+
basename = f"content_studio_api-{model._meta.label_lower}"
|
|
99
|
+
|
|
100
|
+
content_studio_router.register(prefix, ViewSet, basename)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from rest_framework.decorators import action
|
|
2
|
+
from rest_framework.exceptions import NotFound
|
|
3
|
+
from rest_framework.parsers import JSONParser
|
|
4
|
+
from rest_framework.renderers import JSONRenderer
|
|
5
|
+
from rest_framework.response import Response
|
|
6
|
+
from rest_framework.viewsets import ViewSet
|
|
7
|
+
|
|
8
|
+
from content_studio.settings import cs_settings
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Dashboard:
|
|
12
|
+
"""
|
|
13
|
+
The Dashboard class is used to define the structure of the dashboard
|
|
14
|
+
in Django Content Studio.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
widgets = None
|
|
18
|
+
|
|
19
|
+
def __init__(self, **kwargs):
|
|
20
|
+
self.widgets = kwargs.get("widgets", [])
|
|
21
|
+
|
|
22
|
+
def set_up_router(self):
|
|
23
|
+
from content_studio.router import content_studio_router
|
|
24
|
+
|
|
25
|
+
content_studio_router.register(
|
|
26
|
+
"api/dashboard",
|
|
27
|
+
DashboardViewSet,
|
|
28
|
+
basename="content_studio_dashboard",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def serialize(self):
|
|
32
|
+
return {
|
|
33
|
+
"widgets": [
|
|
34
|
+
{"name": w.__class__.__name__, "col_span": getattr(w, "col_span", 1)}
|
|
35
|
+
for w in self.widgets
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class DashboardViewSet(ViewSet):
|
|
41
|
+
parser_classes = [JSONParser]
|
|
42
|
+
renderer_classes = [JSONRenderer]
|
|
43
|
+
|
|
44
|
+
def __init__(self, *args, **kwargs):
|
|
45
|
+
super(ViewSet, self).__init__()
|
|
46
|
+
admin_site = cs_settings.ADMIN_SITE
|
|
47
|
+
|
|
48
|
+
self.dashboard = admin_site.dashboard
|
|
49
|
+
self.authentication_classes = [
|
|
50
|
+
admin_site.token_backend.active_backend.authentication_class
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
@action(detail=False, url_path="(?P<name>[^/.]+)")
|
|
54
|
+
def get(self, request, name=None):
|
|
55
|
+
widget = None
|
|
56
|
+
|
|
57
|
+
for w in self.dashboard.widgets:
|
|
58
|
+
if name == w.__class__.__name__.lower():
|
|
59
|
+
widget = w
|
|
60
|
+
|
|
61
|
+
if not widget:
|
|
62
|
+
raise NotFound()
|
|
63
|
+
|
|
64
|
+
return Response(data=widget.get_data(request))
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from django.contrib.admin.models import LogEntry
|
|
2
|
+
from rest_framework import serializers
|
|
3
|
+
|
|
4
|
+
from content_studio.serializers import ContentSerializer
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LogEntrySerializer(ContentSerializer):
|
|
8
|
+
object_model = serializers.SerializerMethodField()
|
|
9
|
+
|
|
10
|
+
class Meta:
|
|
11
|
+
model = LogEntry
|
|
12
|
+
fields = [
|
|
13
|
+
"id",
|
|
14
|
+
"action_flag",
|
|
15
|
+
"action_time",
|
|
16
|
+
"user",
|
|
17
|
+
"object_id",
|
|
18
|
+
"object_repr",
|
|
19
|
+
"object_model",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
def get_object_model(self, obj):
|
|
23
|
+
return f"{obj.content_type.app_label}.{obj.content_type.model}"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ActivityLogWidget:
|
|
27
|
+
"""
|
|
28
|
+
Widget for showing activity logs.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
col_span = 2
|
|
32
|
+
|
|
33
|
+
def get_data(self, request):
|
|
34
|
+
return LogEntrySerializer(LogEntry.objects.all()[0:5], many=True).data
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
from django.db import models
|
|
4
|
+
from rest_framework.exceptions import ParseError
|
|
5
|
+
from rest_framework.filters import BaseFilterBackend
|
|
6
|
+
|
|
7
|
+
from .utils import flatten
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LookupFilter(BaseFilterBackend):
|
|
11
|
+
"""
|
|
12
|
+
A permissive filter backend that basically allows every supported lookup
|
|
13
|
+
for a given field. Automatically handles multi-value lookups and booleans.
|
|
14
|
+
Also supports exclusion.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
MULTI_VALUE_LOOKUPS = ["in", "range"]
|
|
18
|
+
|
|
19
|
+
EXCLUDE_SYMBOL = "~"
|
|
20
|
+
|
|
21
|
+
NON_FILTER_FIELDS = [
|
|
22
|
+
"search",
|
|
23
|
+
"limit",
|
|
24
|
+
"page",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
def filter_queryset(self, request, queryset, view):
|
|
28
|
+
"""
|
|
29
|
+
Build the queryset based on the query params and the view's model.
|
|
30
|
+
Only apply filters in list endpoints.
|
|
31
|
+
"""
|
|
32
|
+
if getattr(view, "action", None) == "list":
|
|
33
|
+
try:
|
|
34
|
+
filter_kwargs, exclude_kwargs = self.get_filter_kwargs(
|
|
35
|
+
model_class=view.queryset.model,
|
|
36
|
+
query_params=request.query_params,
|
|
37
|
+
)
|
|
38
|
+
except Exception as e:
|
|
39
|
+
print(e)
|
|
40
|
+
raise ParseError(detail="Invalid filter parameters")
|
|
41
|
+
return queryset.filter(**filter_kwargs).exclude(**exclude_kwargs).distinct()
|
|
42
|
+
|
|
43
|
+
return queryset
|
|
44
|
+
|
|
45
|
+
def get_filter_kwargs(self, model_class, query_params):
|
|
46
|
+
filter_kwargs = {}
|
|
47
|
+
exclude_kwargs = {}
|
|
48
|
+
|
|
49
|
+
field_lookups = self.get_field_lookups(model_class=model_class)
|
|
50
|
+
|
|
51
|
+
for key, value in query_params.lists():
|
|
52
|
+
if key in self.NON_FILTER_FIELDS:
|
|
53
|
+
continue
|
|
54
|
+
# By default Django supports repeated multi-values (e.g. `a=1&a=2`)
|
|
55
|
+
# but we allow for comma-seperated multi-values as well (e.g. `a=1,2`).
|
|
56
|
+
value = flatten([param.split(",") for param in value])
|
|
57
|
+
is_exclude = key.startswith(self.EXCLUDE_SYMBOL)
|
|
58
|
+
# The first part of a key is considered the field name
|
|
59
|
+
field_name = key.split("__")[0]
|
|
60
|
+
# Get the model field.
|
|
61
|
+
field = model_class._meta.get_field(field_name)
|
|
62
|
+
# Get the allowed lookups for this field.
|
|
63
|
+
lookups = field_lookups.get(field_name, [])
|
|
64
|
+
try:
|
|
65
|
+
# The last part of a key is considered its lookup
|
|
66
|
+
# but it's not required.
|
|
67
|
+
lookup = key.split("__")[-1]
|
|
68
|
+
if lookup not in lookups:
|
|
69
|
+
lookup = None
|
|
70
|
+
except IndexError:
|
|
71
|
+
lookup = None
|
|
72
|
+
|
|
73
|
+
# Some lookups allow multiple values, otherwise
|
|
74
|
+
# the first value is used.
|
|
75
|
+
is_multi = lookup in self.MULTI_VALUE_LOOKUPS
|
|
76
|
+
# Depending on the field type we cast the value to
|
|
77
|
+
# its correct type (i.e. number, boolean, etc.).
|
|
78
|
+
if is_multi:
|
|
79
|
+
casted_value = [self.cast_field_value(v, field) for v in value]
|
|
80
|
+
elif lookup == "isnull":
|
|
81
|
+
casted_value = value[0] in ["1", "true", "on"]
|
|
82
|
+
else:
|
|
83
|
+
casted_value = self.cast_field_value(value[0], field)
|
|
84
|
+
|
|
85
|
+
if is_exclude:
|
|
86
|
+
exclude_kwargs[key] = casted_value
|
|
87
|
+
else:
|
|
88
|
+
filter_kwargs[key] = casted_value
|
|
89
|
+
|
|
90
|
+
return filter_kwargs, exclude_kwargs
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def get_field_lookups(model_class):
|
|
94
|
+
"""
|
|
95
|
+
Allow all supported lookups.
|
|
96
|
+
"""
|
|
97
|
+
field_lookups = {}
|
|
98
|
+
for model_field in model_class._meta.get_fields():
|
|
99
|
+
lookup_list = model_field.get_lookups().keys()
|
|
100
|
+
field_lookups[model_field.name] = lookup_list
|
|
101
|
+
return field_lookups
|
|
102
|
+
|
|
103
|
+
def cast_field_value(self, value: str, field):
|
|
104
|
+
value = value.strip().lower()
|
|
105
|
+
|
|
106
|
+
if isinstance(field, models.BooleanField):
|
|
107
|
+
if value in ["1", "true", "on"]:
|
|
108
|
+
return True
|
|
109
|
+
if value in ["0", "false", "off"]:
|
|
110
|
+
return False
|
|
111
|
+
if isinstance(field, models.NullBooleanField):
|
|
112
|
+
if value in ["null", "none", "empty"]:
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
if isinstance(field, models.IntegerField):
|
|
116
|
+
return int(value)
|
|
117
|
+
|
|
118
|
+
if isinstance(field, models.DecimalField):
|
|
119
|
+
return Decimal(value)
|
|
120
|
+
|
|
121
|
+
if isinstance(field, models.FloatField):
|
|
122
|
+
return float(value)
|
|
123
|
+
|
|
124
|
+
return value
|
content_studio/form.py
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
|
+
###
|
|
2
|
+
# Form field classes are used for grouping, ordering and laying out fields.
|
|
3
|
+
###
|
|
4
|
+
|
|
5
|
+
|
|
1
6
|
class Field:
|
|
2
7
|
"""
|
|
3
8
|
Field class for configuring the fields in content edit views in Django Content Studio.
|
|
4
9
|
"""
|
|
5
10
|
|
|
6
|
-
def __init__(self, name: str, col_span: int = 1):
|
|
11
|
+
def __init__(self, name: str, col_span: int = 1, label: str = None):
|
|
7
12
|
self.name = name
|
|
8
13
|
self.col_span = col_span
|
|
14
|
+
self.label = label
|
|
9
15
|
|
|
10
16
|
def serialize(self):
|
|
11
17
|
return {
|
|
12
18
|
"name": self.name,
|
|
13
19
|
"col_span": self.col_span,
|
|
20
|
+
"label": self.label,
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
|
|
@@ -20,18 +27,23 @@ class FieldLayout:
|
|
|
20
27
|
"""
|
|
21
28
|
|
|
22
29
|
def __init__(self, fields: list[str | Field] = None, columns: int = 1):
|
|
23
|
-
self.fields = fields
|
|
30
|
+
self.fields = [self._normalize_field(f) for f in fields] if fields else []
|
|
24
31
|
self.columns = columns
|
|
25
32
|
|
|
26
33
|
def serialize(self):
|
|
27
34
|
return {
|
|
28
|
-
"fields": [
|
|
29
|
-
field.serialize() if isinstance(field, Field) else field
|
|
30
|
-
for field in self.fields
|
|
31
|
-
],
|
|
35
|
+
"fields": [field.serialize() for field in self.fields],
|
|
32
36
|
"columns": self.columns,
|
|
33
37
|
}
|
|
34
38
|
|
|
39
|
+
def _normalize_field(self, field):
|
|
40
|
+
if isinstance(field, str):
|
|
41
|
+
return Field(field)
|
|
42
|
+
elif isinstance(field, Field):
|
|
43
|
+
return field
|
|
44
|
+
else:
|
|
45
|
+
raise ValueError(f"Invalid field: {field}. Must be a string or Field.")
|
|
46
|
+
|
|
35
47
|
|
|
36
48
|
class FormSet:
|
|
37
49
|
"""
|
|
@@ -47,18 +59,27 @@ class FormSet:
|
|
|
47
59
|
):
|
|
48
60
|
self.title = title
|
|
49
61
|
self.description = description
|
|
50
|
-
self.fields = fields
|
|
62
|
+
self.fields = [self._normalize_field(f) for f in fields] if fields else []
|
|
51
63
|
|
|
52
64
|
def serialize(self):
|
|
53
65
|
return {
|
|
54
66
|
"title": self.title,
|
|
55
67
|
"description": self.description,
|
|
56
|
-
"fields": [
|
|
57
|
-
field.serialize() if isinstance(field, (Field, FieldLayout)) else field
|
|
58
|
-
for field in self.fields
|
|
59
|
-
],
|
|
68
|
+
"fields": [field.serialize() for field in self.fields],
|
|
60
69
|
}
|
|
61
70
|
|
|
71
|
+
def _normalize_field(self, field):
|
|
72
|
+
if isinstance(field, str):
|
|
73
|
+
return Field(field)
|
|
74
|
+
elif isinstance(field, Field):
|
|
75
|
+
return field
|
|
76
|
+
elif isinstance(field, FieldLayout):
|
|
77
|
+
return field
|
|
78
|
+
else:
|
|
79
|
+
raise ValueError(
|
|
80
|
+
f"Invalid field: {field}. Must be a string, Field or FieldLayout."
|
|
81
|
+
)
|
|
82
|
+
|
|
62
83
|
|
|
63
84
|
class FormSetGroup:
|
|
64
85
|
"""
|