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.
Files changed (69) hide show
  1. django_pudding-0.1.0/PKG-INFO +24 -0
  2. django_pudding-0.1.0/README.md +0 -0
  3. django_pudding-0.1.0/pyproject.toml +48 -0
  4. django_pudding-0.1.0/pyproject.toml.orig +49 -0
  5. django_pudding-0.1.0/src/pudding/__init__.py +0 -0
  6. django_pudding-0.1.0/src/pudding/actions/__init__.py +9 -0
  7. django_pudding-0.1.0/src/pudding/actions/base.py +119 -0
  8. django_pudding-0.1.0/src/pudding/actions/contracts.py +28 -0
  9. django_pudding-0.1.0/src/pudding/actions/defaults/__init__.py +3 -0
  10. django_pudding-0.1.0/src/pudding/actions/defaults/add.py +145 -0
  11. django_pudding-0.1.0/src/pudding/actions/defaults/delete.py +91 -0
  12. django_pudding-0.1.0/src/pudding/actions/defaults/edit.py +82 -0
  13. django_pudding-0.1.0/src/pudding/actions/defaults/utils.py +100 -0
  14. django_pudding-0.1.0/src/pudding/actions/defaults/view.py +76 -0
  15. django_pudding-0.1.0/src/pudding/actions/registry.py +63 -0
  16. django_pudding-0.1.0/src/pudding/actions/utils.py +2 -0
  17. django_pudding-0.1.0/src/pudding/apps.py +7 -0
  18. django_pudding-0.1.0/src/pudding/domains/__init__.py +4 -0
  19. django_pudding-0.1.0/src/pudding/domains/base.py +62 -0
  20. django_pudding-0.1.0/src/pudding/domains/registry.py +56 -0
  21. django_pudding-0.1.0/src/pudding/exceptions.py +10 -0
  22. django_pudding-0.1.0/src/pudding/forms.py +30 -0
  23. django_pudding-0.1.0/src/pudding/py.typed +0 -0
  24. django_pudding-0.1.0/src/pudding/static/pudding/css/styles.css +2105 -0
  25. django_pudding-0.1.0/src/pudding/static/pudding/js/bundle.min.js +1813 -0
  26. django_pudding-0.1.0/src/pudding/static/pudding/js/bundle.min.js.map +7 -0
  27. django_pudding-0.1.0/src/pudding/templates/pudding/base.html +56 -0
  28. django_pudding-0.1.0/src/pudding/templates/pudding/components/actions/dropdown.html +17 -0
  29. django_pudding-0.1.0/src/pudding/templates/pudding/components/form.html +30 -0
  30. django_pudding-0.1.0/src/pudding/templates/pudding/components/messages.html +41 -0
  31. django_pudding-0.1.0/src/pudding/templates/pudding/components/overlays/modal.html +35 -0
  32. django_pudding-0.1.0/src/pudding/templates/pudding/components/sidebar.html +21 -0
  33. django_pudding-0.1.0/src/pudding/templates/pudding/components/table/_table_empty_state.html +29 -0
  34. django_pudding-0.1.0/src/pudding/templates/pudding/components/table/header_checkbox.html +20 -0
  35. django_pudding-0.1.0/src/pudding/templates/pudding/components/table/pagination.html +47 -0
  36. django_pudding-0.1.0/src/pudding/templates/pudding/components/table/table.html +55 -0
  37. django_pudding-0.1.0/src/pudding/templates/pudding/components/table/table_cell.html +5 -0
  38. django_pudding-0.1.0/src/pudding/templates/pudding/components/table/table_controls.html +97 -0
  39. django_pudding-0.1.0/src/pudding/templates/pudding/components/table/table_row.html +54 -0
  40. django_pudding-0.1.0/src/pudding/templates/pudding/frames/modal_frame.html +62 -0
  41. django_pudding-0.1.0/src/pudding/templates/pudding/frames/view_frame.html +36 -0
  42. django_pudding-0.1.0/src/pudding/templates/pudding/table.html +15 -0
  43. django_pudding-0.1.0/src/pudding/templates/pudding/table_columns/profile.html +11 -0
  44. django_pudding-0.1.0/src/pudding/ui/__init__.py +52 -0
  45. django_pudding-0.1.0/src/pudding/ui/_types.py +14 -0
  46. django_pudding-0.1.0/src/pudding/ui/breadcrumbs.py +83 -0
  47. django_pudding-0.1.0/src/pudding/ui/buttons/__init__.py +18 -0
  48. django_pudding-0.1.0/src/pudding/ui/buttons/base.py +165 -0
  49. django_pudding-0.1.0/src/pudding/ui/buttons/looks.py +117 -0
  50. django_pudding-0.1.0/src/pudding/ui/buttons/wrappers/__init__.py +0 -0
  51. django_pudding-0.1.0/src/pudding/ui/buttons/wrappers/base.py +73 -0
  52. django_pudding-0.1.0/src/pudding/ui/buttons/wrappers/factories.py +90 -0
  53. django_pudding-0.1.0/src/pudding/ui/contracts.py +47 -0
  54. django_pudding-0.1.0/src/pudding/ui/frames/__init__.py +20 -0
  55. django_pudding-0.1.0/src/pudding/ui/frames/base.py +150 -0
  56. django_pudding-0.1.0/src/pudding/ui/frames/frame_response.py +77 -0
  57. django_pudding-0.1.0/src/pudding/ui/frames/modal.py +175 -0
  58. django_pudding-0.1.0/src/pudding/ui/hooks/__init__.py +3 -0
  59. django_pudding-0.1.0/src/pudding/ui/hooks/modal.py +17 -0
  60. django_pudding-0.1.0/src/pudding/ui/sidebar/sidebar.py +23 -0
  61. django_pudding-0.1.0/src/pudding/ui/tables/__init__.py +3 -0
  62. django_pudding-0.1.0/src/pudding/ui/tables/base.py +142 -0
  63. django_pudding-0.1.0/src/pudding/ui/tables/columns/base.py +73 -0
  64. django_pudding-0.1.0/src/pudding/ui/tables/columns/column_formatters.py +112 -0
  65. django_pudding-0.1.0/src/pudding/ui/tables/columns/column_renderers.py +114 -0
  66. django_pudding-0.1.0/src/pudding/ui/tables/table_row.py +61 -0
  67. django_pudding-0.1.0/src/pudding/ui/utils.py +86 -0
  68. django_pudding-0.1.0/src/pudding/urls.py +13 -0
  69. 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,3 @@
1
+ from . import add, delete, edit, view
2
+
3
+ __all__ = ["add", "delete", "edit", "view"]
@@ -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
+ )