fastapi-admin-kit 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.
- fastapi_admin_kit/__init__.py +73 -0
- fastapi_admin_kit/actions/__init__.py +63 -0
- fastapi_admin_kit/actions/base.py +68 -0
- fastapi_admin_kit/actions/registry.py +43 -0
- fastapi_admin_kit/admin/__init__.py +17 -0
- fastapi_admin_kit/admin/admin_config.py +95 -0
- fastapi_admin_kit/admin/admin_database.py +138 -0
- fastapi_admin_kit/admin/admin_router.py +74 -0
- fastapi_admin_kit/admin/admin_template.py +203 -0
- fastapi_admin_kit/admin/builtin_models.py +284 -0
- fastapi_admin_kit/admin/core.py +1036 -0
- fastapi_admin_kit/admin/decorators.py +70 -0
- fastapi_admin_kit/admin/state.py +76 -0
- fastapi_admin_kit/admin.py +728 -0
- fastapi_admin_kit/api/__init__.py +44 -0
- fastapi_admin_kit/api/auth.py +342 -0
- fastapi_admin_kit/api/crud.py +128 -0
- fastapi_admin_kit/api/deps.py +79 -0
- fastapi_admin_kit/api/roles.py +128 -0
- fastapi_admin_kit/api/schema_generator.py +171 -0
- fastapi_admin_kit/api/schemas.py +81 -0
- fastapi_admin_kit/api/search.py +132 -0
- fastapi_admin_kit/audit/__init__.py +36 -0
- fastapi_admin_kit/audit/context.py +62 -0
- fastapi_admin_kit/audit/diff.py +77 -0
- fastapi_admin_kit/audit/event_bus.py +96 -0
- fastapi_admin_kit/audit/events.py +48 -0
- fastapi_admin_kit/audit/listener.py +159 -0
- fastapi_admin_kit/audit/logger.py +28 -0
- fastapi_admin_kit/audit/middleware.py +39 -0
- fastapi_admin_kit/audit/models.py +53 -0
- fastapi_admin_kit/audit/sqlalchemy_logger.py +58 -0
- fastapi_admin_kit/auth/__init__.py +34 -0
- fastapi_admin_kit/auth/backend.py +95 -0
- fastapi_admin_kit/auth/csrf.py +240 -0
- fastapi_admin_kit/auth/dependencies.py +150 -0
- fastapi_admin_kit/auth/identity.py +181 -0
- fastapi_admin_kit/auth/models.py +246 -0
- fastapi_admin_kit/auth/password.py +35 -0
- fastapi_admin_kit/auth/permissions.py +205 -0
- fastapi_admin_kit/auth/protocol.py +22 -0
- fastapi_admin_kit/auth/ratelimit.py +88 -0
- fastapi_admin_kit/auth/router.py +10 -0
- fastapi_admin_kit/auth/session.py +79 -0
- fastapi_admin_kit/auth/totp.py +83 -0
- fastapi_admin_kit/auth/views.py +165 -0
- fastapi_admin_kit/cli.py +229 -0
- fastapi_admin_kit/config/__init__.py +19 -0
- fastapi_admin_kit/config/audit.py +18 -0
- fastapi_admin_kit/config/auth.py +54 -0
- fastapi_admin_kit/config/behavior.py +27 -0
- fastapi_admin_kit/config/nav.py +32 -0
- fastapi_admin_kit/config/storage.py +22 -0
- fastapi_admin_kit/config/theme.py +215 -0
- fastapi_admin_kit/config/ui.py +147 -0
- fastapi_admin_kit/dashboard/__init__.py +64 -0
- fastapi_admin_kit/db.py +133 -0
- fastapi_admin_kit/exceptions.py +5 -0
- fastapi_admin_kit/field_types.py +81 -0
- fastapi_admin_kit/filters/__init__.py +21 -0
- fastapi_admin_kit/filters/base.py +170 -0
- fastapi_admin_kit/filters/registry.py +68 -0
- fastapi_admin_kit/flash.py +45 -0
- fastapi_admin_kit/form/__init__.py +1 -0
- fastapi_admin_kit/form/pipeline.py +106 -0
- fastapi_admin_kit/inspection/__init__.py +117 -0
- fastapi_admin_kit/inspection/registry.py +253 -0
- fastapi_admin_kit/inspection.py +115 -0
- fastapi_admin_kit/modeladmin.py +375 -0
- fastapi_admin_kit/models/__init__.py +7 -0
- fastapi_admin_kit/models/base.py +7 -0
- fastapi_admin_kit/nav.py +208 -0
- fastapi_admin_kit/pagination/__init__.py +14 -0
- fastapi_admin_kit/pagination/base.py +40 -0
- fastapi_admin_kit/pagination/cursor.py +97 -0
- fastapi_admin_kit/pagination/dynamic.py +48 -0
- fastapi_admin_kit/pagination/offset.py +42 -0
- fastapi_admin_kit/plugins/__init__.py +1 -0
- fastapi_admin_kit/py.typed +0 -0
- fastapi_admin_kit/registry/__init__.py +5 -0
- fastapi_admin_kit/registry/core.py +287 -0
- fastapi_admin_kit/registry/validation.py +107 -0
- fastapi_admin_kit/registry.py +15 -0
- fastapi_admin_kit/router.py +335 -0
- fastapi_admin_kit/static/css/admin.css +4736 -0
- fastapi_admin_kit/static/css/presets.css +317 -0
- fastapi_admin_kit/static/css/tokens.css +217 -0
- fastapi_admin_kit/static/css/variables.css +74 -0
- fastapi_admin_kit/static/icons/heroicons.svg +160 -0
- fastapi_admin_kit/static/js/admin.js +692 -0
- fastapi_admin_kit/static/js/htmx-config.js +42 -0
- fastapi_admin_kit/storage/__init__.py +6 -0
- fastapi_admin_kit/storage/base.py +48 -0
- fastapi_admin_kit/storage/local.py +73 -0
- fastapi_admin_kit/templates/base.html +142 -0
- fastapi_admin_kit/templates/macros/form_fields.html +660 -0
- fastapi_admin_kit/templates/macros/icons.html +50 -0
- fastapi_admin_kit/templates/macros/table.html +108 -0
- fastapi_admin_kit/templates/macros/widgets.html +159 -0
- fastapi_admin_kit/templates/pages/2fa/setup.html +122 -0
- fastapi_admin_kit/templates/pages/2fa/verify.html +55 -0
- fastapi_admin_kit/templates/pages/audit_detail.html +122 -0
- fastapi_admin_kit/templates/pages/audit_log.html +102 -0
- fastapi_admin_kit/templates/pages/dashboard.html +295 -0
- fastapi_admin_kit/templates/pages/detail.html +183 -0
- fastapi_admin_kit/templates/pages/form.html +119 -0
- fastapi_admin_kit/templates/pages/list.html +277 -0
- fastapi_admin_kit/templates/pages/login.html +85 -0
- fastapi_admin_kit/templates/pages/profile/password.html +78 -0
- fastapi_admin_kit/templates/pages/profile/profile.html +73 -0
- fastapi_admin_kit/templates/pages/role_form.html +75 -0
- fastapi_admin_kit/templates/pages/roles/form.html +117 -0
- fastapi_admin_kit/templates/pages/roles/list.html +69 -0
- fastapi_admin_kit/templates/pages/roles.html +77 -0
- fastapi_admin_kit/templates/pages/settings/theme.html +255 -0
- fastapi_admin_kit/templates/pages/users/form.html +229 -0
- fastapi_admin_kit/templates/pages/users/list.html +83 -0
- fastapi_admin_kit/templates/partials/command_palette.html +52 -0
- fastapi_admin_kit/templates/partials/field_wrapper.html +2 -0
- fastapi_admin_kit/templates/partials/flash_messages.html +39 -0
- fastapi_admin_kit/templates/partials/head.html +21 -0
- fastapi_admin_kit/templates/partials/head_minimal.html +18 -0
- fastapi_admin_kit/templates/partials/list_table.html +178 -0
- fastapi_admin_kit/templates/partials/mobile_backdrop.html +2 -0
- fastapi_admin_kit/templates/partials/pagination.html +82 -0
- fastapi_admin_kit/templates/partials/permission_widget.html +86 -0
- fastapi_admin_kit/templates/partials/scripts.html +13 -0
- fastapi_admin_kit/templates/partials/sidebar.html +94 -0
- fastapi_admin_kit/templates/partials/topbar.html +95 -0
- fastapi_admin_kit/types.py +145 -0
- fastapi_admin_kit/validation.py +43 -0
- fastapi_admin_kit/views/__init__.py +78 -0
- fastapi_admin_kit/views/audit.py +134 -0
- fastapi_admin_kit/views/bulk.py +28 -0
- fastapi_admin_kit/views/class_views.py +1040 -0
- fastapi_admin_kit/views/context.py +588 -0
- fastapi_admin_kit/views/dashboard.py +162 -0
- fastapi_admin_kit/views/delete.py +31 -0
- fastapi_admin_kit/views/extra.py +65 -0
- fastapi_admin_kit/views/factory.py +667 -0
- fastapi_admin_kit/views/form.py +159 -0
- fastapi_admin_kit/views/list.py +28 -0
- fastapi_admin_kit/views/profile.py +219 -0
- fastapi_admin_kit/views/protocols.py +54 -0
- fastapi_admin_kit/views/renderers.py +634 -0
- fastapi_admin_kit/views/roles.py +230 -0
- fastapi_admin_kit/views/search.py +31 -0
- fastapi_admin_kit/views/settings.py +31 -0
- fastapi_admin_kit/views/sidebar.py +101 -0
- fastapi_admin_kit/views/totp.py +249 -0
- fastapi_admin_kit/views/users.py +347 -0
- fastapi_admin_kit/views.py +117 -0
- fastapi_admin_kit/widgets/__init__.py +44 -0
- fastapi_admin_kit/widgets/base.py +44 -0
- fastapi_admin_kit/widgets/inputs.py +363 -0
- fastapi_admin_kit/widgets/registry.py +110 -0
- fastapi_admin_kit/widgets/relation.py +70 -0
- fastapi_admin_kit/widgets/resolver.py +102 -0
- fastapi_admin_kit-0.1.0.dist-info/METADATA +210 -0
- fastapi_admin_kit-0.1.0.dist-info/RECORD +163 -0
- fastapi_admin_kit-0.1.0.dist-info/WHEEL +4 -0
- fastapi_admin_kit-0.1.0.dist-info/entry_points.txt +3 -0
- fastapi_admin_kit-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
{# pages/audit_log.html — audit log timeline page #}
|
|
2
|
+
{% extends "base.html" %}
|
|
3
|
+
|
|
4
|
+
{% block title %}Audit Log — {{ title | default("Admin") }}{% endblock %}
|
|
5
|
+
|
|
6
|
+
{% block content %}
|
|
7
|
+
<div class="space-y-6">
|
|
8
|
+
<div class="page-header">
|
|
9
|
+
<div class="page-header-left">
|
|
10
|
+
<h1 class="page-header-title">Audit Log</h1>
|
|
11
|
+
<p class="page-header-subtitle">Track all changes across your models</p>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<div class="bg-white rounded-xl border border-[var(--color-gray-200)]">
|
|
16
|
+
<div class="px-6 py-4 border-b border-[var(--color-gray-200)]">
|
|
17
|
+
<div class="flex flex-col sm:flex-row sm:items-center gap-4">
|
|
18
|
+
<div class="flex-1">
|
|
19
|
+
<div class="relative">
|
|
20
|
+
{{ icon("magnifying-glass", class="absolute left-3 top-1/2 -translate-y-1/2 text-[var(--color-gray-400)]", size="20px") }}
|
|
21
|
+
<input type="text" name="search" value="{{ search | default('') }}"
|
|
22
|
+
placeholder="Search audit log..."
|
|
23
|
+
class="form-input w-full pl-10 pr-4 py-2 rounded-lg border border-[var(--color-gray-300)] bg-white text-[var(--color-gray-900)] focus:ring-2 focus:ring-[var(--color-primary-500)] focus:border-[var(--color-primary-500)] transition-colors">
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div class="flex items-center gap-2">
|
|
28
|
+
<select name="action" class="form-select px-3 py-2 rounded-lg border border-[var(--color-gray-300)] bg-white text-[var(--color-gray-900)] text-sm">
|
|
29
|
+
<option value="">All Actions</option>
|
|
30
|
+
<option value="CREATE" {% if action_filter == 'CREATE' %}selected{% endif %}>Create</option>
|
|
31
|
+
<option value="UPDATE" {% if action_filter == 'UPDATE' %}selected{% endif %}>Update</option>
|
|
32
|
+
<option value="DELETE" {% if action_filter == 'DELETE' %}selected{% endif %}>Delete</option>
|
|
33
|
+
</select>
|
|
34
|
+
|
|
35
|
+
<select name="model" class="form-select px-3 py-2 rounded-lg border border-[var(--color-gray-300)] bg-white text-[var(--color-gray-900)] text-sm">
|
|
36
|
+
<option value="">All Models</option>
|
|
37
|
+
{% for model_name in model_names %}
|
|
38
|
+
<option value="{{ model_name }}" {% if model_filter == model_name %}selected{% endif %}>{{ model_name }}</option>
|
|
39
|
+
{% endfor %}
|
|
40
|
+
</select>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="divide-y divide-[var(--color-gray-100)]">
|
|
46
|
+
{% if entries %}
|
|
47
|
+
{% for entry in entries %}
|
|
48
|
+
<div class="px-6 py-4 hover:bg-[var(--color-gray-50)] transition-colors">
|
|
49
|
+
<div class="flex items-start gap-4">
|
|
50
|
+
<div class="shrink-0 mt-1">
|
|
51
|
+
{% if entry.action == 'CREATE' %}
|
|
52
|
+
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">Created</span>
|
|
53
|
+
{% elif entry.action == 'UPDATE' %}
|
|
54
|
+
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">Updated</span>
|
|
55
|
+
{% elif entry.action == 'DELETE' %}
|
|
56
|
+
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">Deleted</span>
|
|
57
|
+
{% endif %}
|
|
58
|
+
</div>
|
|
59
|
+
<div class="flex-1 min-w-0">
|
|
60
|
+
<div class="text-sm text-[var(--color-gray-900)]">
|
|
61
|
+
<span class="font-medium">{{ entry.user_email | default("System") }}</span>
|
|
62
|
+
{{ entry.action | lower }}d
|
|
63
|
+
<a href="{{ admin_path | default('/admin') }}/{{ entry.table_name }}/{{ entry.object_id }}"
|
|
64
|
+
class="font-medium text-[var(--color-primary-600)] hover:underline">
|
|
65
|
+
{{ entry.model_name }} #{{ entry.object_id }}
|
|
66
|
+
</a>
|
|
67
|
+
{% if entry.object_repr %}
|
|
68
|
+
<span class="text-[var(--color-gray-500)]">({{ entry.object_repr }})</span>
|
|
69
|
+
{% endif %}
|
|
70
|
+
</div>
|
|
71
|
+
<div class="text-xs text-[var(--color-gray-500)] mt-1">
|
|
72
|
+
{{ entry.timestamp }}
|
|
73
|
+
{% if entry.ip_address %}
|
|
74
|
+
<span class="ml-2">IP: {{ entry.ip_address }}</span>
|
|
75
|
+
{% endif %}
|
|
76
|
+
</div>
|
|
77
|
+
{% if entry.changes %}
|
|
78
|
+
<div class="mt-2 text-xs text-[var(--color-gray-500)]">
|
|
79
|
+
Changed: {{ entry.changes.keys() | list | join(', ') }}
|
|
80
|
+
</div>
|
|
81
|
+
{% endif %}
|
|
82
|
+
</div>
|
|
83
|
+
<a href="{{ admin_path | default('/admin') }}/audit-log/{{ entry.id }}"
|
|
84
|
+
class="shrink-0 text-sm text-[var(--color-primary-600)] hover:underline">
|
|
85
|
+
View
|
|
86
|
+
</a>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
{% endfor %}
|
|
90
|
+
{% else %}
|
|
91
|
+
<div class="px-6 py-12 text-center">
|
|
92
|
+
{{ icon("clock", class="mx-auto text-[var(--color-gray-400)]", size="48px") }}
|
|
93
|
+
<p class="mt-4 text-sm text-[var(--color-gray-500)]">No audit log entries found</p>
|
|
94
|
+
</div>
|
|
95
|
+
{% endif %}
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
{% from "partials/pagination.html" import render_pagination %}
|
|
99
|
+
{{ render_pagination(page, total_pages, total_items, per_page) }}
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
{% endblock %}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
{# pages/dashboard.html — premium dashboard #}
|
|
2
|
+
{% extends "base.html" %}
|
|
3
|
+
|
|
4
|
+
{% block title %}{{ title | default("Admin") }} — Dashboard{% endblock %}
|
|
5
|
+
|
|
6
|
+
{% block content %}
|
|
7
|
+
{% set _colors = {
|
|
8
|
+
'Categories': 'teal',
|
|
9
|
+
'Products': 'purple',
|
|
10
|
+
'Users': 'blue',
|
|
11
|
+
'Orders': 'orange',
|
|
12
|
+
'Order Items': 'green',
|
|
13
|
+
'Order Itemss': 'green',
|
|
14
|
+
'Admin Field Permissions': 'yellow',
|
|
15
|
+
'Admin Field Permissionss': 'yellow',
|
|
16
|
+
'Admin Permissions': 'red',
|
|
17
|
+
'Admin Permissionss': 'red',
|
|
18
|
+
'Admin Users': 'indigo',
|
|
19
|
+
'Admin Userss': 'indigo',
|
|
20
|
+
'Admin Audit Logs': 'cyan',
|
|
21
|
+
'Admin Roles': 'pink',
|
|
22
|
+
'Admin Roless': 'pink'
|
|
23
|
+
} %}
|
|
24
|
+
|
|
25
|
+
<div class="dashboard-wrapper" style="display: flex; flex-direction: column; gap: 28px;">
|
|
26
|
+
{# ─── Page Header ─── #}
|
|
27
|
+
<div class="page-header">
|
|
28
|
+
<div class="page-header-left">
|
|
29
|
+
<h1 class="page-header-title">Dashboard</h1>
|
|
30
|
+
<p class="page-header-subtitle" style="color: var(--text-tertiary); font-size: var(--text-sm);">
|
|
31
|
+
Welcome back, Admin! Here's what's happening with your store today.
|
|
32
|
+
</p>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="page-header-right">
|
|
35
|
+
<span class="badge badge-info" style="font-size: var(--text-2xs);">
|
|
36
|
+
<span style="width: 6px; height: 6px; background: var(--info-500); border-radius: 50%; display: inline-block; margin-right: 6px; animation: pulse 2s infinite;"></span>
|
|
37
|
+
System Online
|
|
38
|
+
</span>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
{# ─── Stat Cards ─── #}
|
|
43
|
+
<div class="stat-cards" style="display: grid; grid-template-columns: repeat(5, 1fr); gap: 16px;">
|
|
44
|
+
{% for card in stat_cards %}
|
|
45
|
+
{% set _color = _colors[card.title] | default('teal') %}
|
|
46
|
+
<a href="{{ card.url }}" class="dashboard-stat-card">
|
|
47
|
+
<div class="stat-header">
|
|
48
|
+
<div class="stat-icon stat-icon--{{ _color }}">
|
|
49
|
+
{{ icon(card.icon, size="20px") }}
|
|
50
|
+
</div>
|
|
51
|
+
<span class="stat-label">{{ card.title }}</span>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="stat-count">{{ card.count }}</div>
|
|
54
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
55
|
+
<span class="stat-subtitle">Total {{ card.title | lower }}</span>
|
|
56
|
+
{% if card.has_trend %}
|
|
57
|
+
<span class="stat-trend {{ card.trend_direction }}">
|
|
58
|
+
{% if card.trend_direction == 'up' %}{{ icon('arrow-up', size="10px") }}{% else %}{{ icon('arrow-down', size="10px") }}{% endif %}
|
|
59
|
+
{{ card.trend_pct }}%
|
|
60
|
+
</span>
|
|
61
|
+
<span class="stat-trend-period">vs last 30 days</span>
|
|
62
|
+
{% endif %}
|
|
63
|
+
</div>
|
|
64
|
+
{# Wave decoration #}
|
|
65
|
+
<svg class="stat-wave" viewBox="0 0 80 30" fill="none">
|
|
66
|
+
<path d="M0 20 Q20 5 40 15 T80 10 V30 H0Z" fill="currentColor" opacity="0.3"/>
|
|
67
|
+
<path d="M0 25 Q25 12 50 20 T80 15 V30 H0Z" fill="currentColor" opacity="0.5"/>
|
|
68
|
+
</svg>
|
|
69
|
+
</a>
|
|
70
|
+
{% endfor %}
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
{# ─── Bottom Grid: Activity + Quick Actions ─── #}
|
|
74
|
+
<div style="display: grid; grid-template-columns: 1fr 340px; gap: 24px; align-items: start;">
|
|
75
|
+
{# Recent Activity #}
|
|
76
|
+
<div style="background: var(--surface-raised); border: 1px solid var(--surface-border); border-radius: var(--radius-lg); overflow: hidden;">
|
|
77
|
+
<div style="padding: 20px 24px; border-bottom: 1px solid var(--surface-border); display: flex; align-items: center; justify-content: space-between;">
|
|
78
|
+
<div style="display: flex; align-items: center; gap: 10px;">
|
|
79
|
+
<div style="width: 36px; height: 36px; border-radius: var(--radius-md); background: var(--primary-50); display: flex; align-items: center; justify-content: center;">
|
|
80
|
+
{{ icon('clock', size="18px") }}
|
|
81
|
+
</div>
|
|
82
|
+
<div>
|
|
83
|
+
<h2 style="font-family: var(--font-display); font-size: var(--text-md); font-weight: 700; color: var(--text-primary);">Recent Activity</h2>
|
|
84
|
+
<p style="font-size: var(--text-xs); color: var(--text-tertiary);">Latest system activities will appear here</p>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
{% if recent_audit %}
|
|
88
|
+
<a href="{{ admin_path | default('/admin') }}/audit-log/" style="display: inline-flex; align-items: center; gap: 4px; font-size: var(--text-sm); font-weight: var(--font-medium); color: var(--text-primary); text-decoration: none; padding: 6px 12px; border-radius: var(--radius-md); border: 1px solid var(--surface-border); transition: all 0.15s;"
|
|
89
|
+
onmouseenter="this.style.borderColor='var(--primary-500)'" onmouseleave="this.style.borderColor='var(--surface-border)'">
|
|
90
|
+
View all
|
|
91
|
+
{{ icon('chevron-right', size="14px") }}
|
|
92
|
+
</a>
|
|
93
|
+
{% endif %}
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
{% if recent_audit %}
|
|
97
|
+
<div>
|
|
98
|
+
{% for entry in recent_audit %}
|
|
99
|
+
<div style="padding: 16px 24px; display: flex; align-items: flex-start; gap: 14px; border-bottom: 1px solid var(--surface-border); transition: background 0.15s;"
|
|
100
|
+
onmouseenter="this.style.background='var(--primary-50)'" onmouseleave="this.style.background=''">
|
|
101
|
+
<div style="flex-shrink: 0; margin-top: 2px;">
|
|
102
|
+
{% if entry.action == 'CREATE' %}
|
|
103
|
+
<div style="width: 32px; height: 32px; border-radius: var(--radius-full); background: rgba(34, 197, 94, 0.1); display: flex; align-items: center; justify-content: center;">
|
|
104
|
+
{{ icon('plus', size="16px") }}
|
|
105
|
+
</div>
|
|
106
|
+
{% elif entry.action == 'UPDATE' %}
|
|
107
|
+
<div style="width: 32px; height: 32px; border-radius: var(--radius-full); background: rgba(245, 158, 11, 0.1); display: flex; align-items: center; justify-content: center;">
|
|
108
|
+
{{ icon('pencil', size="16px") }}
|
|
109
|
+
</div>
|
|
110
|
+
{% elif entry.action == 'DELETE' %}
|
|
111
|
+
<div style="width: 32px; height: 32px; border-radius: var(--radius-full); background: rgba(239, 68, 68, 0.1); display: flex; align-items: center; justify-content: center;">
|
|
112
|
+
{{ icon('trash', size="16px") }}
|
|
113
|
+
</div>
|
|
114
|
+
{% endif %}
|
|
115
|
+
</div>
|
|
116
|
+
<div style="flex: 1; min-width: 0;">
|
|
117
|
+
<div style="font-size: var(--text-sm); color: var(--text-primary); line-height: 1.5;">
|
|
118
|
+
<span style="font-weight: var(--font-semibold);">{{ entry.user_email | default("System") }}</span>
|
|
119
|
+
<span style="color: var(--text-tertiary);"> {{ entry.action | lower }}d </span>
|
|
120
|
+
<span style="font-weight: var(--font-semibold);">{{ entry.model_name }}</span>
|
|
121
|
+
<span style="font-family: var(--font-mono); color: var(--text-tertiary); font-size: var(--text-xs);"> #{{ entry.object_id }}</span>
|
|
122
|
+
</div>
|
|
123
|
+
<div style="font-size: var(--text-xs); color: var(--text-tertiary); margin-top: 4px; font-family: var(--font-mono);">{{ entry.timestamp }}</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
{% endfor %}
|
|
127
|
+
</div>
|
|
128
|
+
{% else %}
|
|
129
|
+
<div style="padding: 48px 24px; text-align: center;">
|
|
130
|
+
<div style="width: 64px; height: 64px; border-radius: var(--radius-full); background: var(--surface-overlay); display: flex; align-items: center; justify-content: center; margin: 0 auto 16px;">
|
|
131
|
+
{{ icon('clock', size="32px") }}
|
|
132
|
+
</div>
|
|
133
|
+
<p style="font-size: var(--text-sm); color: var(--text-primary); font-weight: var(--font-semibold);">No recent Activity</p>
|
|
134
|
+
<p style="font-size: var(--text-xs); color: var(--text-tertiary); margin-top: 4px;">System activities will appear here</p>
|
|
135
|
+
</div>
|
|
136
|
+
{% endif %}
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
{# Quick Actions #}
|
|
140
|
+
<div style="background: var(--surface-raised); border: 1px solid var(--surface-border); border-radius: var(--radius-lg); overflow: hidden;">
|
|
141
|
+
<div style="padding: 20px 24px; border-bottom: 1px solid var(--surface-border); display: flex; align-items: center; gap: 10px;">
|
|
142
|
+
<div style="width: 36px; height: 36px; border-radius: var(--radius-md); background: rgba(234, 179, 8, 0.12); display: flex; align-items: center; justify-content: center;">
|
|
143
|
+
{{ icon('bolt', size="18px") }}
|
|
144
|
+
</div>
|
|
145
|
+
<div>
|
|
146
|
+
<h2 style="font-family: var(--font-display); font-size: var(--text-md); font-weight: 700; color: var(--text-primary);">Quick Actions</h2>
|
|
147
|
+
<p style="font-size: var(--text-xs); color: var(--text-tertiary);">Common actions at your fingertips</p>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
<div class="quick-actions-grid">
|
|
151
|
+
{% set _action_colors = {
|
|
152
|
+
'Categories': {'bg': 'rgba(20, 184, 166, 0.12)', 'fg': '#14b8a6'},
|
|
153
|
+
'Products': {'bg': 'rgba(139, 92, 246, 0.12)', 'fg': '#8b5cf6'},
|
|
154
|
+
'Users': {'bg': 'rgba(59, 130, 246, 0.12)', 'fg': '#3b82f6'},
|
|
155
|
+
'Orders': {'bg': 'rgba(249, 115, 22, 0.12)', 'fg': '#f97316'},
|
|
156
|
+
'Order Items': {'bg': 'rgba(34, 197, 94, 0.12)', 'fg': '#22c55e'},
|
|
157
|
+
'Admin Audit Logs': {'bg': 'rgba(6, 182, 212, 0.12)', 'fg': '#06b6d4'},
|
|
158
|
+
'Admin Roles': {'bg': 'rgba(236, 72, 153, 0.12)', 'fg': '#ec4899'},
|
|
159
|
+
'Admin Users': {'bg': 'rgba(99, 102, 241, 0.12)', 'fg': '#6366f1'},
|
|
160
|
+
'Admin Permissions': {'bg': 'rgba(239, 68, 68, 0.12)', 'fg': '#ef4444'},
|
|
161
|
+
'Admin Field Permissions': {'bg': 'rgba(234, 179, 8, 0.12)', 'fg': '#eab308'}
|
|
162
|
+
} %}
|
|
163
|
+
{% for registered in registered_models %}
|
|
164
|
+
{% set _ac = _action_colors[registered.verbose_name_plural] | default({'bg': 'var(--primary-50)', 'fg': 'var(--primary-600)'}) %}
|
|
165
|
+
<a href="{{ admin_path | default('/admin') }}/{{ registered.table_name }}/" class="quick-action-item">
|
|
166
|
+
<div class="action-icon" style="background: {{ _ac.bg }};">
|
|
167
|
+
{{ icon(registered.admin.icon | default('cube'), size="18px") }}
|
|
168
|
+
</div>
|
|
169
|
+
<div class="action-text">
|
|
170
|
+
<div class="action-title">Add {{ registered.verbose_name }}</div>
|
|
171
|
+
<div class="action-desc">Create a new {{ registered.verbose_name | lower }}</div>
|
|
172
|
+
</div>
|
|
173
|
+
{{ icon('chevron-right', size="18px", class="action-chevron") }}
|
|
174
|
+
</a>
|
|
175
|
+
{% endfor %}
|
|
176
|
+
{# Extra: System Logs link #}
|
|
177
|
+
<a href="{{ admin_path | default('/admin') }}/audit-log/" class="quick-action-item">
|
|
178
|
+
<div class="action-icon" style="background: rgba(6, 182, 212, 0.12);">
|
|
179
|
+
{{ icon('document-text', size="18px") }}
|
|
180
|
+
</div>
|
|
181
|
+
<div class="action-text">
|
|
182
|
+
<div class="action-title">System Logs</div>
|
|
183
|
+
<div class="action-desc">View system audit logs</div>
|
|
184
|
+
</div>
|
|
185
|
+
{{ icon('chevron-right', size="18px", class="action-chevron") }}
|
|
186
|
+
</a>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
{# ─── Charts ─── #}
|
|
192
|
+
{% if overview_data and show_charts %}
|
|
193
|
+
<h2 style="font-family: var(--font-display); font-size: var(--text-md); font-weight: 700; color: var(--text-primary); margin: 0;">Charts</h2>
|
|
194
|
+
<div class="system-overview-card">
|
|
195
|
+
<div class="overview-header">
|
|
196
|
+
<div class="overview-header-icon">
|
|
197
|
+
{{ icon('chart-bar') }}
|
|
198
|
+
</div>
|
|
199
|
+
<span class="overview-title">System Overview</span>
|
|
200
|
+
</div>
|
|
201
|
+
<div class="overview-body">
|
|
202
|
+
<div class="overview-chart">
|
|
203
|
+
<canvas id="overviewDonut"></canvas>
|
|
204
|
+
</div>
|
|
205
|
+
<div class="overview-legend">
|
|
206
|
+
{% for item in overview_data %}
|
|
207
|
+
{% if item.value > 0 %}
|
|
208
|
+
<div class="overview-legend-item">
|
|
209
|
+
<span class="overview-legend-label">
|
|
210
|
+
<span class="overview-legend-dot" style="background: {{ item.color }};"></span>
|
|
211
|
+
{{ item.label }}
|
|
212
|
+
</span>
|
|
213
|
+
<span class="overview-legend-value">{{ item.value }}</span>
|
|
214
|
+
</div>
|
|
215
|
+
{% endif %}
|
|
216
|
+
{% endfor %}
|
|
217
|
+
<div class="overview-total">
|
|
218
|
+
<span>Total</span>
|
|
219
|
+
<span class="overview-legend-value">{{ overview_total }}</span>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
{% endif %}
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
|
|
228
|
+
<script>
|
|
229
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
230
|
+
const canvas = document.getElementById('overviewDonut');
|
|
231
|
+
if (!canvas) return;
|
|
232
|
+
|
|
233
|
+
const data = {{ overview_data | tojson }};
|
|
234
|
+
const labels = data.map(d => d.label);
|
|
235
|
+
const values = data.map(d => d.value);
|
|
236
|
+
const colors = data.map(d => d.color);
|
|
237
|
+
|
|
238
|
+
new Chart(canvas, {
|
|
239
|
+
type: 'doughnut',
|
|
240
|
+
data: {
|
|
241
|
+
labels: labels,
|
|
242
|
+
datasets: [{
|
|
243
|
+
data: values,
|
|
244
|
+
backgroundColor: colors,
|
|
245
|
+
borderWidth: 0,
|
|
246
|
+
hoverOffset: 6
|
|
247
|
+
}]
|
|
248
|
+
},
|
|
249
|
+
options: {
|
|
250
|
+
responsive: true,
|
|
251
|
+
maintainAspectRatio: true,
|
|
252
|
+
cutout: '65%',
|
|
253
|
+
plugins: {
|
|
254
|
+
legend: { display: false },
|
|
255
|
+
tooltip: {
|
|
256
|
+
backgroundColor: 'rgba(15, 23, 42, 0.9)',
|
|
257
|
+
titleColor: '#f1f5f9',
|
|
258
|
+
bodyColor: '#94a3b8',
|
|
259
|
+
borderColor: 'rgba(30, 41, 59, 0.5)',
|
|
260
|
+
borderWidth: 1,
|
|
261
|
+
cornerRadius: 8,
|
|
262
|
+
padding: 10,
|
|
263
|
+
bodyFont: { family: "'DM Sans', sans-serif" }
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
</script>
|
|
270
|
+
|
|
271
|
+
<style>
|
|
272
|
+
@keyframes pulse {
|
|
273
|
+
0%, 100% { opacity: 1; }
|
|
274
|
+
50% { opacity: 0.5; }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
@media (max-width: 1280px) {
|
|
278
|
+
.stat-cards {
|
|
279
|
+
grid-template-columns: repeat(3, 1fr) !important;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
@media (max-width: 1024px) {
|
|
284
|
+
.stat-cards {
|
|
285
|
+
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)) !important;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
@media (max-width: 768px) {
|
|
290
|
+
.stat-cards {
|
|
291
|
+
grid-template-columns: repeat(2, 1fr) !important;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
</style>
|
|
295
|
+
{% endblock %}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
{# pages/detail.html — read-only detail view for a single object #}
|
|
2
|
+
{% extends "base.html" %}
|
|
3
|
+
|
|
4
|
+
{% block title %}{{ registered.verbose_name }} #{{ obj.id }} — {{ title | default("Admin") }}{% endblock %}
|
|
5
|
+
|
|
6
|
+
{% block breadcrumb %}
|
|
7
|
+
{{ super() }}
|
|
8
|
+
<span class="breadcrumb-separator">/</span>
|
|
9
|
+
<a href="{{ admin_path | default('/admin') }}/{{ registered.table_name }}/" class="breadcrumb-segment hover:text-[var(--color-primary-600)] transition-colors">{{ registered.verbose_name_plural }}</a>
|
|
10
|
+
<span class="breadcrumb-separator">/</span>
|
|
11
|
+
<span class="breadcrumb-current">{{ obj }}</span>
|
|
12
|
+
{% endblock %}
|
|
13
|
+
|
|
14
|
+
{% block content %}
|
|
15
|
+
<div class="space-y-6">
|
|
16
|
+
<div class="page-header">
|
|
17
|
+
<div class="page-header-left">
|
|
18
|
+
<a href="{{ admin_path | default('/admin') }}/{{ registered.table_name }}/" class="page-header-back">
|
|
19
|
+
{{ icon("chevron-left", size="16px") }}
|
|
20
|
+
{{ registered.verbose_name_plural }}
|
|
21
|
+
</a>
|
|
22
|
+
<h1 class="page-header-title">View {{ registered.verbose_name }}</h1>
|
|
23
|
+
<p class="page-header-subtitle">
|
|
24
|
+
<span class="mono">#{{ obj.id }}</span>
|
|
25
|
+
{% if obj.__str__() %} — {{ obj.__str__() }}{% endif %}
|
|
26
|
+
</p>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="page-header-right">
|
|
29
|
+
{% if permissions.can_edit %}
|
|
30
|
+
<a href="{{ admin_path | default('/admin') }}/{{ registered.table_name }}/{{ obj.id }}" class="btn btn-primary btn-sm">
|
|
31
|
+
{{ icon("pencil", size="16px") }}
|
|
32
|
+
Edit
|
|
33
|
+
</a>
|
|
34
|
+
{% endif %}
|
|
35
|
+
{% if permissions.can_delete %}
|
|
36
|
+
<form method="post" action="{{ admin_path | default('/admin') }}/{{ registered.table_name }}/{{ obj.id }}/delete" style="display: inline;">
|
|
37
|
+
<input type="hidden" name="csrf_token" value="{{ get_csrf_token(request) }}">
|
|
38
|
+
<button type="button" class="btn btn-danger btn-sm"
|
|
39
|
+
onclick="confirmAction('Delete', 'Are you sure you want to delete this {{ registered.verbose_name | lower }}?', () => this.closest('form').submit())">
|
|
40
|
+
Delete
|
|
41
|
+
</button>
|
|
42
|
+
</form>
|
|
43
|
+
{% endif %}
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
{% if fieldsets %}
|
|
48
|
+
{% for fieldset in fieldsets %}
|
|
49
|
+
<div class="bg-white rounded-xl border border-[var(--color-gray-200)]">
|
|
50
|
+
<div class="px-6 py-4 border-b border-[var(--color-gray-200)]">
|
|
51
|
+
<h2 class="text-lg font-semibold text-[var(--color-gray-900)]">{{ fieldset.title or "Details" }}</h2>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="p-6 grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
54
|
+
{% for field in fieldset.fields %}
|
|
55
|
+
<div{% if field.meta.extra.get('full_width', false) %} class="md:col-span-2"{% endif %}>
|
|
56
|
+
<dt class="text-sm font-medium text-[var(--color-gray-500)]">{{ field.meta.label }}</dt>
|
|
57
|
+
<dd class="mt-1 text-sm text-[var(--color-gray-900)]">
|
|
58
|
+
{% set val = field.widget_context.value | default(none) %}
|
|
59
|
+
{% if val is none %}
|
|
60
|
+
<span class="text-[var(--color-gray-400)]">-</span>
|
|
61
|
+
{% elif field.widget_macro == 'toggle' %}
|
|
62
|
+
{% if val %}
|
|
63
|
+
<span class="status-badge status-badge--success">
|
|
64
|
+
{{ icon("check-circle", size="14px") }}
|
|
65
|
+
Active
|
|
66
|
+
</span>
|
|
67
|
+
{% else %}
|
|
68
|
+
<span class="status-badge status-badge--muted">
|
|
69
|
+
{{ icon("x-circle", size="14px") }}
|
|
70
|
+
Inactive
|
|
71
|
+
</span>
|
|
72
|
+
{% endif %}
|
|
73
|
+
{% elif field.widget_macro == 'select' %}
|
|
74
|
+
{% set choices = field.widget_context.choices | default([]) %}
|
|
75
|
+
{% set found = false %}
|
|
76
|
+
{% for choice in choices %}
|
|
77
|
+
{% if choice[0] | string == val | string %}
|
|
78
|
+
{{ choice[1] }}
|
|
79
|
+
{% set found = true %}
|
|
80
|
+
{% endif %}
|
|
81
|
+
{% endfor %}
|
|
82
|
+
{% if not found and val %}{{ val }}{% endif %}
|
|
83
|
+
{% elif field.widget_macro == 'image_upload' and val %}
|
|
84
|
+
<img src="{{ val }}" style="max-width: 200px; max-height: 200px; border-radius: var(--radius-md); border: 1px solid var(--surface-border);">
|
|
85
|
+
{% elif field.widget_macro == 'file_upload' and val %}
|
|
86
|
+
<a href="{{ val }}" target="_blank" style="color: var(--primary-500);">{{ val }}</a>
|
|
87
|
+
{% elif field.widget_macro == 'json_editor' %}
|
|
88
|
+
<pre class="bg-[var(--color-gray-50)] rounded-lg p-4 overflow-auto text-sm text-[var(--color-gray-900)]" style="max-height: 300px;">{{ val }}</pre>
|
|
89
|
+
{% elif field.widget_macro == 'color_picker' %}
|
|
90
|
+
<div class="flex items-center gap-2">
|
|
91
|
+
<div style="width: 24px; height: 24px; border-radius: var(--radius-sm); border: 1px solid var(--surface-border); background: {{ val }};"></div>
|
|
92
|
+
<span>{{ val }}</span>
|
|
93
|
+
</div>
|
|
94
|
+
{% elif field.widget_macro == 'relation_picker' %}
|
|
95
|
+
{{ field.widget_context.label_text | default(val) }}
|
|
96
|
+
{% elif field.widget_macro == 'tag_input' %}
|
|
97
|
+
{% set tags = val | default('[]') %}
|
|
98
|
+
{% if tags is string %}
|
|
99
|
+
{% set tags = tags | tojson %}
|
|
100
|
+
{% endif %}
|
|
101
|
+
<div class="flex flex-wrap gap-1">
|
|
102
|
+
{% for tag in tags %}
|
|
103
|
+
<span class="tag-chip">{{ tag }}</span>
|
|
104
|
+
{% endfor %}
|
|
105
|
+
</div>
|
|
106
|
+
{% else %}
|
|
107
|
+
{{ val }}
|
|
108
|
+
{% endif %}
|
|
109
|
+
</dd>
|
|
110
|
+
</div>
|
|
111
|
+
{% endfor %}
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
{% endfor %}
|
|
115
|
+
{% else %}
|
|
116
|
+
<div class="bg-white rounded-xl border border-[var(--color-gray-200)]">
|
|
117
|
+
<div class="px-6 py-4 border-b border-[var(--color-gray-200)]">
|
|
118
|
+
<h2 class="text-lg font-semibold text-[var(--color-gray-900)]">Details</h2>
|
|
119
|
+
</div>
|
|
120
|
+
<div class="p-6 grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
121
|
+
{% for field in form_fields %}
|
|
122
|
+
<div{% if field.meta.extra.get('full_width', false) %} class="md:col-span-2"{% endif %}>
|
|
123
|
+
<dt class="text-sm font-medium text-[var(--color-gray-500)]">{{ field.meta.label }}</dt>
|
|
124
|
+
<dd class="mt-1 text-sm text-[var(--color-gray-900)]">
|
|
125
|
+
{% set val = field.widget_context.value | default(none) %}
|
|
126
|
+
{% if val is none %}
|
|
127
|
+
<span class="text-[var(--color-gray-400)]">-</span>
|
|
128
|
+
{% elif field.widget_macro == 'toggle' %}
|
|
129
|
+
{% if val %}
|
|
130
|
+
<span class="status-badge status-badge--success">
|
|
131
|
+
{{ icon("check-circle", size="14px") }}
|
|
132
|
+
Active
|
|
133
|
+
</span>
|
|
134
|
+
{% else %}
|
|
135
|
+
<span class="status-badge status-badge--muted">
|
|
136
|
+
{{ icon("x-circle", size="14px") }}
|
|
137
|
+
Inactive
|
|
138
|
+
</span>
|
|
139
|
+
{% endif %}
|
|
140
|
+
{% elif field.widget_macro == 'select' %}
|
|
141
|
+
{% set choices = field.widget_context.choices | default([]) %}
|
|
142
|
+
{% set found = false %}
|
|
143
|
+
{% for choice in choices %}
|
|
144
|
+
{% if choice[0] | string == val | string %}
|
|
145
|
+
{{ choice[1] }}
|
|
146
|
+
{% set found = true %}
|
|
147
|
+
{% endif %}
|
|
148
|
+
{% endfor %}
|
|
149
|
+
{% if not found and val %}{{ val }}{% endif %}
|
|
150
|
+
{% elif field.widget_macro == 'image_upload' and val %}
|
|
151
|
+
<img src="{{ val }}" style="max-width: 200px; max-height: 200px; border-radius: var(--radius-md); border: 1px solid var(--surface-border);">
|
|
152
|
+
{% elif field.widget_macro == 'file_upload' and val %}
|
|
153
|
+
<a href="{{ val }}" target="_blank" style="color: var(--primary-500);">{{ val }}</a>
|
|
154
|
+
{% elif field.widget_macro == 'json_editor' %}
|
|
155
|
+
<pre class="bg-[var(--color-gray-50)] rounded-lg p-4 overflow-auto text-sm text-[var(--color-gray-900)]" style="max-height: 300px;">{{ val }}</pre>
|
|
156
|
+
{% elif field.widget_macro == 'color_picker' %}
|
|
157
|
+
<div class="flex items-center gap-2">
|
|
158
|
+
<div style="width: 24px; height: 24px; border-radius: var(--radius-sm); border: 1px solid var(--surface-border); background: {{ val }};"></div>
|
|
159
|
+
<span>{{ val }}</span>
|
|
160
|
+
</div>
|
|
161
|
+
{% elif field.widget_macro == 'relation_picker' %}
|
|
162
|
+
{{ field.widget_context.label_text | default(val) }}
|
|
163
|
+
{% elif field.widget_macro == 'tag_input' %}
|
|
164
|
+
{% set tags = val | default('[]') %}
|
|
165
|
+
{% if tags is string %}
|
|
166
|
+
{% set tags = tags | tojson %}
|
|
167
|
+
{% endif %}
|
|
168
|
+
<div class="flex flex-wrap gap-1">
|
|
169
|
+
{% for tag in tags %}
|
|
170
|
+
<span class="tag-chip">{{ tag }}</span>
|
|
171
|
+
{% endfor %}
|
|
172
|
+
</div>
|
|
173
|
+
{% else %}
|
|
174
|
+
{{ val }}
|
|
175
|
+
{% endif %}
|
|
176
|
+
</dd>
|
|
177
|
+
</div>
|
|
178
|
+
{% endfor %}
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
{% endif %}
|
|
182
|
+
</div>
|
|
183
|
+
{% endblock %}
|