django-content-studio 1.0.0b7__py3-none-any.whl → 1.0.0b9__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.
@@ -0,0 +1,22 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Django Content Studio</title>
8
+ <script>
9
+ window.DCS_STATIC_PREFIX = "/";
10
+ window.DCS_BASENAME = "http://localhost:8000/admin";
11
+ </script>
12
+ <link rel="stylesheet" href="/icons/pi/style.css" />
13
+ <link rel="preconnect" href="https://fonts.googleapis.com">
14
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
15
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,100..700;1,100..700&display=swap" rel="stylesheet">
16
+ <script type="module" crossorigin src="/assets/index.js"></script>
17
+ <link rel="stylesheet" crossorigin href="/assets/index.css">
18
+ </head>
19
+ <body>
20
+ <div id="root"></div>
21
+ </body>
22
+ </html>
@@ -89,5 +89,15 @@
89
89
  }
90
90
  }
91
91
  }
92
+ },
93
+ "tenant": {
94
+ "selector": {
95
+ "add": "Voeg nieuwe toe"
96
+ },
97
+ "setup": {
98
+ "title": "Eerste {{tenant}} aanmaken",
99
+ "model_not_found": "Geen admin model gevonden voor {{tenant}}. Voeg een admin model toe of maak handmatig een {{tenant}} aan."
100
+ },
101
+ "shared_message": "{{content_type}} worden gedeeld tussen alle {{tenant}}."
92
102
  }
93
103
  }
@@ -15,8 +15,8 @@
15
15
  <link rel="stylesheet" crossorigin href="{% static 'content_studio/icons/pi/style.css' %}">
16
16
 
17
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">
18
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
19
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,100..700;1,100..700&display=swap" rel="stylesheet">
20
20
  </head>
21
21
  <body>
22
22
  <div id="root"></div>
content_studio/utils.py CHANGED
@@ -3,6 +3,8 @@ import sys
3
3
 
4
4
  from rich.console import Console
5
5
 
6
+ from content_studio.settings import cs_settings
7
+
6
8
  console = Console()
7
9
 
8
10
 
@@ -60,3 +62,28 @@ def get_related_field_name(inline, parent_model):
60
62
  )
61
63
  else:
62
64
  raise ValueError(f"Multiple foreign keys found. Specify fk_name on the inline.")
65
+
66
+
67
+ def get_tenant_field_name(model):
68
+ tenant_model = cs_settings.TENANT_MODEL
69
+
70
+ if not tenant_model:
71
+ return None
72
+
73
+ opts = model._meta
74
+
75
+ # Find all foreign keys pointing to the tenant model
76
+ fks = [
77
+ f
78
+ for f in opts.get_fields()
79
+ if f.many_to_one and f.remote_field.model == tenant_model
80
+ ]
81
+
82
+ if len(fks) == 1:
83
+ return fks[0].name
84
+ elif len(fks) == 0:
85
+ return None
86
+ else:
87
+ raise ValueError(
88
+ f"Multiple fields found pointing to {tenant_model}. Only one field can point to a tenant model."
89
+ )
content_studio/views.py CHANGED
@@ -3,7 +3,9 @@ from django.contrib import admin
3
3
  from django.urls import reverse, NoReverseMatch
4
4
  from django.utils.translation import gettext_lazy as _
5
5
  from django.views.generic import TemplateView
6
+ from rest_framework import serializers
6
7
  from rest_framework.decorators import action
8
+ from rest_framework.exceptions import MethodNotAllowed
7
9
  from rest_framework.permissions import IsAdminUser, AllowAny
8
10
  from rest_framework.renderers import JSONRenderer
9
11
  from rest_framework.response import Response
@@ -31,6 +33,7 @@ class AdminApiViewSet(ViewSet):
31
33
 
32
34
  permission_classes = [IsAdminUser]
33
35
  renderer_classes = [JSONRenderer]
36
+ admin_site = cs_settings.ADMIN_SITE
34
37
 
35
38
  @action(
36
39
  methods=["get"],
@@ -42,23 +45,22 @@ class AdminApiViewSet(ViewSet):
42
45
  """
43
46
  Returns public information about the Content Studio admin.
44
47
  """
45
- admin_site = cs_settings.ADMIN_SITE
46
48
 
47
49
  data = {
48
50
  "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,
51
+ "site_header": self.admin_site.site_header,
52
+ "site_title": self.admin_site.site_title,
53
+ "index_title": self.admin_site.index_title,
54
+ "site_url": self.admin_site.site_url,
53
55
  "health_check": get_health_check_path(),
54
56
  "login_backends": [
55
57
  backend.get_info()
56
- for backend in admin_site.login_backend.active_backends
58
+ for backend in self.admin_site.login_backend.active_backends
57
59
  ],
58
- "token_backend": admin_site.token_backend.active_backend.get_info(),
60
+ "token_backend": self.admin_site.token_backend.active_backend.get_info(),
59
61
  "formats": {
60
62
  model_class.__name__: frmt.serialize()
61
- for model_class, frmt in admin_site.default_format_mapping.items()
63
+ for model_class, frmt in self.admin_site.default_format_mapping.items()
62
64
  },
63
65
  "widgets": get_widgets(),
64
66
  "settings": {
@@ -83,7 +85,6 @@ class AdminApiViewSet(ViewSet):
83
85
  """
84
86
  Returns information about the Django app (models, admin models, admin site, settings, etc.).
85
87
  """
86
- admin_site = cs_settings.ADMIN_SITE
87
88
  data = {
88
89
  "models": get_models(request),
89
90
  "model_groups": get_model_groups(),
@@ -102,11 +103,20 @@ class AdminApiViewSet(ViewSet):
102
103
  },
103
104
  }
104
105
 
105
- if admin_site.dashboard:
106
- data["dashboard"] = admin_site.dashboard.serialize()
106
+ if self.admin_site.dashboard:
107
+ data["dashboard"] = self.admin_site.dashboard.serialize()
107
108
  else:
108
109
  data["dashboard"] = {"widgets": []}
109
110
 
111
+ multitenancy = cs_settings.TENANT_MODEL is not None
112
+
113
+ data["multitenancy"] = {
114
+ "enabled": multitenancy,
115
+ "tenant_model": (
116
+ cs_settings.TENANT_MODEL._meta.label_lower if multitenancy else None
117
+ ),
118
+ }
119
+
110
120
  return Response(data=data)
111
121
 
112
122
  @action(methods=["get"], detail=False, url_path="me")
@@ -116,6 +126,28 @@ class AdminApiViewSet(ViewSet):
116
126
  """
117
127
  return Response(SessionUserSerializer(request.user).data)
118
128
 
129
+ @action(
130
+ methods=["get"],
131
+ detail=False,
132
+ url_path="tenants",
133
+ )
134
+ def list_tenants(self, request):
135
+ tenant_model = cs_settings.TENANT_MODEL
136
+
137
+ if not tenant_model:
138
+ raise MethodNotAllowed("GET", "Tenant model not defined.")
139
+
140
+ class TenantSerializer(serializers.ModelSerializer):
141
+ class Meta:
142
+ model = tenant_model
143
+ fields = ["id", "__str__"]
144
+
145
+ tenants = self.admin_site.get_tenants(
146
+ tenant_model=tenant_model, request=request
147
+ )
148
+
149
+ return Response(TenantSerializer(tenants, many=True).data)
150
+
119
151
 
120
152
  def get_models(request):
121
153
  models = []
@@ -18,6 +18,7 @@ from rest_framework.viewsets import ModelViewSet
18
18
  from .filters import LookupFilter
19
19
  from .serializers import RelatedItemSerializer
20
20
  from .settings import cs_settings
21
+ from .utils import get_tenant_field_name
21
22
 
22
23
 
23
24
  class BaseModelViewSet(ModelViewSet):
@@ -48,10 +49,15 @@ class BaseModelViewSet(ModelViewSet):
48
49
 
49
50
  def perform_create(self, serializer):
50
51
  instance = serializer.save()
52
+ tenant_id = self.request.headers.get("x-dcs-tenant", None)
53
+ tenant_model = cs_settings.TENANT_MODEL
54
+ tenant_field_name = get_tenant_field_name(instance)
55
+
56
+ if tenant_model and tenant_id and tenant_field_name:
57
+ setattr(instance, f"{tenant_field_name}_id", tenant_id)
51
58
 
52
59
  if hasattr(instance, cs_settings.CREATED_BY_ATTR):
53
60
  setattr(instance, cs_settings.CREATED_BY_ATTR, self.request.user)
54
- instance.save()
55
61
 
56
62
  content_type = ContentType.objects.get_for_model(instance)
57
63
  LogEntry.objects.create(
@@ -63,6 +69,8 @@ class BaseModelViewSet(ModelViewSet):
63
69
  change_message="",
64
70
  )
65
71
 
72
+ instance.save()
73
+
66
74
  def perform_update(self, serializer):
67
75
  instance = serializer.save()
68
76
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-content-studio
3
- Version: 1.0.0b7
3
+ Version: 1.0.0b9
4
4
  Summary: Modern & flexible Django admin
5
5
  License: MIT
6
6
  Author: Leon van der Grient
@@ -1,6 +1,6 @@
1
- content_studio/__init__.py,sha256=1DQjlSwKeqpDwC5dpxWzCEFZBu9tZ9AtHbS0LjcoYU0,161
2
- content_studio/admin.py,sha256=BpPdw4g0uQMPPgNObxQzVGbTNzZWDc1L0L3gI_WA8WI,10260
3
- content_studio/apps.py,sha256=cfZvEixECgLN0g2zbu_Y94u5k3QBMyr_bgsgj4qYuoA,3503
1
+ content_studio/__init__.py,sha256=j-a7TDER2hDMaI7k6MsRg0w5sVTbEEL6SpnfDdgn9nw,161
2
+ content_studio/admin.py,sha256=bsGQvHndTHmlFUgsBgWiDk9YOEz5i7duHwJk73CSdyk,11180
3
+ content_studio/apps.py,sha256=iGZYxsGerYYC8EmV9Wu9phuT9R8UHUkIQ6XAm94EUgA,3961
4
4
  content_studio/dashboard/__init__.py,sha256=1GQcAuM5IlTlvqq6aPYA_QfuiEy_It0qx54VqDMQHCQ,1788
5
5
  content_studio/dashboard/activity_log.py,sha256=Yr5F7wRFBnT7RlvWJb9gdwYZzEuEeQV4dhc6YXDCOPY,859
6
6
  content_studio/filters.py,sha256=GyglR2E_wswomW7EnfShEXr14zxuRlLAuxrHVO3QQYg,4335
@@ -10,32 +10,33 @@ content_studio/login_backends/__init__.py,sha256=hgPJh9hFobubImfhynaeZ_dku3sjqNQ
10
10
  content_studio/login_backends/username_password.py,sha256=JJbF3oWnhOwLs-WaqclCCH6nAnrhlKbyBUzAJKVtq7A,2561
11
11
  content_studio/media_library/serializers.py,sha256=1MPFqbZdE0iTYqZrzzYoT_y4q0ItGTbQ-I7omyojAZo,660
12
12
  content_studio/media_library/viewsets.py,sha256=XsQuCrcfZ52xvvsEbHZ2WTI99wf-cheOKfaMJyfAJbo,4318
13
- content_studio/models.py,sha256=SLcgMIXu4FqwbQJBQ-jLw7_hlGjbSmwHmyKfzDsj-2M,2068
13
+ content_studio/models.py,sha256=9F9C-K7KkVT6i5TZGm0SEPyRxaIMlVblrZwzGET199E,2191
14
14
  content_studio/paginators.py,sha256=XrA6ECP2pO75SSj0Mi0Jqj7n_YPuIin-V8_6EOaQj64,589
15
15
  content_studio/router.py,sha256=7Up_sipGaUDoY6ElJNRf85ADaYfJCWV4To523L4LGuw,393
16
16
  content_studio/serializers.py,sha256=tWgL7J2z-Qc5BQjMc3PXpvLZa_6J6MfVUqRDOK_X0Xs,3867
17
- content_studio/settings.py,sha256=6U0o-DxwpFQo-1LLumr3U4FCVTHiBzySK7M77HWvpf4,4510
17
+ content_studio/settings.py,sha256=uu5Pnb502ZQE328J9-3Q4UtLGvD6tTAOOgv652dVUig,4598
18
18
  content_studio/static/content_studio/assets/browser-ponyfill-TyWUZ1Oq.js,sha256=sKLpD8vGwLfnJVLaSGMMi8krArcm2Qj3-igVwDvLMek,10287
19
- content_studio/static/content_studio/assets/index.css,sha256=z4UGhtkbCGeVEL3ivv3f_OrQrWDxZwEGnHPbf4LLoow,84776
20
- content_studio/static/content_studio/assets/index.js,sha256=uhXt8yo_BqDpwaUD3xQsiiYaHwqCzIaw-neWLmjk-vg,1411138
19
+ content_studio/static/content_studio/assets/index.css,sha256=5ppY6CCbxK-0cxchbw3j6JVwZgSKUPorB_8ynR87OTY,86894
20
+ content_studio/static/content_studio/assets/index.js,sha256=W48he-x3ApBAspmg_38FYGRQPHL7Td4oSJ-S2BUqzsE,1419730
21
21
  content_studio/static/content_studio/icons/pi/Phosphor-Bold.svg,sha256=4kdzmyaGcNVhK6qRIA8PdhaZ7raHU0xoU26ht52VG_c,2967217
22
22
  content_studio/static/content_studio/icons/pi/Phosphor-Bold.ttf,sha256=EKChy0-BVqQg-fhM80xOmHHljtLd6h9qgHmtByQ6f7I,495308
23
23
  content_studio/static/content_studio/icons/pi/Phosphor-Bold.woff,sha256=3k3MnRjPMzY8GdLJJMnZ1R4hCXi9Pm7FCK7HFnCz2wY,495388
24
24
  content_studio/static/content_studio/icons/pi/Phosphor-Bold.woff2,sha256=IVO1LOqeBvwMyElgSL5IWzgaD4vxMK3qWpdW_I8QC4c,150052
25
25
  content_studio/static/content_studio/icons/pi/style.css,sha256=yKMt9n-L1X9wxjceFewjLfJd3ro-uQYNeqpoEBps4kA,85821
26
26
  content_studio/static/content_studio/img/media_placeholder.svg,sha256=ZLrfeqvaC5YwuHAg_7YJXLhYzLz2azVcKqCLEGOVTTs,3253
27
+ content_studio/static/content_studio/index.html,sha256=IuVbv9-sX3WUUr7mDDEAVG9Q1tKmsHoZMpZwaWJ9SNM,896
27
28
  content_studio/static/content_studio/locales/en/translation.json,sha256=aYJThaN5db_1Eo5YeeDpqxhf5gohtOL12yG4FF4CaRI,2524
28
- content_studio/static/content_studio/locales/nl/translation.json,sha256=hB9oauYHeGvVDKBQn52sITNSMSGmcJL0TilfaH6Qv5c,2776
29
+ content_studio/static/content_studio/locales/nl/translation.json,sha256=GUuxX_rLKQll_gBe6dSpNb1etF7p7EuF-gJNJzqd4v8,3131
29
30
  content_studio/static/content_studio/vite.svg,sha256=SnSK_UQ5GLsWWRyDTEAdrjPoeGGrXbrQgRw6O0qSFPs,1497
30
- content_studio/templates/content_studio/index.html,sha256=zGS8iro3w1HLs5hLbDRTrchB50rAaFqXbTMu7hdVz6E,1193
31
+ content_studio/templates/content_studio/index.html,sha256=As2C1ahUKDvVy4kTFU0gHTy9mIL82817NUSSf93GC_Q,1032
31
32
  content_studio/token_backends/__init__.py,sha256=dO3aWIHXX8He399ZEvlS4fNgyL84OY9TEtkva9b5N5Q,1183
32
33
  content_studio/token_backends/jwt.py,sha256=niGCpRqaUVhhS9haXfH1uFlg2v8NLB5IpsJ4N79Q0Gg,1565
33
34
  content_studio/urls.py,sha256=EY7lbzC0Q5vLfvqE3rK_4hmDrXhTuxKzYC55cc5tEEo,701
34
- content_studio/utils.py,sha256=xOolXd9Zmty6B6_2febvM8TmZhZ0JRc2nCLj3VooHkM,1488
35
- content_studio/views.py,sha256=GbANmQY_VyzQPD4Hw3MEfWF3pOr2G6a61Ktvu2Bw6d8,5370
36
- content_studio/viewsets.py,sha256=ThTM9J7ecicS0FqVTZqn6UqegSxiQjP3NbPxhL58v7E,5433
35
+ content_studio/utils.py,sha256=dGiYCixg-qcPGbF4hV6fts1Vv4ED8gRi8syLuSS4xzw,2123
36
+ content_studio/views.py,sha256=D5-o5pcj3RHuGyGB7S8fXAXoccRqdFpQEZZQ6sn7TvE,6361
37
+ content_studio/viewsets.py,sha256=iLhV6A_dl64-Ui4_DDT1Gje9ygvu0dYtF4OAO90MHSQ,5776
37
38
  content_studio/widgets.py,sha256=OlFKCAYNhkj2Ww9S_hvQTzUjcSnYXocmi04zusKrOTo,1071
38
- django_content_studio-1.0.0b7.dist-info/LICENSE,sha256=Wnx2EJhtSNnXE5Qs80i1HTBNFZTi8acEtC5TYqtFlnQ,1075
39
- django_content_studio-1.0.0b7.dist-info/METADATA,sha256=Gsmv5-hb25W-9IMHTm7OxBAEsNYcHqZR9VmWeIGScXo,2512
40
- django_content_studio-1.0.0b7.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
41
- django_content_studio-1.0.0b7.dist-info/RECORD,,
39
+ django_content_studio-1.0.0b9.dist-info/LICENSE,sha256=Wnx2EJhtSNnXE5Qs80i1HTBNFZTi8acEtC5TYqtFlnQ,1075
40
+ django_content_studio-1.0.0b9.dist-info/METADATA,sha256=3SczieOeKnS8eZ2sukLXH5PajFqprw2b2KVX-LfStcI,2512
41
+ django_content_studio-1.0.0b9.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
42
+ django_content_studio-1.0.0b9.dist-info/RECORD,,