django-pudding 0.1.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_pudding-0.1.0/PKG-INFO +24 -0
- django_pudding-0.1.0/README.md +0 -0
- django_pudding-0.1.0/pyproject.toml +48 -0
- django_pudding-0.1.0/pyproject.toml.orig +49 -0
- django_pudding-0.1.0/src/pudding/__init__.py +0 -0
- django_pudding-0.1.0/src/pudding/actions/__init__.py +9 -0
- django_pudding-0.1.0/src/pudding/actions/base.py +119 -0
- django_pudding-0.1.0/src/pudding/actions/contracts.py +28 -0
- django_pudding-0.1.0/src/pudding/actions/defaults/__init__.py +3 -0
- django_pudding-0.1.0/src/pudding/actions/defaults/add.py +145 -0
- django_pudding-0.1.0/src/pudding/actions/defaults/delete.py +91 -0
- django_pudding-0.1.0/src/pudding/actions/defaults/edit.py +82 -0
- django_pudding-0.1.0/src/pudding/actions/defaults/utils.py +100 -0
- django_pudding-0.1.0/src/pudding/actions/defaults/view.py +76 -0
- django_pudding-0.1.0/src/pudding/actions/registry.py +63 -0
- django_pudding-0.1.0/src/pudding/actions/utils.py +2 -0
- django_pudding-0.1.0/src/pudding/apps.py +7 -0
- django_pudding-0.1.0/src/pudding/domains/__init__.py +4 -0
- django_pudding-0.1.0/src/pudding/domains/base.py +62 -0
- django_pudding-0.1.0/src/pudding/domains/registry.py +56 -0
- django_pudding-0.1.0/src/pudding/exceptions.py +10 -0
- django_pudding-0.1.0/src/pudding/forms.py +30 -0
- django_pudding-0.1.0/src/pudding/py.typed +0 -0
- django_pudding-0.1.0/src/pudding/static/pudding/css/styles.css +2105 -0
- django_pudding-0.1.0/src/pudding/static/pudding/js/bundle.min.js +1813 -0
- django_pudding-0.1.0/src/pudding/static/pudding/js/bundle.min.js.map +7 -0
- django_pudding-0.1.0/src/pudding/templates/pudding/base.html +56 -0
- django_pudding-0.1.0/src/pudding/templates/pudding/components/actions/dropdown.html +17 -0
- django_pudding-0.1.0/src/pudding/templates/pudding/components/form.html +30 -0
- django_pudding-0.1.0/src/pudding/templates/pudding/components/messages.html +41 -0
- django_pudding-0.1.0/src/pudding/templates/pudding/components/overlays/modal.html +35 -0
- django_pudding-0.1.0/src/pudding/templates/pudding/components/sidebar.html +21 -0
- django_pudding-0.1.0/src/pudding/templates/pudding/components/table/_table_empty_state.html +29 -0
- django_pudding-0.1.0/src/pudding/templates/pudding/components/table/header_checkbox.html +20 -0
- django_pudding-0.1.0/src/pudding/templates/pudding/components/table/pagination.html +47 -0
- django_pudding-0.1.0/src/pudding/templates/pudding/components/table/table.html +55 -0
- django_pudding-0.1.0/src/pudding/templates/pudding/components/table/table_cell.html +5 -0
- django_pudding-0.1.0/src/pudding/templates/pudding/components/table/table_controls.html +97 -0
- django_pudding-0.1.0/src/pudding/templates/pudding/components/table/table_row.html +54 -0
- django_pudding-0.1.0/src/pudding/templates/pudding/frames/modal_frame.html +62 -0
- django_pudding-0.1.0/src/pudding/templates/pudding/frames/view_frame.html +36 -0
- django_pudding-0.1.0/src/pudding/templates/pudding/table.html +15 -0
- django_pudding-0.1.0/src/pudding/templates/pudding/table_columns/profile.html +11 -0
- django_pudding-0.1.0/src/pudding/ui/__init__.py +52 -0
- django_pudding-0.1.0/src/pudding/ui/_types.py +14 -0
- django_pudding-0.1.0/src/pudding/ui/breadcrumbs.py +83 -0
- django_pudding-0.1.0/src/pudding/ui/buttons/__init__.py +18 -0
- django_pudding-0.1.0/src/pudding/ui/buttons/base.py +165 -0
- django_pudding-0.1.0/src/pudding/ui/buttons/looks.py +117 -0
- django_pudding-0.1.0/src/pudding/ui/buttons/wrappers/__init__.py +0 -0
- django_pudding-0.1.0/src/pudding/ui/buttons/wrappers/base.py +73 -0
- django_pudding-0.1.0/src/pudding/ui/buttons/wrappers/factories.py +90 -0
- django_pudding-0.1.0/src/pudding/ui/contracts.py +47 -0
- django_pudding-0.1.0/src/pudding/ui/frames/__init__.py +20 -0
- django_pudding-0.1.0/src/pudding/ui/frames/base.py +150 -0
- django_pudding-0.1.0/src/pudding/ui/frames/frame_response.py +77 -0
- django_pudding-0.1.0/src/pudding/ui/frames/modal.py +175 -0
- django_pudding-0.1.0/src/pudding/ui/hooks/__init__.py +3 -0
- django_pudding-0.1.0/src/pudding/ui/hooks/modal.py +17 -0
- django_pudding-0.1.0/src/pudding/ui/sidebar/sidebar.py +23 -0
- django_pudding-0.1.0/src/pudding/ui/tables/__init__.py +3 -0
- django_pudding-0.1.0/src/pudding/ui/tables/base.py +142 -0
- django_pudding-0.1.0/src/pudding/ui/tables/columns/base.py +73 -0
- django_pudding-0.1.0/src/pudding/ui/tables/columns/column_formatters.py +112 -0
- django_pudding-0.1.0/src/pudding/ui/tables/columns/column_renderers.py +114 -0
- django_pudding-0.1.0/src/pudding/ui/tables/table_row.py +61 -0
- django_pudding-0.1.0/src/pudding/ui/utils.py +86 -0
- django_pudding-0.1.0/src/pudding/urls.py +13 -0
- django_pudding-0.1.0/src/pudding/views.py +33 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-pudding
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Declarative Python framework for Django to build dashboards and internal apps fast.
|
|
5
|
+
Author: Zishan Kadri
|
|
6
|
+
Author-email: Zishan Kadri <zishankadri01@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Classifier: Environment :: Web Environment
|
|
9
|
+
Classifier: Framework :: Django
|
|
10
|
+
Classifier: Framework :: Django :: 5.2
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
20
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
21
|
+
Requires-Dist: django>=5.2
|
|
22
|
+
Requires-Python: >=3.12
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
File without changes
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "django-pudding"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Declarative Python framework for Django to build dashboards and internal apps fast."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = ["django>=5.2"]
|
|
8
|
+
classifiers = [
|
|
9
|
+
"Environment :: Web Environment",
|
|
10
|
+
"Framework :: Django",
|
|
11
|
+
"Framework :: Django :: 5.2",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"Operating System :: OS Independent",
|
|
14
|
+
"Programming Language :: Python",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Programming Language :: Python :: 3.14",
|
|
20
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
21
|
+
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
|
22
|
+
]
|
|
23
|
+
license = "MIT"
|
|
24
|
+
|
|
25
|
+
[[project.authors]]
|
|
26
|
+
name = "Zishan Kadri"
|
|
27
|
+
email = "zishankadri01@gmail.com"
|
|
28
|
+
|
|
29
|
+
[build-system]
|
|
30
|
+
requires = ["uv_build>=0.12.3,<0.13.0"]
|
|
31
|
+
build-backend = "uv_build"
|
|
32
|
+
|
|
33
|
+
[tool.uv.build-backend]
|
|
34
|
+
module-root = "src"
|
|
35
|
+
module-name = "pudding"
|
|
36
|
+
|
|
37
|
+
[tool.mypy]
|
|
38
|
+
plugins = ["mypy_django_plugin.main"]
|
|
39
|
+
|
|
40
|
+
[tool.django-stubs]
|
|
41
|
+
django_settings_module = "tests.settings"
|
|
42
|
+
|
|
43
|
+
[dependency-groups]
|
|
44
|
+
dev = [
|
|
45
|
+
"django-stubs[compatible-mypy]>=6.0.9",
|
|
46
|
+
"mypy>=2.3.0",
|
|
47
|
+
"ruff>=0.16.2",
|
|
48
|
+
]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "django-pudding"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Declarative Python framework for Django to build dashboards and internal apps fast."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Zishan Kadri", email = "zishankadri01@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"django>=5.2",
|
|
12
|
+
]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Environment :: Web Environment",
|
|
15
|
+
"Framework :: Django",
|
|
16
|
+
"Framework :: Django :: 5.2",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Programming Language :: Python :: 3.14",
|
|
25
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
26
|
+
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
|
27
|
+
]
|
|
28
|
+
license = "MIT"
|
|
29
|
+
|
|
30
|
+
[build-system]
|
|
31
|
+
requires = ["uv_build>=0.12.3,<0.13.0"]
|
|
32
|
+
build-backend = "uv_build"
|
|
33
|
+
|
|
34
|
+
[tool.uv.build-backend]
|
|
35
|
+
module-root = "src"
|
|
36
|
+
module-name = "pudding"
|
|
37
|
+
|
|
38
|
+
[dependency-groups]
|
|
39
|
+
dev = [
|
|
40
|
+
"django-stubs[compatible-mypy]>=6.0.9",
|
|
41
|
+
"mypy>=2.3.0",
|
|
42
|
+
"ruff>=0.16.2",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[tool.mypy]
|
|
46
|
+
plugins = ["mypy_django_plugin.main"]
|
|
47
|
+
|
|
48
|
+
[tool.django-stubs]
|
|
49
|
+
django_settings_module = "tests.settings"
|
|
File without changes
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from pudding.actions.base import action_handler
|
|
2
|
+
from pudding.actions.contracts import Action
|
|
3
|
+
from pudding.actions.defaults.add import Add
|
|
4
|
+
from pudding.actions.defaults.delete import Delete
|
|
5
|
+
from pudding.actions.defaults.edit import Edit
|
|
6
|
+
from pudding.actions.defaults.view import View
|
|
7
|
+
from pudding.actions.utils import is_htmx
|
|
8
|
+
|
|
9
|
+
__all__ = ["Action", "Add", "Delete", "Edit", "View", "action_handler", "is_htmx"]
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from django.http import HttpRequest, HttpResponse, HttpResponseNotAllowed
|
|
9
|
+
|
|
10
|
+
from pudding import ui
|
|
11
|
+
from pudding.actions.contracts import ActionHook, ActionStep
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(kw_only=True)
|
|
15
|
+
class InteractiveAction(ABC):
|
|
16
|
+
"""
|
|
17
|
+
Base class for actions requiring user interaction (like confirmation or forms)
|
|
18
|
+
before the backend logic runs.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
slug: str
|
|
22
|
+
icon: str | None = None
|
|
23
|
+
permission: Callable[[HttpRequest], bool] | None = None
|
|
24
|
+
|
|
25
|
+
def handle(self, request: HttpRequest, ctx: dict[str, Any]) -> HttpResponse:
|
|
26
|
+
match request.method:
|
|
27
|
+
case "GET":
|
|
28
|
+
return self.before(request, ctx)
|
|
29
|
+
case "POST":
|
|
30
|
+
return self.execute(request, ctx)
|
|
31
|
+
case _:
|
|
32
|
+
return HttpResponseNotAllowed(["GET", "POST"])
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def before(self, request: HttpRequest, ctx: dict[str, Any]) -> HttpResponse: ...
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def execute(self, request: HttpRequest, ctx: dict[str, Any]) -> HttpResponse: ...
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(kw_only=True)
|
|
42
|
+
class FunctionalAction:
|
|
43
|
+
slug: str
|
|
44
|
+
trigger: ui.Trigger
|
|
45
|
+
icon: str | None = None
|
|
46
|
+
permission: Callable[[HttpRequest], bool] | None = None
|
|
47
|
+
|
|
48
|
+
execute: ActionStep | None = None
|
|
49
|
+
before: ActionStep | None = None
|
|
50
|
+
|
|
51
|
+
allowed_methods: list[str] = field(init=False, repr=False)
|
|
52
|
+
|
|
53
|
+
def __post_init__(self):
|
|
54
|
+
allowed = []
|
|
55
|
+
if self.before:
|
|
56
|
+
allowed.append("GET")
|
|
57
|
+
if self.execute:
|
|
58
|
+
allowed.append("POST")
|
|
59
|
+
|
|
60
|
+
if not allowed:
|
|
61
|
+
raise ValueError("At least one of `execute` or `before` must be provided.")
|
|
62
|
+
|
|
63
|
+
self.allowed_methods = allowed
|
|
64
|
+
|
|
65
|
+
def handle(self, request: HttpRequest, ctx: dict[str, Any]) -> HttpResponse:
|
|
66
|
+
if request.method == "GET" and self.before:
|
|
67
|
+
return self.before(self, request, ctx)
|
|
68
|
+
|
|
69
|
+
if request.method == "POST" and self.execute:
|
|
70
|
+
return self.execute(self, request, ctx)
|
|
71
|
+
|
|
72
|
+
return HttpResponseNotAllowed(self.allowed_methods)
|
|
73
|
+
|
|
74
|
+
def __repr__(self):
|
|
75
|
+
return (
|
|
76
|
+
f"FunctionalAction("
|
|
77
|
+
f"slug={self.slug!r}, "
|
|
78
|
+
f"execute={getattr(self.execute, '__name__', None)!r}, "
|
|
79
|
+
f"before={getattr(self.before, '__name__', None)!r})"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def action_handler(
|
|
84
|
+
*,
|
|
85
|
+
slug: str,
|
|
86
|
+
label: str | None = None,
|
|
87
|
+
icon: str | None = None,
|
|
88
|
+
before: ActionHook | Callable | None = None,
|
|
89
|
+
permission: Callable[[HttpRequest], bool] | None = None,
|
|
90
|
+
) -> Callable[[Callable], FunctionalAction]:
|
|
91
|
+
def decorator(func: Callable) -> FunctionalAction:
|
|
92
|
+
display_label = label or slug.replace("_", " ").replace("-", " ").capitalize()
|
|
93
|
+
trigger = ui.action(
|
|
94
|
+
slug,
|
|
95
|
+
label=display_label,
|
|
96
|
+
icon=icon,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
before_func = None
|
|
100
|
+
|
|
101
|
+
if isinstance(before, ActionHook):
|
|
102
|
+
frame = before.frame
|
|
103
|
+
before_func = before.before
|
|
104
|
+
trigger = trigger(**frame.loading_mod)
|
|
105
|
+
elif callable(before):
|
|
106
|
+
before_func = before
|
|
107
|
+
|
|
108
|
+
action = FunctionalAction(
|
|
109
|
+
slug=slug,
|
|
110
|
+
icon=icon,
|
|
111
|
+
trigger=trigger,
|
|
112
|
+
execute=func,
|
|
113
|
+
before=before_func,
|
|
114
|
+
permission=permission,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
return action
|
|
118
|
+
|
|
119
|
+
return decorator
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from pudding.ui.frames.base import Frame
|
|
9
|
+
|
|
10
|
+
from django.http import HttpRequest, HttpResponse
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@runtime_checkable
|
|
14
|
+
class Action(Protocol):
|
|
15
|
+
slug: str
|
|
16
|
+
icon: str | None
|
|
17
|
+
permission: Callable[[HttpRequest], bool] | None
|
|
18
|
+
|
|
19
|
+
def handle(self, request: HttpRequest, ctx: dict[str, Any]) -> HttpResponse: ...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
type ActionStep = Callable[[Action, HttpRequest, dict[str, Any]], HttpResponse]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True, slots=True)
|
|
26
|
+
class ActionHook:
|
|
27
|
+
frame: Frame
|
|
28
|
+
before: ActionStep
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from django.contrib import messages
|
|
7
|
+
from django.http import HttpRequest, HttpResponse
|
|
8
|
+
from django.shortcuts import render
|
|
9
|
+
from django.template.loader import render_to_string
|
|
10
|
+
from django.urls import reverse
|
|
11
|
+
from django.utils.html import format_html
|
|
12
|
+
from django.utils.translation import gettext_lazy as _
|
|
13
|
+
|
|
14
|
+
from pudding import actions, ui
|
|
15
|
+
from pudding.actions.base import InteractiveAction
|
|
16
|
+
from pudding.actions.defaults.utils import (
|
|
17
|
+
build_sidebar_from_domains,
|
|
18
|
+
get_form_class,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_content(self, form, request, ctx):
|
|
23
|
+
submit_btn = ui.action(
|
|
24
|
+
self.slug,
|
|
25
|
+
label=_("Save"),
|
|
26
|
+
method="post",
|
|
27
|
+
look=ui.looks.button(variant="solid"),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
form_id = "modal-form"
|
|
31
|
+
validity_guard = (
|
|
32
|
+
f"const f = document.getElementById('{form_id}');"
|
|
33
|
+
" if (!f.checkValidity()) { event.preventDefault(); f.reportValidity(); }"
|
|
34
|
+
)
|
|
35
|
+
submit_btn = submit_btn(
|
|
36
|
+
attrs={
|
|
37
|
+
"form": form_id,
|
|
38
|
+
"hx-on::before-request": validity_guard,
|
|
39
|
+
"hx-encoding": "multipart/form-data",
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
form_html = render_to_string(
|
|
43
|
+
"pudding/components/form.html",
|
|
44
|
+
{"form": form, "form_id": form_id},
|
|
45
|
+
request=request,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
submit_btn = submit_btn.render(ctx=ctx)
|
|
49
|
+
|
|
50
|
+
return form_html, submit_btn
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Add(InteractiveAction):
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
slug: str = "add",
|
|
57
|
+
icon: str | None = "plus",
|
|
58
|
+
modal_frame: ui.Frame = ui.modal_frame,
|
|
59
|
+
**kwargs,
|
|
60
|
+
):
|
|
61
|
+
self.modal_frame = modal_frame
|
|
62
|
+
super().__init__(slug=slug, icon=icon, **kwargs)
|
|
63
|
+
|
|
64
|
+
def before(self, request: HttpRequest, ctx: dict[str, Any]) -> HttpResponse:
|
|
65
|
+
domain = ctx["domain"]
|
|
66
|
+
model = domain.model
|
|
67
|
+
|
|
68
|
+
pks = request.POST.getlist("pks[]") or request.GET.getlist("pks[]")
|
|
69
|
+
|
|
70
|
+
FormClass = get_form_class(model=model)
|
|
71
|
+
|
|
72
|
+
base_label = _(model._meta.verbose_name_plural).title()
|
|
73
|
+
breadcrumbs = ui.Breadcrumbs(
|
|
74
|
+
children=tuple(
|
|
75
|
+
[
|
|
76
|
+
ui.action("view", label=base_label, icon=domain.icon),
|
|
77
|
+
ui.BreadcrumbLabel(label=_("Add"), icon="plus"),
|
|
78
|
+
]
|
|
79
|
+
)
|
|
80
|
+
).render(request=request, ctx=ctx)
|
|
81
|
+
|
|
82
|
+
form_html, submit_btn = get_content(
|
|
83
|
+
self, form=FormClass(), request=request, ctx=ctx
|
|
84
|
+
)
|
|
85
|
+
header_triggers = format_html(
|
|
86
|
+
'<div hx-vals="{ids}" class="flex gap-2 items-center"> {submit}</div>',
|
|
87
|
+
ids=json.dumps({"pks[]": pks}),
|
|
88
|
+
submit=submit_btn,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if actions.is_htmx(request):
|
|
92
|
+
return ui.ship(
|
|
93
|
+
ui.view_frame.fill(form_html),
|
|
94
|
+
ui.breadcrumb_frame.fill(breadcrumbs),
|
|
95
|
+
ui.header_triggers_frame.fill(header_triggers),
|
|
96
|
+
push_action_url={"action_slug": "add", "domain_slug": domain.slug},
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
context = ctx | {
|
|
100
|
+
"sidebar": build_sidebar_from_domains(ctx=ctx),
|
|
101
|
+
"breadcrumbs": ui.breadcrumb_frame(default_content=breadcrumbs).render(
|
|
102
|
+
request=request
|
|
103
|
+
),
|
|
104
|
+
"view_area": ui.view_frame(default_content=form_html).render(
|
|
105
|
+
request=request
|
|
106
|
+
),
|
|
107
|
+
"modal_frame": self.modal_frame.render(request=request),
|
|
108
|
+
"header_actions": ui.header_triggers_frame(
|
|
109
|
+
default_content=header_triggers
|
|
110
|
+
).render(request=request),
|
|
111
|
+
}
|
|
112
|
+
response = render(request, "pudding/table.html", context)
|
|
113
|
+
return response
|
|
114
|
+
|
|
115
|
+
def execute(self, request: HttpRequest, ctx: dict[str, Any]) -> HttpResponse:
|
|
116
|
+
domain = ctx["domain"]
|
|
117
|
+
model = domain.model
|
|
118
|
+
|
|
119
|
+
FormClass = get_form_class(model=model)
|
|
120
|
+
|
|
121
|
+
form = FormClass(request.POST, request.FILES)
|
|
122
|
+
if form.is_valid():
|
|
123
|
+
form.save()
|
|
124
|
+
messages.success(
|
|
125
|
+
request,
|
|
126
|
+
_("{name} created successfully!").format(
|
|
127
|
+
name=_(model._meta.verbose_name.capitalize())
|
|
128
|
+
),
|
|
129
|
+
)
|
|
130
|
+
response = HttpResponse(status=204)
|
|
131
|
+
response["HX-Redirect"] = reverse(
|
|
132
|
+
"pudding:run_action",
|
|
133
|
+
kwargs={
|
|
134
|
+
"domain_slug": domain.slug,
|
|
135
|
+
"action_slug": "view",
|
|
136
|
+
},
|
|
137
|
+
)
|
|
138
|
+
return response
|
|
139
|
+
else:
|
|
140
|
+
form_html, submit_btn = get_content(
|
|
141
|
+
self, form=form, request=request, ctx=ctx
|
|
142
|
+
)
|
|
143
|
+
return ui.ship(
|
|
144
|
+
ui.view_frame.fill(form_html), ui.header_triggers_frame.fill(submit_btn)
|
|
145
|
+
)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from django.contrib import messages
|
|
6
|
+
from django.http import Http404, HttpRequest, HttpResponse
|
|
7
|
+
from django.urls import reverse
|
|
8
|
+
from django.utils.translation import gettext_lazy as _
|
|
9
|
+
from django.utils.translation import ngettext
|
|
10
|
+
|
|
11
|
+
from pudding import ui
|
|
12
|
+
from pudding.actions.base import InteractiveAction
|
|
13
|
+
from pudding.ui.frames.base import Frame
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Delete(InteractiveAction):
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
slug: str = "delete",
|
|
20
|
+
icon: str | None = "trash-2",
|
|
21
|
+
modal_frame: Frame = ui.modal_frame,
|
|
22
|
+
*args,
|
|
23
|
+
**kwargs,
|
|
24
|
+
):
|
|
25
|
+
self.modal_frame = modal_frame
|
|
26
|
+
super().__init__(slug=slug, icon=icon, *args, **kwargs)
|
|
27
|
+
|
|
28
|
+
def before(self, request: HttpRequest, ctx: dict[str, Any]) -> HttpResponse:
|
|
29
|
+
domain = ctx["domain"]
|
|
30
|
+
model = domain.model
|
|
31
|
+
|
|
32
|
+
opts = model._meta
|
|
33
|
+
queryset = ctx["queryset"]
|
|
34
|
+
count = queryset.count()
|
|
35
|
+
if count == 0:
|
|
36
|
+
raise Http404(_("No items selected for deletion."))
|
|
37
|
+
|
|
38
|
+
if count == 1:
|
|
39
|
+
title = str(queryset.first())
|
|
40
|
+
else:
|
|
41
|
+
title = ngettext(
|
|
42
|
+
"{count} {model_name}", "{count} {model_name_plural}", count
|
|
43
|
+
).format(
|
|
44
|
+
count=count,
|
|
45
|
+
model_name=opts.verbose_name,
|
|
46
|
+
model_name_plural=opts.verbose_name_plural,
|
|
47
|
+
)
|
|
48
|
+
return ui.ship(
|
|
49
|
+
self.modal_frame.fill(
|
|
50
|
+
ui.ActionModal(
|
|
51
|
+
action=self,
|
|
52
|
+
eyebrow=_("Delete"),
|
|
53
|
+
title=title,
|
|
54
|
+
button=lambda btn: btn(look=ui.looks.button(variant="danger")),
|
|
55
|
+
icon_color="danger",
|
|
56
|
+
).render(ctx=ctx)
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def execute(self, request: HttpRequest, ctx: dict[str, Any]) -> HttpResponse:
|
|
61
|
+
model = ctx["domain"].model
|
|
62
|
+
|
|
63
|
+
opts = model._meta
|
|
64
|
+
queryset = ctx["queryset"]
|
|
65
|
+
count = queryset.count()
|
|
66
|
+
if count == 0:
|
|
67
|
+
raise Http404(_("No items selected for deletion."))
|
|
68
|
+
|
|
69
|
+
queryset.delete()
|
|
70
|
+
|
|
71
|
+
success_message = ngettext(
|
|
72
|
+
"Deleted {count} {model_name}.",
|
|
73
|
+
"Deleted {count} {model_name_plural}.",
|
|
74
|
+
count,
|
|
75
|
+
).format(
|
|
76
|
+
count=count,
|
|
77
|
+
model_name=opts.verbose_name,
|
|
78
|
+
model_name_plural=opts.verbose_name_plural,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
messages.success(request, success_message)
|
|
82
|
+
response = HttpResponse()
|
|
83
|
+
response["HX-Redirect"] = reverse(
|
|
84
|
+
"pudding:run_action",
|
|
85
|
+
kwargs={
|
|
86
|
+
"domain_slug": ctx["domain"].slug,
|
|
87
|
+
"action_slug": "view",
|
|
88
|
+
"step": "init",
|
|
89
|
+
},
|
|
90
|
+
)
|
|
91
|
+
return response
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from django.contrib import messages
|
|
6
|
+
from django.http import HttpRequest, HttpResponse
|
|
7
|
+
from django.urls import reverse
|
|
8
|
+
from django.utils.translation import gettext_lazy as _
|
|
9
|
+
|
|
10
|
+
from pudding import ui
|
|
11
|
+
from pudding.actions.base import InteractiveAction
|
|
12
|
+
from pudding.actions.defaults.utils import get_form_class
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Edit(InteractiveAction):
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
slug: str = "edit",
|
|
19
|
+
icon: str | None = "pen-line",
|
|
20
|
+
fields: list[str] | None = None,
|
|
21
|
+
exclude: list[str] | None = None,
|
|
22
|
+
dock_actions: list[ui.Trigger] | None = None,
|
|
23
|
+
modal_frame: ui.Frame = ui.modal_frame,
|
|
24
|
+
**kwargs,
|
|
25
|
+
):
|
|
26
|
+
self.fields = fields
|
|
27
|
+
self.exclude = exclude
|
|
28
|
+
self.dock_actions: list[ui.Trigger] = dock_actions or []
|
|
29
|
+
self.modal_frame = modal_frame
|
|
30
|
+
|
|
31
|
+
super().__init__(slug=slug, icon=icon, **kwargs)
|
|
32
|
+
|
|
33
|
+
def before(self, request: HttpRequest, ctx: dict[str, Any]) -> HttpResponse:
|
|
34
|
+
domain = ctx["domain"]
|
|
35
|
+
model = domain.model
|
|
36
|
+
|
|
37
|
+
FormClass = get_form_class(model=model)
|
|
38
|
+
|
|
39
|
+
instance = ctx["queryset"][0] if ctx["queryset"] else None
|
|
40
|
+
form = FormClass(instance=instance)
|
|
41
|
+
|
|
42
|
+
return ui.ship(
|
|
43
|
+
self.modal_frame.fill(
|
|
44
|
+
ui.ActionModal(
|
|
45
|
+
action=self, form=form, icon=None, title=f"Edit {instance}"
|
|
46
|
+
).render(ctx=ctx)
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def execute(self, request: HttpRequest, ctx: dict[str, Any]) -> HttpResponse:
|
|
51
|
+
domain = ctx["domain"]
|
|
52
|
+
model = domain.model
|
|
53
|
+
|
|
54
|
+
FormClass = get_form_class(model=model)
|
|
55
|
+
|
|
56
|
+
instance = ctx["queryset"][0] if ctx["queryset"] else None
|
|
57
|
+
form = FormClass(request.POST, request.FILES, instance=instance)
|
|
58
|
+
if form.is_valid():
|
|
59
|
+
form.save()
|
|
60
|
+
messages.success(
|
|
61
|
+
request,
|
|
62
|
+
_("{name} edited successfully!").format(
|
|
63
|
+
name=_(model._meta.verbose_name.capitalize())
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
response = HttpResponse(status=204)
|
|
67
|
+
response["HX-Redirect"] = reverse(
|
|
68
|
+
"pudding:run_action",
|
|
69
|
+
kwargs={
|
|
70
|
+
"domain_slug": domain.slug,
|
|
71
|
+
"action_slug": "view",
|
|
72
|
+
},
|
|
73
|
+
)
|
|
74
|
+
return response
|
|
75
|
+
else:
|
|
76
|
+
return ui.ship(
|
|
77
|
+
self.modal_frame.fill(
|
|
78
|
+
ui.ActionModal(
|
|
79
|
+
action=self, form=form, icon=None, title=f"Edit {instance}"
|
|
80
|
+
).render(ctx=ctx)
|
|
81
|
+
)
|
|
82
|
+
)
|