polyadmin 0.1.0b1__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.
- polyadmin/__init__.py +47 -0
- polyadmin/core/__init__.py +0 -0
- polyadmin/core/_async.py +21 -0
- polyadmin/core/action.py +135 -0
- polyadmin/core/admin.py +129 -0
- polyadmin/core/audit.py +65 -0
- polyadmin/core/auth.py +52 -0
- polyadmin/core/authorization.py +48 -0
- polyadmin/core/csrf.py +70 -0
- polyadmin/core/dashboard.py +41 -0
- polyadmin/core/delete.py +146 -0
- polyadmin/core/exporter.py +136 -0
- polyadmin/core/field.py +201 -0
- polyadmin/core/filter.py +305 -0
- polyadmin/core/inline.py +97 -0
- polyadmin/core/login.py +110 -0
- polyadmin/core/model_admin.py +349 -0
- polyadmin/core/page.py +62 -0
- polyadmin/core/pagination.py +70 -0
- polyadmin/core/query.py +243 -0
- polyadmin/core/relation.py +40 -0
- polyadmin/core/slug.py +57 -0
- polyadmin/core/template_context.py +679 -0
- polyadmin/core/widget.py +280 -0
- polyadmin/fastapi/__init__.py +3 -0
- polyadmin/fastapi/audit.py +52 -0
- polyadmin/fastapi/auth.py +112 -0
- polyadmin/fastapi/csrf.py +93 -0
- polyadmin/fastapi/deletes.py +76 -0
- polyadmin/fastapi/errors.py +93 -0
- polyadmin/fastapi/handlers.py +798 -0
- polyadmin/fastapi/inlines.py +177 -0
- polyadmin/fastapi/locale.py +128 -0
- polyadmin/fastapi/login.py +110 -0
- polyadmin/fastapi/pages.py +97 -0
- polyadmin/fastapi/relations.py +264 -0
- polyadmin/fastapi/responses.py +55 -0
- polyadmin/fastapi/router.py +174 -0
- polyadmin/fastapi/static.py +21 -0
- polyadmin/i18n/__init__.py +50 -0
- polyadmin/i18n/context.py +54 -0
- polyadmin/i18n/negotiation.py +56 -0
- polyadmin/i18n/setup.py +81 -0
- polyadmin/i18n/translator.py +124 -0
- polyadmin/locale/fr/LC_MESSAGES/polyadmin.mo +0 -0
- polyadmin/locale/fr/LC_MESSAGES/polyadmin.po +486 -0
- polyadmin/locale/polyadmin.pot +485 -0
- polyadmin/locale/ru/LC_MESSAGES/polyadmin.mo +0 -0
- polyadmin/locale/ru/LC_MESSAGES/polyadmin.po +496 -0
- polyadmin/templates/admin/base.html +91 -0
- polyadmin/templates/admin/components/action_confirm_modal.html +86 -0
- polyadmin/templates/admin/components/csrf-field.html +5 -0
- polyadmin/templates/admin/components/error_fragment.html +6 -0
- polyadmin/templates/admin/components/field.html +60 -0
- polyadmin/templates/admin/components/form_wrapper.html +131 -0
- polyadmin/templates/admin/components/icons.html +67 -0
- polyadmin/templates/admin/components/inline.html +251 -0
- polyadmin/templates/admin/components/inline_fragment.html +2 -0
- polyadmin/templates/admin/components/list_content.html +54 -0
- polyadmin/templates/admin/components/lookup_results.html +19 -0
- polyadmin/templates/admin/components/search.html +19 -0
- polyadmin/templates/admin/components/toasts.html +151 -0
- polyadmin/templates/admin/components/ui/breadcrumb.html +30 -0
- polyadmin/templates/admin/components/ui/bulk-actions.html +69 -0
- polyadmin/templates/admin/components/ui/calendar.html +175 -0
- polyadmin/templates/admin/components/ui/combobox.html +82 -0
- polyadmin/templates/admin/components/ui/delete-preview.html +37 -0
- polyadmin/templates/admin/components/ui/dropdown-menu.html +71 -0
- polyadmin/templates/admin/components/ui/field.html +110 -0
- polyadmin/templates/admin/components/ui/filter-panel.html +155 -0
- polyadmin/templates/admin/components/ui/locale-switcher.html +30 -0
- polyadmin/templates/admin/components/ui/multi-select.html +253 -0
- polyadmin/templates/admin/components/ui/pagination.html +81 -0
- polyadmin/templates/admin/components/ui/radio-group.html +28 -0
- polyadmin/templates/admin/components/ui/select.html +165 -0
- polyadmin/templates/admin/components/ui/sidebar.html +175 -0
- polyadmin/templates/admin/components/ui/slider.html +22 -0
- polyadmin/templates/admin/components/ui/switch.html +36 -0
- polyadmin/templates/admin/components/ui/table.html +221 -0
- polyadmin/templates/admin/components/ui/theme-toggle.html +33 -0
- polyadmin/templates/admin/dashboard.html +35 -0
- polyadmin/templates/admin/error.html +33 -0
- polyadmin/templates/admin/login.html +94 -0
- polyadmin/templates/admin/resource/delete.html +29 -0
- polyadmin/templates/admin/resource/delete_selected.html +49 -0
- polyadmin/templates/admin/resource/detail.html +78 -0
- polyadmin/templates/admin/resource/form.html +5 -0
- polyadmin/templates/admin/resource/list.html +5 -0
- polyadmin/templates/admin/theme.html +372 -0
- polyadmin/templates/admin/widgets/activity.html +8 -0
- polyadmin/templates/admin/widgets/chart.html +15 -0
- polyadmin/templates/admin/widgets/donut.html +59 -0
- polyadmin/templates/admin/widgets/metric.html +1 -0
- polyadmin/templates/admin/widgets/progress.html +7 -0
- polyadmin/templates/admin/widgets/stat.html +22 -0
- polyadmin/templates/admin/widgets/table.html +29 -0
- polyadmin/templates/admin/widgets/tabs.html +34 -0
- polyadmin/templates/admin/widgets/timeline.html +21 -0
- polyadmin/templating.py +528 -0
- polyadmin/ui.py +817 -0
- polyadmin-0.1.0b1.dist-info/METADATA +239 -0
- polyadmin-0.1.0b1.dist-info/RECORD +104 -0
- polyadmin-0.1.0b1.dist-info/WHEEL +4 -0
- polyadmin-0.1.0b1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{# Light/dark toggle, plus the tooltip that labels it.
|
|
2
|
+
|
|
3
|
+
The button reads and writes $store.theme, which owns both the `dark`
|
|
4
|
+
class on <html> and the localStorage write, so this is pure
|
|
5
|
+
presentation and a second copy cannot fall out of sync with it.
|
|
6
|
+
|
|
7
|
+
The tooltip is CSS-only -- `peer-hover:`/`peer-focus-visible:` on a
|
|
8
|
+
sibling -- rather than shadcn's Radix version, which portals and
|
|
9
|
+
tracks with Floating UI: neither is needed for a label pinned under a
|
|
10
|
+
fixed-size icon button, and this costs no JS. `pointer-events-none`
|
|
11
|
+
keeps it from eating the hover it depends on. Right-aligned, not
|
|
12
|
+
centred: the toggle ends the header, and a centred label wider than
|
|
13
|
+
the button overhung the viewport and scrolled every page sideways. #}
|
|
14
|
+
{% from "admin/components/icons.html" import icon %}
|
|
15
|
+
{% macro theme_toggle() %}
|
|
16
|
+
<span class="relative inline-flex">
|
|
17
|
+
<button type="button" @click="$store.theme.toggle()"
|
|
18
|
+
class="peer {{ ui('button', 'ghost-muted', 'size-icon-sm') }}"
|
|
19
|
+
:aria-pressed="$store.theme.dark ? 'true' : 'false'"
|
|
20
|
+
aria-label="{{ _('Toggle dark mode') }}">
|
|
21
|
+
{# No x-cloak: with Alpine still loading, x-show has not run and
|
|
22
|
+
both icons would show at once. The sun is the server-rendered
|
|
23
|
+
default and the moon starts hidden, so the pre-Alpine frame shows
|
|
24
|
+
exactly one icon either way. #}
|
|
25
|
+
<span x-show="!$store.theme.dark">{{ icon("sun", "w-4 h-4") }}</span>
|
|
26
|
+
<span x-show="$store.theme.dark" style="display: none">{{ icon("moon", "w-4 h-4") }}</span>
|
|
27
|
+
</button>
|
|
28
|
+
<span role="tooltip"
|
|
29
|
+
class="pointer-events-none absolute right-0 top-full z-50 mt-1.5 whitespace-nowrap opacity-0 transition-opacity duration-150 peer-hover:opacity-100 peer-focus-visible:opacity-100 {{ ui('tooltip', 'content') }}">
|
|
30
|
+
<span x-text='$store.theme.dark ? {{ _("Light mode")|tojson }} : {{ _("Dark mode")|tojson }}'>{{ _("Dark mode") }}</span>
|
|
31
|
+
</span>
|
|
32
|
+
</span>
|
|
33
|
+
{% endmacro %}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{% extends "admin/base.html" %}
|
|
2
|
+
{% from "admin/components/icons.html" import icon %}
|
|
3
|
+
{% block title %}{{ _(dashboard.title) }}{% endblock %}
|
|
4
|
+
{% block content %}
|
|
5
|
+
{% set widget_icons = {
|
|
6
|
+
"admin/widgets/metric.html": "metric",
|
|
7
|
+
"admin/widgets/progress.html": "progress",
|
|
8
|
+
"admin/widgets/table.html": "table",
|
|
9
|
+
"admin/widgets/chart.html": "chart",
|
|
10
|
+
"admin/widgets/activity.html": "activity",
|
|
11
|
+
"admin/widgets/donut.html": "donut",
|
|
12
|
+
"admin/widgets/stat.html": "stat",
|
|
13
|
+
"admin/widgets/timeline.html": "timeline",
|
|
14
|
+
"admin/widgets/tabs.html": "tabs",
|
|
15
|
+
} %}
|
|
16
|
+
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
|
17
|
+
{% for widget in widgets %}
|
|
18
|
+
{% set widget_data = widget.get_data() %}
|
|
19
|
+
<div class="{{ ui('widget') }} {% if widget.size == 'lg' %}{{ ui('widget', 'span-lg') }}{% endif %}">
|
|
20
|
+
<div class="{{ ui('widget', 'header') }}">
|
|
21
|
+
<span class="{{ ui('widget', 'icon') }}">
|
|
22
|
+
{{ icon(widget_icons.get(widget.template, "metric"), "w-5 h-5") }}
|
|
23
|
+
</span>
|
|
24
|
+
<h2 class="{{ ui('widget', 'title') }}">{{ _(widget.title) }}</h2>
|
|
25
|
+
</div>
|
|
26
|
+
{# Bounded, so a widget with hundreds of rows scrolls inside its own
|
|
27
|
+
card instead of stretching every card beside it. #}
|
|
28
|
+
<div class="{{ ui('widget', 'body') }}">{% include widget.template %}</div>
|
|
29
|
+
</div>
|
|
30
|
+
{% endfor %}
|
|
31
|
+
{% if not widgets %}
|
|
32
|
+
<p class="{{ ui('text', 'muted') }}">{{ _("No widgets configured.") }}</p>
|
|
33
|
+
{% endif %}
|
|
34
|
+
</div>
|
|
35
|
+
{% endblock %}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{# Error page: 401, 403, 404, and a rejected CSRF token.
|
|
2
|
+
|
|
3
|
+
Outside the admin shell, like login.html and for a related reason: a 401
|
|
4
|
+
has no principal to build a sidebar for, and a 403's sidebar would
|
|
5
|
+
advertise resources the visitor has just been told they cannot reach.
|
|
6
|
+
What replaces it is one link home, which is the thing a bare "Permission
|
|
7
|
+
denied." text response never gave anyone.
|
|
8
|
+
|
|
9
|
+
It still pulls in the theme, so an error in a dark-mode admin is not a
|
|
10
|
+
white flash. #}
|
|
11
|
+
{% from "admin/components/icons.html" import icon %}
|
|
12
|
+
<!doctype html>
|
|
13
|
+
<html lang="{{ locale }}" class="h-full">
|
|
14
|
+
<head>
|
|
15
|
+
<meta charset="utf-8">
|
|
16
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
17
|
+
<title>{{ title }} · {{ site_title }}</title>
|
|
18
|
+
{% include "admin/theme.html" %}
|
|
19
|
+
</head>
|
|
20
|
+
<body class="h-full bg-background text-foreground antialiased">
|
|
21
|
+
<div class="{{ ui('error', 'page') }}">
|
|
22
|
+
<div class="{{ ui('error', 'card') }}">
|
|
23
|
+
<p class="{{ ui('error', 'status') }}">{{ status }}</p>
|
|
24
|
+
<h1 class="{{ ui('error', 'title') }}">{{ title }}</h1>
|
|
25
|
+
<p class="{{ ui('error', 'message') }}">{{ message }}</p>
|
|
26
|
+
{% if home_url %}
|
|
27
|
+
<a href="{{ home_url }}" class="{{ ui('button', 'outline') }} {{ ui('button', 'size-sm') }}">
|
|
28
|
+
{{ icon("home", "size-4") }} {{ _("Back to the admin") }}</a>
|
|
29
|
+
{% endif %}
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{# Login page, ported from shadcn/ui's login-04 block.
|
|
2
|
+
|
|
3
|
+
The one page that does not extend base.html, and the reason a bare
|
|
4
|
+
layout exists: base.html wraps a nav built from what the current
|
|
5
|
+
principal may see, and here there is no principal yet. It still
|
|
6
|
+
includes admin/theme.html, so signing in does not flash a light page
|
|
7
|
+
at someone who chose dark.
|
|
8
|
+
|
|
9
|
+
Deliberately absent from the port: the social buttons, "Sign up" and
|
|
10
|
+
"Forgot your password?" -- the framework has no route behind any of
|
|
11
|
+
them. #}
|
|
12
|
+
{% from "admin/components/csrf-field.html" import csrf_field %}
|
|
13
|
+
{% from "admin/components/ui/locale-switcher.html" import locale_switcher_menu %}
|
|
14
|
+
<!doctype html>
|
|
15
|
+
<html lang="{{ locale }}" class="h-full">
|
|
16
|
+
<head>
|
|
17
|
+
<meta charset="utf-8">
|
|
18
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
19
|
+
{# No hx-* on this page, so the token travels as a hidden field only --
|
|
20
|
+
but the meta tag stays for consistency with base.html. #}
|
|
21
|
+
<meta name="csrf-token" content="{{ csrf_token }}">
|
|
22
|
+
<title>{{ _("Sign in") }} · {{ _(site_title) }}</title>
|
|
23
|
+
{% include "admin/theme.html" %}
|
|
24
|
+
</head>
|
|
25
|
+
<body class="relative h-full bg-background text-foreground antialiased">
|
|
26
|
+
{% if locale_switcher %}<div class="absolute right-4 top-4">{{ locale_switcher_menu(locale_switcher, base_path, csrf_token) }}</div>{% endif %}
|
|
27
|
+
<div class="{{ ui('login', 'page') }}">
|
|
28
|
+
<div class="{{ ui('login', 'container') }}">
|
|
29
|
+
<div class="{{ ui('card') }} overflow-hidden">
|
|
30
|
+
<div class="{{ ui('login', 'grid') }}">
|
|
31
|
+
{# method="post" with no action: the form posts back to the
|
|
32
|
+
same URL, which carries ?next= through a failed attempt
|
|
33
|
+
without needing a second hidden field. #}
|
|
34
|
+
<form method="post" class="{{ ui('login', 'form') }}">
|
|
35
|
+
<div class="{{ ui('login', 'stack') }}">
|
|
36
|
+
<div class="{{ ui('login', 'heading') }}">
|
|
37
|
+
<h1 class="{{ ui('login', 'title') }}">{{ _("Welcome back") }}</h1>
|
|
38
|
+
<p class="{{ ui('login', 'subtitle') }}">{{ _("Sign in to %(site)s", site=_(site_title)) }}</p>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
{# One message for a bad password and for no such user
|
|
42
|
+
alike -- see core/login.py's LoginBackend on why the
|
|
43
|
+
backend must not distinguish them either. #}
|
|
44
|
+
{% if error %}
|
|
45
|
+
<div class="{{ ui('alert', 'destructive') }}" role="alert">
|
|
46
|
+
<p class="{{ ui('alert', 'description') }}">{{ error }}</p>
|
|
47
|
+
</div>
|
|
48
|
+
{% endif %}
|
|
49
|
+
{# Not an error: what logout redirects back with. #}
|
|
50
|
+
{% if notice %}
|
|
51
|
+
<div class="{{ ui('alert') }}" role="status">
|
|
52
|
+
<p class="{{ ui('alert', 'description') }}">{{ notice }}</p>
|
|
53
|
+
</div>
|
|
54
|
+
{% endif %}
|
|
55
|
+
|
|
56
|
+
<div class="{{ ui('login', 'group') }}">
|
|
57
|
+
<label for="login-identifier" class="{{ ui('label') }}">{{ _("Email") }}</label>
|
|
58
|
+
{# The submitted value is echoed back so a typo in the
|
|
59
|
+
password does not cost the email as well. #}
|
|
60
|
+
<input type="email" id="login-identifier" name="identifier"
|
|
61
|
+
value="{{ identifier }}" placeholder="{{ _('m@example.com') }}"
|
|
62
|
+
autocomplete="username" required autofocus
|
|
63
|
+
class="{{ ui('input') }}">
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div class="{{ ui('login', 'group') }}">
|
|
67
|
+
<label for="login-password" class="{{ ui('label') }}">{{ _("Password") }}</label>
|
|
68
|
+
<input type="password" id="login-password" name="password"
|
|
69
|
+
autocomplete="current-password" required
|
|
70
|
+
class="{{ ui('input') }}">
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
{{ csrf_field(csrf_token) }}
|
|
74
|
+
<button type="submit" class="{{ ui('button') }} w-full">{{ _("Sign in") }}</button>
|
|
75
|
+
</div>
|
|
76
|
+
</form>
|
|
77
|
+
|
|
78
|
+
{# login-04's photograph column. A framework has no photo to
|
|
79
|
+
ship, so this is the site's own identity instead -- which is
|
|
80
|
+
more use to someone who administers several. #}
|
|
81
|
+
<div class="{{ ui('login', 'aside') }}">
|
|
82
|
+
<div class="{{ ui('login', 'panel') }}">
|
|
83
|
+
{% if site_logo_url %}
|
|
84
|
+
<img src="{{ site_logo_url }}" alt="" class="{{ ui('login', 'logo') }}">
|
|
85
|
+
{% endif %}
|
|
86
|
+
<span class="{{ ui('login', 'brand') }}">{{ _(site_title) }}</span>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</body>
|
|
94
|
+
</html>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{% extends "admin/base.html" %}
|
|
2
|
+
{% from "admin/components/csrf-field.html" import csrf_field %}
|
|
3
|
+
{% from "admin/components/icons.html" import icon %}
|
|
4
|
+
{% from "admin/components/ui/delete-preview.html" import delete_preview %}
|
|
5
|
+
{% block title %}{{ _("Delete %(name)s", name=_(model_admin.get_verbose_name())) }}{% endblock %}
|
|
6
|
+
{% block content %}
|
|
7
|
+
<form method="post" class="{{ ui('panel', 'form') }} max-w-xl">
|
|
8
|
+
{{ csrf_field(csrf_token) }}
|
|
9
|
+
{% if list_token %}<input type="hidden" name="_list" value="{{ list_token }}">{% endif %}
|
|
10
|
+
<div class="flex items-start gap-3">
|
|
11
|
+
<span class="inline-flex shrink-0 items-center justify-center rounded-full bg-destructive/10 p-2 text-destructive">
|
|
12
|
+
{{ icon("delete", "w-5 h-5") }}
|
|
13
|
+
</span>
|
|
14
|
+
<div class="mt-1.5">
|
|
15
|
+
<p class="{{ ui('text', 'heading') }}">{{ _("Delete «%(label)s»?", label=object_label) }}</p>
|
|
16
|
+
<p class="mt-1 {{ ui('text', 'muted') }}">{{ _("This action cannot be undone.") }}</p>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
{{ delete_preview(preview) }}
|
|
20
|
+
<div class="mt-4 flex flex-col gap-2 sm:flex-row">
|
|
21
|
+
{% if not preview.blocked %}
|
|
22
|
+
<button type="submit" id="delete-confirm" class="{{ ui('button', 'destructive') }}">
|
|
23
|
+
{{ icon("delete", "w-4 h-4") }} {{ _("Delete") }}
|
|
24
|
+
</button>
|
|
25
|
+
{% endif %}
|
|
26
|
+
<a href="{{ base_path }}/{{ model_admin.get_slug() }}" class="{{ ui('button', 'outline') }}">{{ _("Cancel") }}</a>
|
|
27
|
+
</div>
|
|
28
|
+
</form>
|
|
29
|
+
{% endblock %}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{% extends "admin/base.html" %}
|
|
2
|
+
{% from "admin/components/csrf-field.html" import csrf_field %}
|
|
3
|
+
{% from "admin/components/icons.html" import icon %}
|
|
4
|
+
{% from "admin/components/ui/delete-preview.html" import delete_preview %}
|
|
5
|
+
{% block title %}{{ heading }}{% endblock %}
|
|
6
|
+
{% block content %}
|
|
7
|
+
<form method="post" action="{{ base_path }}/{{ model_admin.get_slug() }}/actions/delete_selected" class="{{ ui('panel', 'form') }} max-w-xl">
|
|
8
|
+
{{ csrf_field(csrf_token) }}
|
|
9
|
+
<input type="hidden" name="_confirmed" value="1">
|
|
10
|
+
<input type="hidden" name="_return" value="{{ selection.return_to }}">
|
|
11
|
+
{% if selection.select_all %}
|
|
12
|
+
<input type="hidden" name="_select_all" value="1">
|
|
13
|
+
<input type="hidden" name="_fingerprint" value="{{ selection.fingerprint }}">
|
|
14
|
+
<input type="hidden" name="search" value="{{ selection.list_request.search or '' }}">
|
|
15
|
+
<input type="hidden" name="sort" value="{{ selection.list_request.ordering or '' }}">
|
|
16
|
+
{% for name, value in selection.list_request.filters.items() %}<input type="hidden" name="filter[{{ name }}]" value="{{ value }}">
|
|
17
|
+
{% endfor %}
|
|
18
|
+
{% else %}
|
|
19
|
+
{% for pk in selection.pks %}<input type="hidden" name="pks" value="{{ pk }}">
|
|
20
|
+
{% endfor %}
|
|
21
|
+
{% endif %}
|
|
22
|
+
{% if selection.changed %}
|
|
23
|
+
<div role="alert" class="{{ ui('alert', 'destructive') }} mb-4"><p class="{{ ui('alert', 'description') }}">{{ _("The selection changed since you reviewed it.") }}</p></div>
|
|
24
|
+
{% endif %}
|
|
25
|
+
<div class="flex items-start gap-3">
|
|
26
|
+
<span class="inline-flex shrink-0 items-center justify-center rounded-full bg-destructive/10 p-2 text-destructive">{{ icon("delete", "w-5 h-5") }}</span>
|
|
27
|
+
<div class="mt-1.5">
|
|
28
|
+
<p class="{{ ui('text', 'heading') }}">{{ heading }}</p>
|
|
29
|
+
<p class="mt-1 {{ ui('text', 'muted') }}">{{ _("This action cannot be undone.") }}</p>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
<section class="{{ ui('panel') }} mt-4">
|
|
33
|
+
<h3 class="{{ ui('text', 'heading') }}">{{ _("Selected records") }}</h3>
|
|
34
|
+
<ul class="mt-2 flex flex-col gap-1 text-sm">
|
|
35
|
+
{% for item in items %}
|
|
36
|
+
<li>{% if item.url %}<a href="{{ item.url }}" class="{{ ui('text', 'link') }}">{{ item.label }}</a>{% else %}{{ item.label }}{% endif %}</li>
|
|
37
|
+
{% endfor %}
|
|
38
|
+
{% if more %}<li class="{{ ui('text', 'muted') }}">{{ ngettext("…and %(num)d more", "…and %(num)d more", more) }}</li>{% endif %}
|
|
39
|
+
</ul>
|
|
40
|
+
</section>
|
|
41
|
+
{{ delete_preview(preview) }}
|
|
42
|
+
<div class="mt-4 flex flex-col gap-2 sm:flex-row">
|
|
43
|
+
{% if not preview.blocked %}
|
|
44
|
+
<button type="submit" id="delete-confirm" class="{{ ui('button', 'destructive') }}">{{ icon("delete", "w-4 h-4") }} {{ _("Delete") }}</button>
|
|
45
|
+
{% endif %}
|
|
46
|
+
<a href="{{ selection.return_to }}" class="{{ ui('button', 'outline') }}">{{ _("Cancel") }}</a>
|
|
47
|
+
</div>
|
|
48
|
+
</form>
|
|
49
|
+
{% endblock %}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{% extends "admin/base.html" %}
|
|
2
|
+
{% from "admin/components/csrf-field.html" import csrf_field %}
|
|
3
|
+
{% from "admin/components/field.html" import render_field_value with context %}
|
|
4
|
+
{% from "admin/components/inline.html" import render_inline_section with context %}
|
|
5
|
+
{% from "admin/components/icons.html" import icon %}
|
|
6
|
+
{% block title %}{{ _(model_admin.get_verbose_name()) }}{% endblock %}
|
|
7
|
+
{% block content %}
|
|
8
|
+
{# Layout comes from ui("page"), shared with the form, so viewing and
|
|
9
|
+
editing the same record do not jump width or alignment.
|
|
10
|
+
|
|
11
|
+
Edit, Delete and the record actions sit below the record, not above
|
|
12
|
+
it -- the record is what a viewer came to read -- and stay reachable
|
|
13
|
+
on a long one thanks to the pinned bar. #}
|
|
14
|
+
<div class="{{ ui('page') }}">
|
|
15
|
+
<div class="{{ ui('page', 'body-wide') if wide_body else ui('page', 'body') }}">
|
|
16
|
+
<dl class="{{ ui('card') }} divide-y divide-border">
|
|
17
|
+
{% for name in detail_fields %}
|
|
18
|
+
{% set field = model_admin.get_field(name) %}
|
|
19
|
+
<div class="grid grid-cols-1 gap-1 px-4 py-3 sm:grid-cols-3 sm:gap-4">
|
|
20
|
+
{# A field's help_text deliberately does not appear here: it
|
|
21
|
+
explains how to fill a field in, which is a question the form
|
|
22
|
+
answers and this page does not ask. See ui/field.html. #}
|
|
23
|
+
<dt class="text-sm font-medium text-muted-foreground">{{ _(field.label) }}</dt>
|
|
24
|
+
<dd class="text-sm text-foreground sm:col-span-2">{{ render_field_value(field, field.get_value(object), model_admin.get_empty_value()) }}</dd>
|
|
25
|
+
</div>
|
|
26
|
+
{% endfor %}
|
|
27
|
+
</dl>
|
|
28
|
+
{% for inline in inlines %}{{ render_inline_section(inline) }}{% endfor %}
|
|
29
|
+
{# Only rendered when the configured audit logger can read back
|
|
30
|
+
(core.audit.AuditReader) -- a write-only logger records silently. #}
|
|
31
|
+
{% if history %}
|
|
32
|
+
<div class="{{ ui('history') }}">
|
|
33
|
+
<h2 class="{{ ui('history', 'title') }}">{{ _("History") }}</h2>
|
|
34
|
+
<ul class="divide-y divide-border">
|
|
35
|
+
{% for entry in history %}
|
|
36
|
+
<li class="{{ ui('history', 'row') }}">
|
|
37
|
+
<span><span class="{{ ui('history', 'who') }}">{{ entry.who }}</span>
|
|
38
|
+
<span class="{{ ui('history', 'what') }}">{{ entry.what }}</span></span>
|
|
39
|
+
<span class="{{ ui('history', 'when') }}">{{ entry.when }}</span>
|
|
40
|
+
</li>
|
|
41
|
+
{% endfor %}
|
|
42
|
+
</ul>
|
|
43
|
+
</div>
|
|
44
|
+
{% endif %}
|
|
45
|
+
</div>
|
|
46
|
+
{# No Delete here -- it lives on the edit form, so destroying a record
|
|
47
|
+
is something you opt into by entering edit mode rather than a click
|
|
48
|
+
away from merely looking at one. #}
|
|
49
|
+
<div class="{{ ui('page', 'actions') }}">
|
|
50
|
+
<div class="{{ ui('page', 'actions-inner-wide') if wide_body else ui('page', 'actions-inner') }}">
|
|
51
|
+
<div class="{{ ui('page', 'actions-primary') }}">
|
|
52
|
+
{% if permissions.can_update %}
|
|
53
|
+
<a href="{{ base_path }}/{{ model_admin.get_slug() }}/{{ model_admin.get_pk(object) }}/edit"
|
|
54
|
+
class="{{ ui('button', 'outline') }}">
|
|
55
|
+
{{ icon("edit", "w-4 h-4") }} {{ _("Edit") }}
|
|
56
|
+
</a>
|
|
57
|
+
{% endif %}
|
|
58
|
+
{% for action in actions %}
|
|
59
|
+
{# w-full sm:w-auto tracks "actions-primary"'s flex-col ->
|
|
60
|
+
sm:flex-row switch by hand: stretch gives Edit its full width
|
|
61
|
+
as a direct child, but an action's button is a grandchild --
|
|
62
|
+
the stretch stops at the <form> -- and would otherwise sit
|
|
63
|
+
stunted beside a full-width Edit. #}
|
|
64
|
+
<form method="post" action="{{ base_path }}/{{ model_admin.get_slug() }}/actions/{{ action.name }}"
|
|
65
|
+
{% if action.confirm %}data-confirm="{{ _(action.confirm) }}"{% endif %} {% if action.preview %}data-preview{% endif %}
|
|
66
|
+
onsubmit="return window.adminConfirmSubmit(event, this, this.dataset.confirm)">
|
|
67
|
+
{{ csrf_field(csrf_token) }}
|
|
68
|
+
<input type="hidden" name="pks" value="{{ model_admin.get_pk(object) }}">
|
|
69
|
+
<button type="submit" class="{{ ui('button', 'outline') }} w-full sm:w-auto">
|
|
70
|
+
{{ _(action.label) }}
|
|
71
|
+
</button>
|
|
72
|
+
</form>
|
|
73
|
+
{% endfor %}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
{% endblock %}
|