tungsten-admin 0.1.0__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.
- tungsten/__init__.py +53 -0
- tungsten/actions/__init__.py +25 -0
- tungsten/actions/action.py +432 -0
- tungsten/actions/prebuilt.py +396 -0
- tungsten/auth/__init__.py +333 -0
- tungsten/auth/profile.py +73 -0
- tungsten/auth/rbac.py +138 -0
- tungsten/auth/two_factor.py +147 -0
- tungsten/auth/verification.py +91 -0
- tungsten/cli/__init__.py +352 -0
- tungsten/cli/stubs.py +251 -0
- tungsten/context.py +112 -0
- tungsten/forms/__init__.py +36 -0
- tungsten/forms/base.py +57 -0
- tungsten/forms/fields.py +1960 -0
- tungsten/forms/form.py +292 -0
- tungsten/forms/layout.py +341 -0
- tungsten/hosts.py +343 -0
- tungsten/i18n.py +284 -0
- tungsten/importexport.py +413 -0
- tungsten/infolists.py +263 -0
- tungsten/lang/hi.json +362 -0
- tungsten/models.py +97 -0
- tungsten/navigation.py +37 -0
- tungsten/notifications.py +120 -0
- tungsten/pages.py +222 -0
- tungsten/panel.py +494 -0
- tungsten/plugins.py +35 -0
- tungsten/rendering.py +71 -0
- tungsten/resources/__init__.py +4 -0
- tungsten/resources/relation_manager.py +88 -0
- tungsten/resources/resource.py +232 -0
- tungsten/routes.py +1305 -0
- tungsten/static/tungsten/favicon.svg +1 -0
- tungsten/static/tungsten/tungsten.css +1 -0
- tungsten/static/tungsten/tungsten.js +330 -0
- tungsten/static/tungsten/vendor/alpine.min.js +21 -0
- tungsten/static/tungsten/vendor/chart.umd.min.js +14 -0
- tungsten/static/tungsten/vendor/htmx.min.js +1 -0
- tungsten/static/tungsten/vendor/qrcode.js +2297 -0
- tungsten/static/tungsten/vendor/sortable.min.js +2 -0
- tungsten/static/tungsten/vendor/tom-select.complete.min.js +454 -0
- tungsten/static/tungsten/vendor/tom-select.css +413 -0
- tungsten/static/tungsten/vendor/trix.css +470 -0
- tungsten/static/tungsten/vendor/trix.umd.min.js +7 -0
- tungsten/storage.py +69 -0
- tungsten/support/__init__.py +0 -0
- tungsten/support/aio.py +71 -0
- tungsten/support/colors.py +172 -0
- tungsten/support/component.py +57 -0
- tungsten/support/evaluate.py +52 -0
- tungsten/support/html.py +85 -0
- tungsten/support/icons.json +1 -0
- tungsten/support/icons.py +69 -0
- tungsten/support/palettes.py +26 -0
- tungsten/support/state.py +99 -0
- tungsten/tables/__init__.py +38 -0
- tungsten/tables/columns.py +849 -0
- tungsten/tables/filters.py +365 -0
- tungsten/tables/query_builder.py +617 -0
- tungsten/tables/summarizers.py +81 -0
- tungsten/tables/table.py +826 -0
- tungsten/templates/tungsten/actions/button.html +21 -0
- tungsten/templates/tungsten/actions/group.html +14 -0
- tungsten/templates/tungsten/actions/modal.html +47 -0
- tungsten/templates/tungsten/auth/forgot-password.html +19 -0
- tungsten/templates/tungsten/auth/login.html +21 -0
- tungsten/templates/tungsten/auth/register.html +19 -0
- tungsten/templates/tungsten/auth/reset-password.html +17 -0
- tungsten/templates/tungsten/auth/two-factor-challenge.html +15 -0
- tungsten/templates/tungsten/auth/two-factor-setup.html +69 -0
- tungsten/templates/tungsten/auth/verify-email.html +36 -0
- tungsten/templates/tungsten/components/brand.html +10 -0
- tungsten/templates/tungsten/components/global-search.html +13 -0
- tungsten/templates/tungsten/components/notification-badge.html +1 -0
- tungsten/templates/tungsten/components/notification-bell.html +11 -0
- tungsten/templates/tungsten/components/notifications.html +32 -0
- tungsten/templates/tungsten/components/page-filters.html +6 -0
- tungsten/templates/tungsten/components/page-header.html +14 -0
- tungsten/templates/tungsten/components/search-results.html +34 -0
- tungsten/templates/tungsten/components/toasts.html +21 -0
- tungsten/templates/tungsten/components/ui.html +182 -0
- tungsten/templates/tungsten/components/widgets.html +16 -0
- tungsten/templates/tungsten/forms/fields/checkbox-list.html +26 -0
- tungsten/templates/tungsten/forms/fields/checkbox.html +10 -0
- tungsten/templates/tungsten/forms/fields/color-picker.html +12 -0
- tungsten/templates/tungsten/forms/fields/date-picker.html +8 -0
- tungsten/templates/tungsten/forms/fields/file-upload.html +50 -0
- tungsten/templates/tungsten/forms/fields/hidden.html +1 -0
- tungsten/templates/tungsten/forms/fields/key-value.html +32 -0
- tungsten/templates/tungsten/forms/fields/permissions.html +34 -0
- tungsten/templates/tungsten/forms/fields/placeholder.html +4 -0
- tungsten/templates/tungsten/forms/fields/radio.html +16 -0
- tungsten/templates/tungsten/forms/fields/repeater.html +75 -0
- tungsten/templates/tungsten/forms/fields/rich-editor.html +15 -0
- tungsten/templates/tungsten/forms/fields/select.html +26 -0
- tungsten/templates/tungsten/forms/fields/tags-input.html +30 -0
- tungsten/templates/tungsten/forms/fields/text-input.html +33 -0
- tungsten/templates/tungsten/forms/fields/textarea.html +10 -0
- tungsten/templates/tungsten/forms/fields/toggle-buttons.html +28 -0
- tungsten/templates/tungsten/forms/fields/toggle.html +23 -0
- tungsten/templates/tungsten/forms/form.html +7 -0
- tungsten/templates/tungsten/forms/grid.html +6 -0
- tungsten/templates/tungsten/forms/layouts/fieldset.html +4 -0
- tungsten/templates/tungsten/forms/layouts/group.html +1 -0
- tungsten/templates/tungsten/forms/layouts/section.html +26 -0
- tungsten/templates/tungsten/forms/layouts/tabs.html +19 -0
- tungsten/templates/tungsten/forms/layouts/wizard.html +41 -0
- tungsten/templates/tungsten/forms/macros.html +46 -0
- tungsten/templates/tungsten/infolists/entry.html +9 -0
- tungsten/templates/tungsten/infolists/infolist.html +3 -0
- tungsten/templates/tungsten/infolists/key-value-entry.html +15 -0
- tungsten/templates/tungsten/infolists/repeatable-entry.html +8 -0
- tungsten/templates/tungsten/layout/app.html +167 -0
- tungsten/templates/tungsten/layout/auth.html +36 -0
- tungsten/templates/tungsten/layout/base.html +53 -0
- tungsten/templates/tungsten/pages/dashboard.html +13 -0
- tungsten/templates/tungsten/pages/error.html +14 -0
- tungsten/templates/tungsten/pages/page.html +19 -0
- tungsten/templates/tungsten/pages/profile.html +23 -0
- tungsten/templates/tungsten/resources/list.html +8 -0
- tungsten/templates/tungsten/resources/record.html +54 -0
- tungsten/templates/tungsten/tables/columns/checkbox.html +5 -0
- tungsten/templates/tungsten/tables/columns/color.html +3 -0
- tungsten/templates/tungsten/tables/columns/icon.html +3 -0
- tungsten/templates/tungsten/tables/columns/image.html +12 -0
- tungsten/templates/tungsten/tables/columns/select.html +8 -0
- tungsten/templates/tungsten/tables/columns/text-input.html +6 -0
- tungsten/templates/tungsten/tables/columns/text.html +33 -0
- tungsten/templates/tungsten/tables/columns/toggle.html +11 -0
- tungsten/templates/tungsten/tables/query-builder.html +74 -0
- tungsten/templates/tungsten/tables/table.html +253 -0
- tungsten/templates/tungsten/widgets/account.html +9 -0
- tungsten/templates/tungsten/widgets/chart.html +37 -0
- tungsten/templates/tungsten/widgets/progress.html +21 -0
- tungsten/templates/tungsten/widgets/stats.html +7 -0
- tungsten/templates/tungsten/widgets/table.html +14 -0
- tungsten/tenancy.py +102 -0
- tungsten/widgets.py +269 -0
- tungsten_admin-0.1.0.dist-info/METADATA +581 -0
- tungsten_admin-0.1.0.dist-info/RECORD +144 -0
- tungsten_admin-0.1.0.dist-info/WHEEL +4 -0
- tungsten_admin-0.1.0.dist-info/entry_points.txt +2 -0
- tungsten_admin-0.1.0.dist-info/licenses/LICENSE +21 -0
tungsten/__init__.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Tungsten — a Filament-style admin panel for FastAPI.
|
|
2
|
+
|
|
3
|
+
Quick start::
|
|
4
|
+
|
|
5
|
+
from tungsten import Panel, Resource, Auth
|
|
6
|
+
from tungsten.forms import TextInput
|
|
7
|
+
from tungsten.tables import TextColumn
|
|
8
|
+
|
|
9
|
+
class ProductResource(Resource):
|
|
10
|
+
model = Product
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def form(cls, form):
|
|
14
|
+
return form.schema([TextInput("name").required()])
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def table(cls, table):
|
|
18
|
+
return table.columns([TextColumn("name").searchable().sortable()])
|
|
19
|
+
|
|
20
|
+
panel = Panel(engine=engine, secret_key="...", auth=Auth(User))
|
|
21
|
+
panel.resources([ProductResource]).mount(app)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from .auth import Auth, hash_password, verify_password
|
|
25
|
+
from .context import Context
|
|
26
|
+
from .i18n import __, translate
|
|
27
|
+
from .navigation import NavigationGroup, NavigationItem
|
|
28
|
+
from .notifications import Notification
|
|
29
|
+
from .pages import Dashboard, Page
|
|
30
|
+
from .panel import VERSION as __version__
|
|
31
|
+
from .panel import Panel
|
|
32
|
+
from .plugins import Plugin
|
|
33
|
+
from .resources.relation_manager import RelationManager
|
|
34
|
+
from .resources.resource import Resource
|
|
35
|
+
from .storage import LocalStorage, Storage
|
|
36
|
+
from .tenancy import Tenancy
|
|
37
|
+
from .widgets import (
|
|
38
|
+
AccountWidget,
|
|
39
|
+
ChartWidget,
|
|
40
|
+
ProgressItem,
|
|
41
|
+
ProgressListWidget,
|
|
42
|
+
Stat,
|
|
43
|
+
StatsOverviewWidget,
|
|
44
|
+
TableWidget,
|
|
45
|
+
Widget,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
"AccountWidget", "Auth", "ChartWidget", "Context", "Dashboard", "LocalStorage", "NavigationGroup",
|
|
50
|
+
"NavigationItem", "Notification", "Page", "Panel", "Plugin", "ProgressItem", "ProgressListWidget",
|
|
51
|
+
"RelationManager", "Resource", "Stat", "StatsOverviewWidget", "Storage", "TableWidget", "Tenancy", "Widget",
|
|
52
|
+
"__", "__version__", "hash_password", "translate", "verify_password",
|
|
53
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Actions: buttons, confirmation boxes and modal forms."""
|
|
2
|
+
|
|
3
|
+
from .action import Action, ActionGroup, BulkAction, BulkActionGroup, Halt, flatten_actions
|
|
4
|
+
from .prebuilt import (
|
|
5
|
+
AttachAction,
|
|
6
|
+
CreateAction,
|
|
7
|
+
DeleteAction,
|
|
8
|
+
DeleteBulkAction,
|
|
9
|
+
DetachAction,
|
|
10
|
+
DetachBulkAction,
|
|
11
|
+
EditAction,
|
|
12
|
+
ForceDeleteAction,
|
|
13
|
+
ForceDeleteBulkAction,
|
|
14
|
+
ReplicateAction,
|
|
15
|
+
RestoreAction,
|
|
16
|
+
RestoreBulkAction,
|
|
17
|
+
ViewAction,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"Action", "ActionGroup", "AttachAction", "BulkAction", "BulkActionGroup", "CreateAction", "DeleteAction",
|
|
22
|
+
"DeleteBulkAction", "DetachAction", "DetachBulkAction", "EditAction", "ForceDeleteAction",
|
|
23
|
+
"ForceDeleteBulkAction", "Halt", "ReplicateAction", "RestoreAction", "RestoreBulkAction", "ViewAction",
|
|
24
|
+
"flatten_actions",
|
|
25
|
+
]
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
"""Actions: buttons that run code, open a confirmation box or a modal form."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
6
|
+
|
|
7
|
+
from markupsafe import Markup
|
|
8
|
+
|
|
9
|
+
from ..i18n import maybe
|
|
10
|
+
from ..i18n import translate as __
|
|
11
|
+
from ..support.component import Component, headline
|
|
12
|
+
from ..support.evaluate import call, evaluate
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
15
|
+
from ..context import Context
|
|
16
|
+
from ..forms.form import Form
|
|
17
|
+
from ..hosts import Host
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Halt(Exception):
|
|
21
|
+
"""Raise inside an action to stop it quietly (the modal stays open)."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Action(Component):
|
|
25
|
+
"""A button. Examples::
|
|
26
|
+
|
|
27
|
+
Action("approve").icon("check").color("success").requires_confirmation()
|
|
28
|
+
.action(lambda record: record.approve())
|
|
29
|
+
|
|
30
|
+
Action("email").form([TextInput("subject").required(), Textarea("body")])
|
|
31
|
+
.action(lambda record, data: send_mail(record.email, **data))
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
#: where this action lives — set by the host when it is resolved
|
|
35
|
+
scope = "page"
|
|
36
|
+
#: shown as icon buttons in table rows when an icon exists
|
|
37
|
+
default_style = "button"
|
|
38
|
+
|
|
39
|
+
def __init__(self, name: str) -> None:
|
|
40
|
+
super().__init__()
|
|
41
|
+
self.name = name
|
|
42
|
+
self._label: Any = None
|
|
43
|
+
self._icon: Any = None
|
|
44
|
+
self._color: Any = "primary"
|
|
45
|
+
self._url: Any = None
|
|
46
|
+
self._new_tab = False
|
|
47
|
+
self._requires_confirmation = False
|
|
48
|
+
self._modal_heading: Any = None
|
|
49
|
+
self._modal_description: Any = None
|
|
50
|
+
self._modal_submit_label: Any = None
|
|
51
|
+
self._modal_cancel_label: Any = "Cancel"
|
|
52
|
+
self._modal_icon: Any = None
|
|
53
|
+
self._modal_icon_color: Any = None
|
|
54
|
+
self._modal_width = "lg"
|
|
55
|
+
self._slide_over = False
|
|
56
|
+
self._form: Any = None
|
|
57
|
+
self._form_columns = 1
|
|
58
|
+
self._fill_form: Callable | None = None
|
|
59
|
+
self._action: Callable | None = None
|
|
60
|
+
self._success_title: Any = None
|
|
61
|
+
self._success_body: Any = None
|
|
62
|
+
self._failure_title: Any = None
|
|
63
|
+
self._success_redirect: Any = None
|
|
64
|
+
self._disabled: Any = False
|
|
65
|
+
self._tooltip: Any = None
|
|
66
|
+
self._style: str | None = None
|
|
67
|
+
self._size = "md"
|
|
68
|
+
self._badge: Any = None
|
|
69
|
+
self._outlined = False
|
|
70
|
+
self._authorize: Any = None
|
|
71
|
+
self._hidden_label = False
|
|
72
|
+
self._keyboard: str | None = None
|
|
73
|
+
self._refresh_table = True
|
|
74
|
+
self._modal_content: Any = None
|
|
75
|
+
self._deselect_after = True
|
|
76
|
+
|
|
77
|
+
# ------------------------------------------------------------------ config
|
|
78
|
+
def label(self, label: Any) -> "Action":
|
|
79
|
+
self._label = label
|
|
80
|
+
return self
|
|
81
|
+
|
|
82
|
+
def icon(self, icon: Any) -> "Action":
|
|
83
|
+
self._icon = icon
|
|
84
|
+
return self
|
|
85
|
+
|
|
86
|
+
def color(self, color: Any) -> "Action":
|
|
87
|
+
self._color = color
|
|
88
|
+
return self
|
|
89
|
+
|
|
90
|
+
def url(self, url: Any, open_in_new_tab: bool = False) -> "Action":
|
|
91
|
+
"""Make the action a link. ``url`` may be a closure: ``lambda record: ...``."""
|
|
92
|
+
self._url = url
|
|
93
|
+
self._new_tab = open_in_new_tab
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
def requires_confirmation(self, condition: bool = True) -> "Action":
|
|
97
|
+
self._requires_confirmation = condition
|
|
98
|
+
return self
|
|
99
|
+
|
|
100
|
+
def modal_heading(self, text: Any) -> "Action":
|
|
101
|
+
self._modal_heading = text
|
|
102
|
+
return self
|
|
103
|
+
|
|
104
|
+
def modal_description(self, text: Any) -> "Action":
|
|
105
|
+
self._modal_description = text
|
|
106
|
+
return self
|
|
107
|
+
|
|
108
|
+
def modal_submit_action_label(self, text: Any) -> "Action":
|
|
109
|
+
self._modal_submit_label = text
|
|
110
|
+
return self
|
|
111
|
+
|
|
112
|
+
modal_submit_label = modal_submit_action_label
|
|
113
|
+
|
|
114
|
+
def modal_cancel_action_label(self, text: Any) -> "Action":
|
|
115
|
+
self._modal_cancel_label = text
|
|
116
|
+
return self
|
|
117
|
+
|
|
118
|
+
def modal_icon(self, icon: Any, color: Any = None) -> "Action":
|
|
119
|
+
self._modal_icon = icon
|
|
120
|
+
self._modal_icon_color = color
|
|
121
|
+
return self
|
|
122
|
+
|
|
123
|
+
def modal_width(self, width: str) -> "Action":
|
|
124
|
+
"""sm, md, lg, xl, 2xl, 3xl, 4xl, 5xl"""
|
|
125
|
+
self._modal_width = width
|
|
126
|
+
return self
|
|
127
|
+
|
|
128
|
+
def modal_content(self, content: Any) -> "Action":
|
|
129
|
+
"""Extra HTML shown in the modal (``Markup`` or a closure returning it)."""
|
|
130
|
+
self._modal_content = content
|
|
131
|
+
return self
|
|
132
|
+
|
|
133
|
+
def slide_over(self, condition: bool = True) -> "Action":
|
|
134
|
+
self._slide_over = condition
|
|
135
|
+
return self
|
|
136
|
+
|
|
137
|
+
def form(self, schema: Any, columns: int = 1) -> "Action":
|
|
138
|
+
"""A list of fields (or ``lambda form: form.schema([...])``) shown in a modal."""
|
|
139
|
+
self._form = schema
|
|
140
|
+
self._form_columns = columns
|
|
141
|
+
return self
|
|
142
|
+
|
|
143
|
+
def fill_form(self, fn: Callable) -> "Action":
|
|
144
|
+
"""Initial modal data: ``fill_form(lambda record: {"status": record.status})``."""
|
|
145
|
+
self._fill_form = fn
|
|
146
|
+
return self
|
|
147
|
+
|
|
148
|
+
def action(self, fn: Callable) -> "Action":
|
|
149
|
+
"""The code to run. Gets ``record``/``records``/``data``/``ctx``/``db``/``user``."""
|
|
150
|
+
self._action = fn
|
|
151
|
+
return self
|
|
152
|
+
|
|
153
|
+
def success_notification_title(self, title: Any) -> "Action":
|
|
154
|
+
self._success_title = title
|
|
155
|
+
return self
|
|
156
|
+
|
|
157
|
+
def success_notification_body(self, body: Any) -> "Action":
|
|
158
|
+
self._success_body = body
|
|
159
|
+
return self
|
|
160
|
+
|
|
161
|
+
def failure_notification_title(self, title: Any) -> "Action":
|
|
162
|
+
self._failure_title = title
|
|
163
|
+
return self
|
|
164
|
+
|
|
165
|
+
def success_redirect_url(self, url: Any) -> "Action":
|
|
166
|
+
self._success_redirect = url
|
|
167
|
+
return self
|
|
168
|
+
|
|
169
|
+
def disabled(self, condition: Any = True) -> "Action":
|
|
170
|
+
self._disabled = condition
|
|
171
|
+
return self
|
|
172
|
+
|
|
173
|
+
def tooltip(self, text: Any) -> "Action":
|
|
174
|
+
self._tooltip = text
|
|
175
|
+
return self
|
|
176
|
+
|
|
177
|
+
def button(self) -> "Action":
|
|
178
|
+
self._style = "button"
|
|
179
|
+
return self
|
|
180
|
+
|
|
181
|
+
def link(self) -> "Action":
|
|
182
|
+
self._style = "link"
|
|
183
|
+
return self
|
|
184
|
+
|
|
185
|
+
def icon_button(self) -> "Action":
|
|
186
|
+
self._style = "icon"
|
|
187
|
+
return self
|
|
188
|
+
|
|
189
|
+
def soft(self) -> "Action":
|
|
190
|
+
"""Light colored button (used for bulk actions)."""
|
|
191
|
+
self._style = "soft"
|
|
192
|
+
return self
|
|
193
|
+
|
|
194
|
+
def outlined(self, condition: bool = True) -> "Action":
|
|
195
|
+
self._outlined = condition
|
|
196
|
+
return self
|
|
197
|
+
|
|
198
|
+
def size(self, size: str) -> "Action":
|
|
199
|
+
self._size = size
|
|
200
|
+
return self
|
|
201
|
+
|
|
202
|
+
def badge(self, badge: Any) -> "Action":
|
|
203
|
+
self._badge = badge
|
|
204
|
+
return self
|
|
205
|
+
|
|
206
|
+
def hidden_label(self, condition: bool = True) -> "Action":
|
|
207
|
+
self._hidden_label = condition
|
|
208
|
+
return self
|
|
209
|
+
|
|
210
|
+
def authorize(self, ability: Any) -> "Action":
|
|
211
|
+
"""A permission ability (``"delete"``), a closure, or a bool."""
|
|
212
|
+
self._authorize = ability
|
|
213
|
+
return self
|
|
214
|
+
|
|
215
|
+
def keyboard_shortcut(self, keys: str) -> "Action":
|
|
216
|
+
self._keyboard = keys
|
|
217
|
+
return self
|
|
218
|
+
|
|
219
|
+
def deselect_records_after_completion(self, condition: bool = True) -> "Action":
|
|
220
|
+
self._deselect_after = condition
|
|
221
|
+
return self
|
|
222
|
+
|
|
223
|
+
# ------------------------------------------------------------------ resolve
|
|
224
|
+
def ev(self, ctx: "Context | None", record: Any = None, records: Any = None, **extra: Any) -> dict[str, Any]:
|
|
225
|
+
return {
|
|
226
|
+
"ctx": ctx,
|
|
227
|
+
"request": ctx.request if ctx else None,
|
|
228
|
+
"user": ctx.user if ctx else None,
|
|
229
|
+
"db": ctx.db if ctx else None,
|
|
230
|
+
"tenant": ctx.tenant if ctx else None,
|
|
231
|
+
"record": record,
|
|
232
|
+
"records": records,
|
|
233
|
+
"action": self,
|
|
234
|
+
**extra,
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
def get_label(self, ev: dict | None = None) -> str:
|
|
238
|
+
if self._label is None:
|
|
239
|
+
return __(headline(self.name))
|
|
240
|
+
return __(str(evaluate(self._label, **(ev or {}))))
|
|
241
|
+
|
|
242
|
+
def needs_modal(self) -> bool:
|
|
243
|
+
return bool(self._requires_confirmation or self._form is not None)
|
|
244
|
+
|
|
245
|
+
def is_url(self) -> bool:
|
|
246
|
+
return self._url is not None
|
|
247
|
+
|
|
248
|
+
def is_authorized(self, host: "Host | None", ctx: "Context", record: Any = None) -> bool:
|
|
249
|
+
if self._authorize is None:
|
|
250
|
+
return True
|
|
251
|
+
if isinstance(self._authorize, str):
|
|
252
|
+
return host.can(ctx, self._authorize, record) if host else ctx.can(self._authorize, record)
|
|
253
|
+
return bool(evaluate(self._authorize, **self.ev(ctx, record)))
|
|
254
|
+
|
|
255
|
+
def is_available(self, host: "Host | None", ctx: "Context", record: Any = None) -> bool:
|
|
256
|
+
ev = self.ev(ctx, record, host=host)
|
|
257
|
+
return self.is_visible(**ev) and self.is_authorized(host, ctx, record)
|
|
258
|
+
|
|
259
|
+
def get_url(self, ctx: "Context", record: Any = None) -> str | None:
|
|
260
|
+
return evaluate(self._url, **self.ev(ctx, record)) if self._url is not None else None
|
|
261
|
+
|
|
262
|
+
def build_form(self, ctx: "Context", host: "Host | None", record: Any = None, records: Any = None) -> "Form | None":
|
|
263
|
+
from ..forms.form import Form
|
|
264
|
+
|
|
265
|
+
if self._form is None:
|
|
266
|
+
return None
|
|
267
|
+
form = Form()
|
|
268
|
+
form.columns(self._form_columns)
|
|
269
|
+
if callable(self._form) and not isinstance(self._form, list):
|
|
270
|
+
result = call(self._form, **{**self.ev(ctx, record, records), "form": form})
|
|
271
|
+
if isinstance(result, Form):
|
|
272
|
+
form = result
|
|
273
|
+
elif isinstance(result, list):
|
|
274
|
+
form.schema(result)
|
|
275
|
+
else:
|
|
276
|
+
form.schema(list(self._form))
|
|
277
|
+
if host is not None and getattr(host, "model", None) is not None and form._model is None:
|
|
278
|
+
form.model(host.model)
|
|
279
|
+
return form
|
|
280
|
+
|
|
281
|
+
def fill(self, form: "Form", ctx: "Context", record: Any = None, records: Any = None) -> None:
|
|
282
|
+
form.fill(None)
|
|
283
|
+
if self._fill_form is not None:
|
|
284
|
+
data = call(self._fill_form, **self.ev(ctx, record, records, form=form)) or {}
|
|
285
|
+
form.fill_from(data)
|
|
286
|
+
|
|
287
|
+
def run(self, ctx: "Context", host: "Host | None", record: Any = None, records: Any = None,
|
|
288
|
+
data: dict | None = None, form: "Form | None" = None) -> Any:
|
|
289
|
+
if self._action is None:
|
|
290
|
+
return None
|
|
291
|
+
return call(self._action, **{**self.ev(ctx, record, records), "data": data or {}, "form": form, "host": host})
|
|
292
|
+
|
|
293
|
+
def success_notification(self, ctx: "Context", record: Any = None, records: Any = None):
|
|
294
|
+
from ..notifications import Notification
|
|
295
|
+
|
|
296
|
+
title = evaluate(self._success_title, **self.ev(ctx, record, records))
|
|
297
|
+
if not title:
|
|
298
|
+
return None
|
|
299
|
+
body = evaluate(self._success_body, **self.ev(ctx, record, records))
|
|
300
|
+
return Notification(title).body(body).success()
|
|
301
|
+
|
|
302
|
+
# ------------------------------------------------------------------ render
|
|
303
|
+
def view_data(self, ctx: "Context", host: "Host | None", record: Any = None, *, style: str | None = None,
|
|
304
|
+
table_id: str | None = None) -> dict[str, Any]:
|
|
305
|
+
ev = self.ev(ctx, record, host=host)
|
|
306
|
+
style = style or self._style or self.default_style
|
|
307
|
+
color = evaluate(self._color, **ev) or "primary"
|
|
308
|
+
url = self.get_url(ctx, record)
|
|
309
|
+
record_key = host.record_key(record) if (host is not None and record is not None) else None
|
|
310
|
+
endpoint = ctx.panel.url("_tw", "action")
|
|
311
|
+
vals = {"_tw_host": host.key if host else "", "_tw_scope": self.scope, "_tw_name": self.name}
|
|
312
|
+
if record_key is not None:
|
|
313
|
+
vals["_tw_record"] = record_key
|
|
314
|
+
label = self.get_label(ev)
|
|
315
|
+
return {
|
|
316
|
+
"name": self.name,
|
|
317
|
+
"label": label,
|
|
318
|
+
"icon": evaluate(self._icon, **ev),
|
|
319
|
+
"color": color,
|
|
320
|
+
"style": style,
|
|
321
|
+
"size": self._size,
|
|
322
|
+
"url": url,
|
|
323
|
+
"new_tab": self._new_tab,
|
|
324
|
+
"modal": self.needs_modal(),
|
|
325
|
+
"endpoint": endpoint,
|
|
326
|
+
"vals": vals,
|
|
327
|
+
"disabled": bool(evaluate(self._disabled, **ev)),
|
|
328
|
+
"tooltip": maybe(evaluate(self._tooltip, **ev)) or (label if style == "icon" else None),
|
|
329
|
+
"badge": evaluate(self._badge, **ev),
|
|
330
|
+
"outlined": self._outlined,
|
|
331
|
+
"hidden_label": self._hidden_label,
|
|
332
|
+
"bulk": self.scope == "bulk",
|
|
333
|
+
"table_id": table_id,
|
|
334
|
+
"keyboard": self._keyboard,
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
def render(self, ctx: "Context", host: "Host | None", record: Any = None, *, style: str | None = None,
|
|
338
|
+
table_id: str | None = None) -> Markup:
|
|
339
|
+
if not self.is_available(host, ctx, record):
|
|
340
|
+
return Markup("")
|
|
341
|
+
return ctx.panel.renderer.render(
|
|
342
|
+
"tungsten/actions/button.html",
|
|
343
|
+
a=self.view_data(ctx, host, record, style=style, table_id=table_id),
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class BulkAction(Action):
|
|
348
|
+
"""An action run on the selected table rows. The closure gets ``records``."""
|
|
349
|
+
|
|
350
|
+
scope = "bulk"
|
|
351
|
+
default_style = "soft"
|
|
352
|
+
|
|
353
|
+
def __init__(self, name: str) -> None:
|
|
354
|
+
super().__init__(name)
|
|
355
|
+
self._color = "gray"
|
|
356
|
+
|
|
357
|
+
def is_available(self, host: "Host | None", ctx: "Context", record: Any = None) -> bool:
|
|
358
|
+
ev = self.ev(ctx, None, host=host)
|
|
359
|
+
return self.is_visible(**ev) and self.is_authorized(host, ctx, None)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
class ActionGroup(Component):
|
|
363
|
+
"""Several actions behind one dropdown button (``...``)."""
|
|
364
|
+
|
|
365
|
+
def __init__(self, actions: list[Action]) -> None:
|
|
366
|
+
super().__init__()
|
|
367
|
+
self.actions = list(actions)
|
|
368
|
+
self._label: Any = "More actions"
|
|
369
|
+
self._icon: Any = "ellipsis"
|
|
370
|
+
self._color = "gray"
|
|
371
|
+
self._button = False
|
|
372
|
+
self._style = None
|
|
373
|
+
|
|
374
|
+
def label(self, label: Any) -> "ActionGroup":
|
|
375
|
+
self._label = label
|
|
376
|
+
return self
|
|
377
|
+
|
|
378
|
+
def icon(self, icon: Any) -> "ActionGroup":
|
|
379
|
+
self._icon = icon
|
|
380
|
+
return self
|
|
381
|
+
|
|
382
|
+
def color(self, color: str) -> "ActionGroup":
|
|
383
|
+
self._color = color
|
|
384
|
+
return self
|
|
385
|
+
|
|
386
|
+
def button(self) -> "ActionGroup":
|
|
387
|
+
"""Show as a labelled button instead of an icon."""
|
|
388
|
+
self._button = True
|
|
389
|
+
return self
|
|
390
|
+
|
|
391
|
+
def flatten(self) -> list[Action]:
|
|
392
|
+
out: list[Action] = []
|
|
393
|
+
for a in self.actions:
|
|
394
|
+
out.extend(a.flatten() if isinstance(a, ActionGroup) else [a])
|
|
395
|
+
return out
|
|
396
|
+
|
|
397
|
+
def render(self, ctx: "Context", host: "Host | None", record: Any = None, *, style: str | None = None,
|
|
398
|
+
table_id: str | None = None) -> Markup:
|
|
399
|
+
items = [a.view_data(ctx, host, record, style="dropdown", table_id=table_id)
|
|
400
|
+
for a in self.flatten() if a.is_available(host, ctx, record)]
|
|
401
|
+
if not items:
|
|
402
|
+
return Markup("")
|
|
403
|
+
return ctx.panel.renderer.render(
|
|
404
|
+
"tungsten/actions/group.html",
|
|
405
|
+
items=items,
|
|
406
|
+
label=maybe(evaluate(self._label, record=record)),
|
|
407
|
+
group_icon=self._icon,
|
|
408
|
+
color=self._color,
|
|
409
|
+
button=self._button or style == "soft",
|
|
410
|
+
soft=style == "soft",
|
|
411
|
+
table_id=table_id,
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
class BulkActionGroup(ActionGroup):
|
|
416
|
+
def __init__(self, actions: list[Action]) -> None:
|
|
417
|
+
super().__init__(actions)
|
|
418
|
+
self._button = True
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def flatten_actions(actions: list) -> list[Action]:
|
|
422
|
+
out: list[Action] = []
|
|
423
|
+
for a in actions or []:
|
|
424
|
+
out.extend(a.flatten() if isinstance(a, ActionGroup) else [a])
|
|
425
|
+
return out
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _to_state(form: "Form", key: str, value: Any) -> Any:
|
|
429
|
+
found = form.find(key)
|
|
430
|
+
if found:
|
|
431
|
+
return found[0].to_state(value)
|
|
432
|
+
return value
|