django-var-cms 1.0.0__tar.gz
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.
- django_var_cms-1.0.0/PKG-INFO +16 -0
- django_var_cms-1.0.0/pyproject.toml +34 -0
- django_var_cms-1.0.0/var_cms/__init__.py +1 -0
- django_var_cms-1.0.0/var_cms/apps.py +19 -0
- django_var_cms-1.0.0/var_cms/media_utils/__init__.py +0 -0
- django_var_cms-1.0.0/var_cms/media_utils/convert.py +140 -0
- django_var_cms-1.0.0/var_cms/media_utils/crop.py +91 -0
- django_var_cms-1.0.0/var_cms/permissions.py +123 -0
- django_var_cms-1.0.0/var_cms/registry.py +834 -0
- django_var_cms-1.0.0/var_cms/static/var_cms/var.png +0 -0
- django_var_cms-1.0.0/var_cms/templates/var_cms/about.html +79 -0
- django_var_cms-1.0.0/var_cms/templates/var_cms/base.html +1060 -0
- django_var_cms-1.0.0/var_cms/templates/var_cms/change_password.html +61 -0
- django_var_cms-1.0.0/var_cms/templates/var_cms/delete.html +30 -0
- django_var_cms-1.0.0/var_cms/templates/var_cms/detail.html +41 -0
- django_var_cms-1.0.0/var_cms/templates/var_cms/forgot_password.html +255 -0
- django_var_cms-1.0.0/var_cms/templates/var_cms/forgot_password_verify.html +271 -0
- django_var_cms-1.0.0/var_cms/templates/var_cms/form.html +235 -0
- django_var_cms-1.0.0/var_cms/templates/var_cms/index.html +198 -0
- django_var_cms-1.0.0/var_cms/templates/var_cms/list.html +175 -0
- django_var_cms-1.0.0/var_cms/templates/var_cms/login.html +248 -0
- django_var_cms-1.0.0/var_cms/templates/var_cms/otp_verify.html +234 -0
- django_var_cms-1.0.0/var_cms/templatetags/__init__.py +0 -0
- django_var_cms-1.0.0/var_cms/templatetags/cms_tags.py +56 -0
- django_var_cms-1.0.0/var_cms/templatetags/var_cms_tags.py +94 -0
- django_var_cms-1.0.0/var_cms/urls.py +5 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-var-cms
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A modern, role-based Django CMS registry with rich media previews
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: django>=5.0
|
|
7
|
+
Requires-Dist: pillow>=10.0
|
|
8
|
+
Requires-Dist: whitenoise>=6.6
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: django-debug-toolbar>=4.0; extra == 'dev'
|
|
11
|
+
Provides-Extra: geo
|
|
12
|
+
Requires-Dist: django[gis]>=5.0; extra == 'geo'
|
|
13
|
+
Provides-Extra: pdf
|
|
14
|
+
Requires-Dist: pdf2image>=1.16; extra == 'pdf'
|
|
15
|
+
Provides-Extra: tailwind
|
|
16
|
+
Requires-Dist: django-tailwind-cli>=2.0; extra == 'tailwind'
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "django-var-cms"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "A modern, role-based Django CMS registry with rich media previews"
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
|
|
7
|
+
dependencies = [
|
|
8
|
+
"django>=5.0",
|
|
9
|
+
"pillow>=10.0",
|
|
10
|
+
"whitenoise>=6.6",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[project.optional-dependencies]
|
|
14
|
+
tailwind = ["django-tailwind-cli>=2.0"]
|
|
15
|
+
geo = ["django[gis]>=5.0"]
|
|
16
|
+
pdf = ["pdf2image>=1.16"]
|
|
17
|
+
dev = ["django-debug-toolbar>=4.0"]
|
|
18
|
+
|
|
19
|
+
[tool.hatch.build.targets.sdist]
|
|
20
|
+
include = [
|
|
21
|
+
"/var_cms",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[tool.hatch.build.targets.wheel]
|
|
25
|
+
packages = ["var_cms"]
|
|
26
|
+
|
|
27
|
+
[dependency-groups]
|
|
28
|
+
dev = ["django-debug-toolbar>=4.0"]
|
|
29
|
+
|
|
30
|
+
[build-system]
|
|
31
|
+
requires = ["hatchling"]
|
|
32
|
+
build-backend = "hatchling.build"
|
|
33
|
+
|
|
34
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
default_app_config = "var_cms.apps.VarCmsConfig"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from django.apps import AppConfig
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class VarCmsConfig(AppConfig):
|
|
5
|
+
name = "var_cms"
|
|
6
|
+
verbose_name = "VAR CMS"
|
|
7
|
+
|
|
8
|
+
def ready(self):
|
|
9
|
+
from django.apps import apps
|
|
10
|
+
for app_config in apps.get_app_configs():
|
|
11
|
+
try:
|
|
12
|
+
__import__(f"{app_config.name}.var_cms_admin")
|
|
13
|
+
except ModuleNotFoundError:
|
|
14
|
+
pass
|
|
15
|
+
except Exception as e:
|
|
16
|
+
import logging
|
|
17
|
+
logging.getLogger(__name__).warning(
|
|
18
|
+
"var_cms_admin import error in %s: %s", app_config.name, e
|
|
19
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""
|
|
2
|
+
var_cms/media_utils/convert.py
|
|
3
|
+
==============================
|
|
4
|
+
File format converter.
|
|
5
|
+
|
|
6
|
+
Supported conversions:
|
|
7
|
+
Images — jpeg ↔ png ↔ webp ↔ bmp ↔ tiff ↔ gif
|
|
8
|
+
Audio — mp3 ↔ wav ↔ ogg ↔ flac ↔ aac (requires ffmpeg)
|
|
9
|
+
Video — mp4 ↔ webm ↔ avi ↔ mov (requires ffmpeg)
|
|
10
|
+
PDF → images (page-by-page PNG) (requires pdf2image + poppler)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import io
|
|
14
|
+
import os
|
|
15
|
+
import subprocess
|
|
16
|
+
import tempfile
|
|
17
|
+
import uuid
|
|
18
|
+
|
|
19
|
+
from django.conf import settings
|
|
20
|
+
from django.core.files.base import ContentFile
|
|
21
|
+
from django.core.files.storage import default_storage
|
|
22
|
+
from django.http import JsonResponse
|
|
23
|
+
|
|
24
|
+
IMAGE_FORMATS = {"jpeg", "jpg", "png", "webp", "bmp", "tiff", "gif"}
|
|
25
|
+
AUDIO_FORMATS = {"mp3", "wav", "ogg", "flac", "aac", "m4a"}
|
|
26
|
+
VIDEO_FORMATS = {"mp4", "webm", "avi", "mov", "mkv"}
|
|
27
|
+
DOC_FORMATS = {"pdf"}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _abs(file_path: str) -> str:
|
|
31
|
+
clean = file_path.lstrip("/").replace(settings.MEDIA_URL.lstrip("/"), "", 1)
|
|
32
|
+
return os.path.join(settings.MEDIA_ROOT, clean)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _save(data: bytes, ext: str) -> dict:
|
|
36
|
+
name = f"converted/{uuid.uuid4().hex[:12]}.{ext}"
|
|
37
|
+
path = default_storage.save(name, ContentFile(data))
|
|
38
|
+
return {"url": default_storage.url(path), "path": path}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def handle_convert(request):
|
|
42
|
+
"""
|
|
43
|
+
POST params:
|
|
44
|
+
file_path — relative media path of source file
|
|
45
|
+
target_fmt — desired output extension (e.g. "webp", "mp3", "mp4")
|
|
46
|
+
"""
|
|
47
|
+
file_path = request.POST.get("file_path", "")
|
|
48
|
+
target_fmt = request.POST.get("target_fmt", "").lower().lstrip(".")
|
|
49
|
+
|
|
50
|
+
if not file_path or not target_fmt:
|
|
51
|
+
return JsonResponse({"error": "file_path and target_fmt required"}, status=400)
|
|
52
|
+
|
|
53
|
+
abs_path = _abs(file_path)
|
|
54
|
+
if not os.path.exists(abs_path):
|
|
55
|
+
return JsonResponse({"error": "File not found"}, status=404)
|
|
56
|
+
|
|
57
|
+
src_ext = abs_path.rsplit(".", 1)[-1].lower()
|
|
58
|
+
|
|
59
|
+
# ── Image conversion ──────────────────────────────────────────────────────
|
|
60
|
+
if src_ext in IMAGE_FORMATS and target_fmt in IMAGE_FORMATS:
|
|
61
|
+
return _convert_image(abs_path, target_fmt)
|
|
62
|
+
|
|
63
|
+
# ── Audio / Video conversion (ffmpeg) ─────────────────────────────────────
|
|
64
|
+
if src_ext in (AUDIO_FORMATS | VIDEO_FORMATS) or target_fmt in (AUDIO_FORMATS | VIDEO_FORMATS):
|
|
65
|
+
return _convert_ffmpeg(abs_path, target_fmt)
|
|
66
|
+
|
|
67
|
+
# ── PDF → images ──────────────────────────────────────────────────────────
|
|
68
|
+
if src_ext == "pdf" and target_fmt in ("png", "jpg", "jpeg"):
|
|
69
|
+
return _convert_pdf(abs_path, target_fmt)
|
|
70
|
+
|
|
71
|
+
return JsonResponse({"error": f"Unsupported conversion: {src_ext} → {target_fmt}"}, status=400)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _convert_image(abs_path: str, target_fmt: str) -> JsonResponse:
|
|
75
|
+
try:
|
|
76
|
+
from PIL import Image
|
|
77
|
+
except ImportError:
|
|
78
|
+
return JsonResponse({"error": "Pillow not installed"}, status=500)
|
|
79
|
+
|
|
80
|
+
fmt_map = {"jpg": "JPEG", "jpeg": "JPEG", "png": "PNG",
|
|
81
|
+
"webp": "WEBP", "bmp": "BMP", "tiff": "TIFF", "gif": "GIF"}
|
|
82
|
+
pil_fmt = fmt_map.get(target_fmt, target_fmt.upper())
|
|
83
|
+
|
|
84
|
+
img = Image.open(abs_path)
|
|
85
|
+
if pil_fmt == "JPEG" and img.mode in ("RGBA", "P"):
|
|
86
|
+
img = img.convert("RGB")
|
|
87
|
+
|
|
88
|
+
buf = io.BytesIO()
|
|
89
|
+
img.save(buf, format=pil_fmt, quality=90)
|
|
90
|
+
buf.seek(0)
|
|
91
|
+
ext = "jpg" if target_fmt in ("jpg", "jpeg") else target_fmt
|
|
92
|
+
return JsonResponse(_save(buf.read(), ext) | {"type": "image"})
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _convert_ffmpeg(abs_path: str, target_fmt: str) -> JsonResponse:
|
|
96
|
+
if subprocess.run(["which", "ffmpeg"], capture_output=True).returncode != 0:
|
|
97
|
+
return JsonResponse({
|
|
98
|
+
"error": "ffmpeg not found. Install with: sudo apt install ffmpeg",
|
|
99
|
+
"install_hint": "sudo apt install ffmpeg"
|
|
100
|
+
}, status=500)
|
|
101
|
+
|
|
102
|
+
with tempfile.NamedTemporaryFile(suffix=f".{target_fmt}", delete=False) as tmp:
|
|
103
|
+
out_path = tmp.name
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
cmd = ["ffmpeg", "-y", "-i", abs_path, out_path]
|
|
107
|
+
result = subprocess.run(cmd, capture_output=True, timeout=120)
|
|
108
|
+
if result.returncode != 0:
|
|
109
|
+
err = result.stderr.decode(errors="replace")[-500:]
|
|
110
|
+
return JsonResponse({"error": f"ffmpeg error: {err}"}, status=500)
|
|
111
|
+
|
|
112
|
+
with open(out_path, "rb") as f:
|
|
113
|
+
data = f.read()
|
|
114
|
+
|
|
115
|
+
media_type = "audio" if target_fmt in AUDIO_FORMATS else "video"
|
|
116
|
+
return JsonResponse(_save(data, target_fmt) | {"type": media_type})
|
|
117
|
+
finally:
|
|
118
|
+
if os.path.exists(out_path):
|
|
119
|
+
os.unlink(out_path)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _convert_pdf(abs_path: str, target_fmt: str) -> JsonResponse:
|
|
123
|
+
try:
|
|
124
|
+
from pdf2image import convert_from_path
|
|
125
|
+
except ImportError:
|
|
126
|
+
return JsonResponse({
|
|
127
|
+
"error": "pdf2image not installed. Run: uv add pdf2image",
|
|
128
|
+
"install_hint": "uv add pdf2image"
|
|
129
|
+
}, status=500)
|
|
130
|
+
|
|
131
|
+
images = convert_from_path(abs_path, dpi=150)
|
|
132
|
+
urls = []
|
|
133
|
+
for i, img in enumerate(images):
|
|
134
|
+
buf = io.BytesIO()
|
|
135
|
+
img.save(buf, format="PNG")
|
|
136
|
+
buf.seek(0)
|
|
137
|
+
result = _save(buf.read(), "png")
|
|
138
|
+
urls.append(result["url"])
|
|
139
|
+
|
|
140
|
+
return JsonResponse({"type": "pdf_pages", "pages": urls, "count": len(urls)})
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
var_cms/media_utils/crop.py
|
|
3
|
+
===========================
|
|
4
|
+
Image crop handler — receives crop coordinates from the frontend Cropper.js
|
|
5
|
+
widget and returns a new cropped image saved to MEDIA_ROOT.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import io
|
|
9
|
+
import os
|
|
10
|
+
import uuid
|
|
11
|
+
|
|
12
|
+
from django.conf import settings
|
|
13
|
+
from django.core.files.base import ContentFile
|
|
14
|
+
from django.core.files.storage import default_storage
|
|
15
|
+
from django.http import JsonResponse
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def handle_crop(request):
|
|
19
|
+
"""
|
|
20
|
+
POST params:
|
|
21
|
+
file_path — relative media path of the source image
|
|
22
|
+
x, y, w, h — crop box in pixels
|
|
23
|
+
rotate — rotation degrees (optional, default 0)
|
|
24
|
+
scale_x — horizontal flip (-1 or 1)
|
|
25
|
+
scale_y — vertical flip (-1 or 1)
|
|
26
|
+
format — output format: jpeg | png | webp (default: same as source)
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
from PIL import Image
|
|
30
|
+
except ImportError:
|
|
31
|
+
return JsonResponse({"error": "Pillow not installed. Run: uv add pillow"}, status=500)
|
|
32
|
+
|
|
33
|
+
file_path = request.POST.get("file_path", "")
|
|
34
|
+
if not file_path:
|
|
35
|
+
return JsonResponse({"error": "file_path required"}, status=400)
|
|
36
|
+
|
|
37
|
+
# Sanitise — must be inside MEDIA_ROOT
|
|
38
|
+
abs_path = os.path.join(settings.MEDIA_ROOT, file_path.lstrip("/").replace(settings.MEDIA_URL.lstrip("/"), "", 1))
|
|
39
|
+
if not os.path.exists(abs_path):
|
|
40
|
+
return JsonResponse({"error": "File not found"}, status=404)
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
x = int(float(request.POST.get("x", 0)))
|
|
44
|
+
y = int(float(request.POST.get("y", 0)))
|
|
45
|
+
w = int(float(request.POST.get("w", 0)))
|
|
46
|
+
h = int(float(request.POST.get("h", 0)))
|
|
47
|
+
rotate = float(request.POST.get("rotate", 0))
|
|
48
|
+
scale_x = float(request.POST.get("scale_x", 1))
|
|
49
|
+
scale_y = float(request.POST.get("scale_y", 1))
|
|
50
|
+
out_fmt = request.POST.get("format", "").upper()
|
|
51
|
+
except (ValueError, TypeError) as e:
|
|
52
|
+
return JsonResponse({"error": f"Invalid params: {e}"}, status=400)
|
|
53
|
+
|
|
54
|
+
img = Image.open(abs_path).convert("RGBA")
|
|
55
|
+
|
|
56
|
+
# Flip
|
|
57
|
+
if scale_x == -1:
|
|
58
|
+
img = img.transpose(Image.FLIP_LEFT_RIGHT)
|
|
59
|
+
if scale_y == -1:
|
|
60
|
+
img = img.transpose(Image.FLIP_TOP_BOTTOM)
|
|
61
|
+
|
|
62
|
+
# Rotate (expand=True keeps full image)
|
|
63
|
+
if rotate:
|
|
64
|
+
img = img.rotate(-rotate, expand=True)
|
|
65
|
+
|
|
66
|
+
# Crop
|
|
67
|
+
if w > 0 and h > 0:
|
|
68
|
+
img = img.crop((x, y, x + w, y + h))
|
|
69
|
+
|
|
70
|
+
# Determine output format
|
|
71
|
+
orig_ext = abs_path.rsplit(".", 1)[-1].upper()
|
|
72
|
+
fmt_map = {"JPG": "JPEG", "JPEG": "JPEG", "PNG": "PNG", "WEBP": "WEBP"}
|
|
73
|
+
save_fmt = fmt_map.get(out_fmt) or fmt_map.get(orig_ext) or "PNG"
|
|
74
|
+
|
|
75
|
+
# JPEG can't have alpha
|
|
76
|
+
if save_fmt == "JPEG":
|
|
77
|
+
img = img.convert("RGB")
|
|
78
|
+
|
|
79
|
+
ext_map = {"JPEG": "jpg", "PNG": "png", "WEBP": "webp"}
|
|
80
|
+
ext = ext_map.get(save_fmt, "png")
|
|
81
|
+
|
|
82
|
+
buf = io.BytesIO()
|
|
83
|
+
img.save(buf, format=save_fmt, quality=90)
|
|
84
|
+
buf.seek(0)
|
|
85
|
+
|
|
86
|
+
# Save alongside originals
|
|
87
|
+
new_name = f"crops/{uuid.uuid4().hex[:12]}.{ext}"
|
|
88
|
+
saved_path = default_storage.save(new_name, ContentFile(buf.read()))
|
|
89
|
+
url = default_storage.url(saved_path)
|
|
90
|
+
|
|
91
|
+
return JsonResponse({"url": url, "path": saved_path, "format": save_fmt, "size": [img.width, img.height]})
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""
|
|
2
|
+
var_cms/permissions.py
|
|
3
|
+
======================
|
|
4
|
+
Role-based permission system for django-var-cms.
|
|
5
|
+
|
|
6
|
+
RolePermission — defines what a named role can do
|
|
7
|
+
GroupPermission — same but matches Django auth groups
|
|
8
|
+
UserPermission — per-user overrides (highest priority)
|
|
9
|
+
|
|
10
|
+
Resolution order: UserPermission > GroupPermission > RolePermission > deny
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import Dict, List, Optional, Union
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
ACTIONS = ("add", "list", "view", "edit", "delete")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class RolePermission:
|
|
23
|
+
"""
|
|
24
|
+
Map a role name (string) to a set of allowed actions.
|
|
25
|
+
|
|
26
|
+
The role name is matched against:
|
|
27
|
+
1. "superuser" — request.user.is_superuser
|
|
28
|
+
2. Django group names — request.user.groups
|
|
29
|
+
3. Custom role logic — override VarCMSModelAdmin._get_user_role()
|
|
30
|
+
"""
|
|
31
|
+
role: str
|
|
32
|
+
add: bool = False
|
|
33
|
+
list: bool = True
|
|
34
|
+
view: bool = True
|
|
35
|
+
edit: bool = False
|
|
36
|
+
delete: bool = False
|
|
37
|
+
|
|
38
|
+
def allows(self, action: str) -> bool:
|
|
39
|
+
return bool(getattr(self, action, False))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class GroupPermission(RolePermission):
|
|
44
|
+
"""Alias — identical to RolePermission, role matches a Django group name."""
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class UserPermission:
|
|
50
|
+
"""
|
|
51
|
+
Per-user permission override (matched by username or user pk).
|
|
52
|
+
Takes priority over all role/group permissions.
|
|
53
|
+
"""
|
|
54
|
+
username: str
|
|
55
|
+
add: bool = False
|
|
56
|
+
list: bool = True
|
|
57
|
+
view: bool = True
|
|
58
|
+
edit: bool = False
|
|
59
|
+
delete: bool = False
|
|
60
|
+
|
|
61
|
+
def allows(self, action: str) -> bool:
|
|
62
|
+
return bool(getattr(self, action, False))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def resolve_permission(
|
|
66
|
+
permissions: List[Union[RolePermission, UserPermission]],
|
|
67
|
+
role: str,
|
|
68
|
+
action: str,
|
|
69
|
+
username: str = "",
|
|
70
|
+
) -> bool:
|
|
71
|
+
"""
|
|
72
|
+
Walk the permission list and return True if allowed.
|
|
73
|
+
UserPermission (matched by username) takes priority.
|
|
74
|
+
"""
|
|
75
|
+
if action not in ACTIONS:
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
# 1. User-level overrides first
|
|
79
|
+
for perm in permissions:
|
|
80
|
+
if isinstance(perm, UserPermission) and perm.username == username:
|
|
81
|
+
return perm.allows(action)
|
|
82
|
+
|
|
83
|
+
# 2. Role / group match
|
|
84
|
+
for perm in permissions:
|
|
85
|
+
if isinstance(perm, RolePermission) and perm.role == role:
|
|
86
|
+
return perm.allows(action)
|
|
87
|
+
|
|
88
|
+
# 3. Default deny
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def resolve_editable_fields(
|
|
93
|
+
role_editable_fields: Dict[str, Union[List[str], str]],
|
|
94
|
+
role: str,
|
|
95
|
+
) -> Union[List[str], str]:
|
|
96
|
+
"""
|
|
97
|
+
Return the editable fields for a given role.
|
|
98
|
+
Falls back to "__all__" for superuser if not explicitly set.
|
|
99
|
+
"""
|
|
100
|
+
if role in role_editable_fields:
|
|
101
|
+
return role_editable_fields[role]
|
|
102
|
+
if role == "superuser":
|
|
103
|
+
return "__all__"
|
|
104
|
+
# Check wildcard
|
|
105
|
+
if "*" in role_editable_fields:
|
|
106
|
+
return role_editable_fields["*"]
|
|
107
|
+
return [] # deny all edits for unknown roles
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def permission_summary(permissions: List[RolePermission]) -> List[Dict]:
|
|
111
|
+
"""Return a list of dicts for rendering a permission table in templates."""
|
|
112
|
+
return [
|
|
113
|
+
{
|
|
114
|
+
"role": p.role,
|
|
115
|
+
"add": p.add,
|
|
116
|
+
"list": p.list,
|
|
117
|
+
"view": p.view,
|
|
118
|
+
"edit": p.edit,
|
|
119
|
+
"delete": p.delete,
|
|
120
|
+
}
|
|
121
|
+
for p in permissions
|
|
122
|
+
if isinstance(p, (RolePermission, UserPermission))
|
|
123
|
+
]
|