splent-feature-admin 1.0.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 (21) hide show
  1. splent_feature_admin-1.0.0/MANIFEST.in +8 -0
  2. splent_feature_admin-1.0.0/PKG-INFO +5 -0
  3. splent_feature_admin-1.0.0/pyproject.toml +52 -0
  4. splent_feature_admin-1.0.0/setup.cfg +4 -0
  5. splent_feature_admin-1.0.0/src/splent_feature_admin.egg-info/PKG-INFO +5 -0
  6. splent_feature_admin-1.0.0/src/splent_feature_admin.egg-info/SOURCES.txt +19 -0
  7. splent_feature_admin-1.0.0/src/splent_feature_admin.egg-info/dependency_links.txt +1 -0
  8. splent_feature_admin-1.0.0/src/splent_feature_admin.egg-info/top_level.txt +1 -0
  9. splent_feature_admin-1.0.0/src/splent_io/splent_feature_admin/__init__.py +14 -0
  10. splent_feature_admin-1.0.0/src/splent_io/splent_feature_admin/config.py +10 -0
  11. splent_feature_admin-1.0.0/src/splent_io/splent_feature_admin/hooks.py +17 -0
  12. splent_feature_admin-1.0.0/src/splent_io/splent_feature_admin/migrations/alembic.ini +36 -0
  13. splent_feature_admin-1.0.0/src/splent_io/splent_feature_admin/migrations/env.py +9 -0
  14. splent_feature_admin-1.0.0/src/splent_io/splent_feature_admin/migrations/script.py.mako +24 -0
  15. splent_feature_admin-1.0.0/src/splent_io/splent_feature_admin/routes.py +121 -0
  16. splent_feature_admin-1.0.0/src/splent_io/splent_feature_admin/seeders.py +10 -0
  17. splent_feature_admin-1.0.0/src/splent_io/splent_feature_admin/services.py +119 -0
  18. splent_feature_admin-1.0.0/src/splent_io/splent_feature_admin/templates/admin/dashboard.html +49 -0
  19. splent_feature_admin-1.0.0/src/splent_io/splent_feature_admin/templates/admin/delete.html +50 -0
  20. splent_feature_admin-1.0.0/src/splent_io/splent_feature_admin/templates/admin/form.html +88 -0
  21. splent_feature_admin-1.0.0/src/splent_io/splent_feature_admin/templates/admin/list.html +101 -0
@@ -0,0 +1,8 @@
1
+ # Compiled frontend bundles
2
+ recursive-include src/splent_io/splent_feature_admin/assets/dist *.js *.css *.map
3
+
4
+ # Jinja templates (feature views + hook fragments)
5
+ recursive-include src/splent_io/splent_feature_admin/templates *.html
6
+
7
+ # Alembic migration config and scripts
8
+ recursive-include src/splent_io/splent_feature_admin/migrations *.py *.ini *.mako
@@ -0,0 +1,5 @@
1
+ Metadata-Version: 2.4
2
+ Name: splent_feature_admin
3
+ Version: 1.0.0
4
+ Requires-Python: >=3.13
5
+ Description-Content-Type: text/markdown
@@ -0,0 +1,52 @@
1
+ [build-system]
2
+ requires = ["setuptools>=80.3.1", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "splent_feature_admin"
7
+ version = "1.0.0"
8
+ readme = "README.md"
9
+ requires-python = ">=3.13"
10
+
11
+ dependencies = [
12
+
13
+ ]
14
+
15
+ [tool.setuptools]
16
+ package-dir = { "" = "src" }
17
+ include-package-data = true
18
+
19
+ [tool.setuptools.packages.find]
20
+ where = ["src"]
21
+ exclude = ["*.tests", "*.tests.*"]
22
+
23
+ [tool.splent]
24
+ cli_version = "1.4.5"
25
+
26
+ # ── Feature Contract (auto-generated) ────────────────────────────────────────
27
+ # Do not edit manually — re-run `splent feature:contract --write` to refresh.
28
+ [tool.splent.contract]
29
+ description = "Auto-generated CRUD admin panel for all product models"
30
+
31
+ [tool.splent.contract.provides]
32
+ routes = ["/admin", "/admin/<model_name>", "/admin/<model_name>/<int:id>", "/admin/<model_name>/<int:id>/delete", "/admin/<model_name>/create"]
33
+ blueprints = []
34
+ models = []
35
+ commands = []
36
+ hooks = ["layout.authenticated_sidebar"]
37
+ services = []
38
+ signals = []
39
+ translations = []
40
+ docker = []
41
+
42
+ [tool.splent.contract.requires]
43
+ features = ["auth"]
44
+ env_vars = []
45
+ signals = []
46
+
47
+ [tool.splent.contract.extensible]
48
+ services = []
49
+ templates = ["admin/dashboard.html", "admin/delete.html", "admin/form.html", "admin/list.html"]
50
+ models = []
51
+ hooks = ["layout.authenticated_sidebar"]
52
+ routes = false
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ Metadata-Version: 2.4
2
+ Name: splent_feature_admin
3
+ Version: 1.0.0
4
+ Requires-Python: >=3.13
5
+ Description-Content-Type: text/markdown
@@ -0,0 +1,19 @@
1
+ MANIFEST.in
2
+ pyproject.toml
3
+ src/splent_feature_admin.egg-info/PKG-INFO
4
+ src/splent_feature_admin.egg-info/SOURCES.txt
5
+ src/splent_feature_admin.egg-info/dependency_links.txt
6
+ src/splent_feature_admin.egg-info/top_level.txt
7
+ src/splent_io/splent_feature_admin/__init__.py
8
+ src/splent_io/splent_feature_admin/config.py
9
+ src/splent_io/splent_feature_admin/hooks.py
10
+ src/splent_io/splent_feature_admin/routes.py
11
+ src/splent_io/splent_feature_admin/seeders.py
12
+ src/splent_io/splent_feature_admin/services.py
13
+ src/splent_io/splent_feature_admin/migrations/alembic.ini
14
+ src/splent_io/splent_feature_admin/migrations/env.py
15
+ src/splent_io/splent_feature_admin/migrations/script.py.mako
16
+ src/splent_io/splent_feature_admin/templates/admin/dashboard.html
17
+ src/splent_io/splent_feature_admin/templates/admin/delete.html
18
+ src/splent_io/splent_feature_admin/templates/admin/form.html
19
+ src/splent_io/splent_feature_admin/templates/admin/list.html
@@ -0,0 +1,14 @@
1
+ from splent_framework.blueprints.base_blueprint import create_blueprint
2
+ from splent_framework.services.service_locator import register_service
3
+
4
+ from splent_io.splent_feature_admin.services import AdminService
5
+
6
+ admin_bp = create_blueprint(__name__)
7
+
8
+
9
+ def init_feature(app):
10
+ register_service(app, "AdminService", AdminService)
11
+
12
+
13
+ def inject_context_vars(app):
14
+ return {}
@@ -0,0 +1,10 @@
1
+ """
2
+ admin feature configuration.
3
+
4
+ Injects environment variables into Flask app.config.
5
+ To regenerate from source code: splent feature:inject-config splent_feature_admin
6
+ """
7
+
8
+
9
+ def inject_config(app):
10
+ app.config.update({})
@@ -0,0 +1,17 @@
1
+ from splent_framework.hooks.template_hooks import register_template_hook
2
+ from flask import request, url_for
3
+
4
+
5
+ def admin_sidebar_link():
6
+ active = "active" if request.endpoint and request.endpoint.startswith("admin.") else ""
7
+ return (
8
+ f'<li class="sidebar-item {active}">'
9
+ f'<a class="sidebar-link" href="{url_for("admin.dashboard")}">'
10
+ '<i class="align-middle" data-feather="settings"></i> '
11
+ '<span class="align-middle">Admin</span>'
12
+ "</a>"
13
+ "</li>"
14
+ )
15
+
16
+
17
+ register_template_hook("layout.authenticated_sidebar", admin_sidebar_link)
@@ -0,0 +1,36 @@
1
+ [alembic]
2
+ script_location = migrations
3
+
4
+ [loggers]
5
+ keys = root,sqlalchemy,alembic
6
+
7
+ [handlers]
8
+ keys = console
9
+
10
+ [formatters]
11
+ keys = generic
12
+
13
+ [logger_root]
14
+ level = WARN
15
+ handlers = console
16
+ qualname =
17
+
18
+ [logger_sqlalchemy]
19
+ level = WARN
20
+ handlers =
21
+ qualname = sqlalchemy.engine
22
+
23
+ [logger_alembic]
24
+ level = INFO
25
+ handlers =
26
+ qualname = alembic
27
+
28
+ [handler_console]
29
+ class = StreamHandler
30
+ args = (sys.stderr,)
31
+ level = NOTSET
32
+ formatter = generic
33
+
34
+ [formatter_generic]
35
+ format = %(levelname)-5.5s [%(name)s] %(message)s
36
+ datefmt = %H:%M:%S
@@ -0,0 +1,9 @@
1
+ """Alembic migration environment for splent_feature_admin."""
2
+
3
+ from splent_io.splent_feature_admin import models # noqa
4
+ from splent_framework.migrations.feature_env import run_feature_migrations
5
+
6
+ FEATURE_NAME = "splent_feature_admin"
7
+ FEATURE_TABLES = set()
8
+
9
+ run_feature_migrations(FEATURE_NAME, FEATURE_TABLES)
@@ -0,0 +1,24 @@
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+ ${imports if imports else ""}
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = ${repr(up_revision)}
14
+ down_revision = ${repr(down_revision)}
15
+ branch_labels = ${repr(branch_labels)}
16
+ depends_on = ${repr(depends_on)}
17
+
18
+
19
+ def upgrade():
20
+ ${upgrades if upgrades else "pass"}
21
+
22
+
23
+ def downgrade():
24
+ ${downgrades if downgrades else "pass"}
@@ -0,0 +1,121 @@
1
+ from flask import flash, render_template, redirect, url_for, request
2
+ from flask_login import login_required
3
+
4
+ from splent_io.splent_feature_admin import admin_bp
5
+ from splent_io.splent_feature_admin.services import AdminService
6
+
7
+
8
+ @admin_bp.route("/admin", methods=["GET"])
9
+ @login_required
10
+ def dashboard():
11
+ models = AdminService.get_models()
12
+ model_stats = []
13
+ for name, cls in sorted(models.items()):
14
+ model_stats.append({
15
+ "name": name,
16
+ "count": AdminService.count_records(cls),
17
+ "columns": len(AdminService.get_columns(cls)),
18
+ })
19
+ return render_template("admin/dashboard.html", models=model_stats)
20
+
21
+
22
+ @admin_bp.route("/admin/<model_name>", methods=["GET"])
23
+ @login_required
24
+ def model_list(model_name):
25
+ model = AdminService.get_model(model_name)
26
+ if model is None:
27
+ flash(f"Model '{model_name}' not found.", "danger")
28
+ return redirect(url_for("admin.dashboard"))
29
+
30
+ page = request.args.get("page", 1, type=int)
31
+ pagination = AdminService.get_records(model, page=page)
32
+ columns = AdminService.get_list_columns(model)
33
+
34
+ return render_template(
35
+ "admin/list.html",
36
+ model_name=model_name,
37
+ columns=columns,
38
+ pagination=pagination,
39
+ records=pagination.items,
40
+ )
41
+
42
+
43
+ @admin_bp.route("/admin/<model_name>/create", methods=["GET", "POST"])
44
+ @login_required
45
+ def create(model_name):
46
+ model = AdminService.get_model(model_name)
47
+ if model is None:
48
+ flash(f"Model '{model_name}' not found.", "danger")
49
+ return redirect(url_for("admin.dashboard"))
50
+
51
+ columns = AdminService.get_editable_columns(model)
52
+
53
+ if request.method == "POST":
54
+ AdminService.create_record(model, request.form.to_dict())
55
+ flash(f"{model_name} created successfully.", "success")
56
+ return redirect(url_for("admin.model_list", model_name=model_name))
57
+
58
+ return render_template(
59
+ "admin/form.html",
60
+ model_name=model_name,
61
+ columns=columns,
62
+ record=None,
63
+ action="Create",
64
+ )
65
+
66
+
67
+ @admin_bp.route("/admin/<model_name>/<int:id>", methods=["GET", "POST"])
68
+ @login_required
69
+ def edit(model_name, id):
70
+ model = AdminService.get_model(model_name)
71
+ if model is None:
72
+ flash(f"Model '{model_name}' not found.", "danger")
73
+ return redirect(url_for("admin.dashboard"))
74
+
75
+ record = AdminService.get_record(model, id)
76
+ if record is None:
77
+ flash(f"{model_name} with id {id} not found.", "danger")
78
+ return redirect(url_for("admin.model_list", model_name=model_name))
79
+
80
+ columns = AdminService.get_editable_columns(model)
81
+
82
+ if request.method == "POST":
83
+ AdminService.update_record(record, request.form.to_dict(), model)
84
+ flash(f"{model_name} updated successfully.", "success")
85
+ return redirect(url_for("admin.model_list", model_name=model_name))
86
+
87
+ return render_template(
88
+ "admin/form.html",
89
+ model_name=model_name,
90
+ columns=columns,
91
+ record=record,
92
+ action="Edit",
93
+ )
94
+
95
+
96
+ @admin_bp.route("/admin/<model_name>/<int:id>/delete", methods=["GET", "POST"])
97
+ @login_required
98
+ def delete(model_name, id):
99
+ model = AdminService.get_model(model_name)
100
+ if model is None:
101
+ flash(f"Model '{model_name}' not found.", "danger")
102
+ return redirect(url_for("admin.dashboard"))
103
+
104
+ record = AdminService.get_record(model, id)
105
+ if record is None:
106
+ flash(f"{model_name} with id {id} not found.", "danger")
107
+ return redirect(url_for("admin.model_list", model_name=model_name))
108
+
109
+ if request.method == "POST":
110
+ AdminService.delete_record(record)
111
+ flash(f"{model_name} deleted successfully.", "success")
112
+ return redirect(url_for("admin.model_list", model_name=model_name))
113
+
114
+ columns = AdminService.get_columns(model)
115
+
116
+ return render_template(
117
+ "admin/delete.html",
118
+ model_name=model_name,
119
+ columns=columns,
120
+ record=record,
121
+ )
@@ -0,0 +1,10 @@
1
+ from splent_io.splent_feature_auth.models import User
2
+ from splent_framework.seeders.BaseSeeder import BaseSeeder
3
+
4
+
5
+ class AdminSeeder(BaseSeeder):
6
+ priority = 0 # Run before other seeders
7
+
8
+ def run(self):
9
+ admin = User(email="admin@admin.com", password="admin", active=True)
10
+ self.seed([admin])
@@ -0,0 +1,119 @@
1
+ from sqlalchemy import inspect as sa_inspect
2
+
3
+ from splent_framework.db import db
4
+
5
+
6
+ # Column types to skip in list views (too verbose for table display)
7
+ _SKIP_TYPES_IN_LIST = {"TEXT", "BLOB", "JSON", "LONGTEXT", "MEDIUMTEXT"}
8
+
9
+ # Maximum columns shown in the list view
10
+ _MAX_LIST_COLUMNS = 7
11
+
12
+
13
+ class AdminService:
14
+ """Service for auto-generated CRUD operations on all registered models."""
15
+
16
+ # ── Model discovery ──────────────────────────────────────────────────
17
+
18
+ @staticmethod
19
+ def get_models() -> dict[str, type]:
20
+ """Return all registered SQLAlchemy model classes keyed by name."""
21
+ return {cls.__name__: cls for cls in db.Model.__subclasses__()}
22
+
23
+ @staticmethod
24
+ def get_model(name: str) -> type | None:
25
+ """Return a single model class by name, or None."""
26
+ models = AdminService.get_models()
27
+ return models.get(name)
28
+
29
+ # ── Column introspection ─────────────────────────────────────────────
30
+
31
+ @staticmethod
32
+ def get_columns(model: type) -> list[dict]:
33
+ """Return column metadata for a model via SQLAlchemy inspect."""
34
+ mapper = sa_inspect(model)
35
+ cols = []
36
+ for col in mapper.columns:
37
+ fk = None
38
+ if col.foreign_keys:
39
+ fk = str(list(col.foreign_keys)[0].target_fullname)
40
+ cols.append({
41
+ "name": col.name,
42
+ "type": str(col.type),
43
+ "primary_key": col.primary_key,
44
+ "nullable": col.nullable,
45
+ "foreign_key": fk,
46
+ })
47
+ return cols
48
+
49
+ @staticmethod
50
+ def get_list_columns(model: type) -> list[dict]:
51
+ """Return columns suitable for the list view (filtered and capped)."""
52
+ cols = AdminService.get_columns(model)
53
+ filtered = [
54
+ c for c in cols
55
+ if c["type"].upper().split("(")[0] not in _SKIP_TYPES_IN_LIST
56
+ ]
57
+ return filtered[:_MAX_LIST_COLUMNS]
58
+
59
+ @staticmethod
60
+ def get_editable_columns(model: type) -> list[dict]:
61
+ """Return columns that can be edited (excludes primary keys)."""
62
+ return [c for c in AdminService.get_columns(model) if not c["primary_key"]]
63
+
64
+ # ── CRUD operations ──────────────────────────────────────────────────
65
+
66
+ @staticmethod
67
+ def get_record(model: type, record_id: int):
68
+ """Fetch a single record by primary key."""
69
+ return db.session.get(model, record_id)
70
+
71
+ @staticmethod
72
+ def get_records(model: type, page: int = 1, per_page: int = 20):
73
+ """Return a paginated query for the given model."""
74
+ return model.query.paginate(page=page, per_page=per_page, error_out=False)
75
+
76
+ @staticmethod
77
+ def count_records(model: type) -> int:
78
+ """Return the total number of records for a model."""
79
+ return model.query.count()
80
+
81
+ @staticmethod
82
+ def create_record(model: type, form_data: dict):
83
+ """Create a new record from form data."""
84
+ editable = {c["name"] for c in AdminService.get_editable_columns(model)}
85
+ nullable = {c["name"] for c in AdminService.get_columns(model) if c["nullable"]}
86
+
87
+ data = {}
88
+ for key, value in form_data.items():
89
+ if key in editable:
90
+ if value == "" and key in nullable:
91
+ data[key] = None
92
+ else:
93
+ data[key] = value
94
+
95
+ record = model(**data)
96
+ db.session.add(record)
97
+ db.session.commit()
98
+ return record
99
+
100
+ @staticmethod
101
+ def update_record(record, form_data: dict, model: type):
102
+ """Update an existing record from form data."""
103
+ editable = {c["name"] for c in AdminService.get_editable_columns(model)}
104
+ nullable = {c["name"] for c in AdminService.get_columns(model) if c["nullable"]}
105
+
106
+ for key, value in form_data.items():
107
+ if key in editable:
108
+ if value == "" and key in nullable:
109
+ setattr(record, key, None)
110
+ else:
111
+ setattr(record, key, value)
112
+
113
+ db.session.commit()
114
+
115
+ @staticmethod
116
+ def delete_record(record):
117
+ """Delete a record from the database."""
118
+ db.session.delete(record)
119
+ db.session.commit()
@@ -0,0 +1,49 @@
1
+ {% extends "base_template.html" %}
2
+
3
+ {% block title %}Admin Dashboard{% endblock %}
4
+
5
+ {% block content %}
6
+
7
+ <div class="d-flex justify-content-between align-items-center mb-4">
8
+ <h1 class="h2 mb-0"><b>Admin Dashboard</b></h1>
9
+ <span class="badge bg-secondary fs-6">{{ models|length }} model{{ 's' if models|length != 1 }}</span>
10
+ </div>
11
+
12
+ {% with messages = get_flashed_messages(with_categories=true) %}
13
+ {% if messages %}
14
+ {% for category, message in messages %}
15
+ <div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
16
+ {{ message }}
17
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
18
+ </div>
19
+ {% endfor %}
20
+ {% endif %}
21
+ {% endwith %}
22
+
23
+ {% if models %}
24
+ <div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
25
+ {% for model in models %}
26
+ <div class="col">
27
+ <div class="card h-100 shadow-sm">
28
+ <div class="card-body d-flex flex-column">
29
+ <h5 class="card-title mb-1">{{ model.name }}</h5>
30
+ <p class="text-muted small mb-3">{{ model.columns }} column{{ 's' if model.columns != 1 }}</p>
31
+ <div class="mt-auto d-flex justify-content-between align-items-center">
32
+ <span class="badge bg-primary rounded-pill fs-6">{{ model.count }}</span>
33
+ <div>
34
+ <a href="{{ url_for('admin.model_list', model_name=model.name) }}" class="btn btn-sm btn-outline-primary">Browse</a>
35
+ <a href="{{ url_for('admin.create', model_name=model.name) }}" class="btn btn-sm btn-outline-success">+ New</a>
36
+ </div>
37
+ </div>
38
+ </div>
39
+ </div>
40
+ </div>
41
+ {% endfor %}
42
+ </div>
43
+ {% else %}
44
+ <div class="alert alert-info">
45
+ No models discovered. Make sure your features are loaded and have SQLAlchemy models defined.
46
+ </div>
47
+ {% endif %}
48
+
49
+ {% endblock %}
@@ -0,0 +1,50 @@
1
+ {% extends "base_template.html" %}
2
+
3
+ {% block title %}Delete {{ model_name }} - Admin{% endblock %}
4
+
5
+ {% block breadcrumb %}
6
+ <nav aria-label="breadcrumb">
7
+ <ol class="breadcrumb">
8
+ <li class="breadcrumb-item"><a href="{{ url_for('admin.dashboard') }}">Admin</a></li>
9
+ <li class="breadcrumb-item"><a href="{{ url_for('admin.model_list', model_name=model_name) }}">{{ model_name }}</a></li>
10
+ <li class="breadcrumb-item active">Delete</li>
11
+ </ol>
12
+ </nav>
13
+ {% endblock %}
14
+
15
+ {% block content %}
16
+
17
+ <h1 class="h2 mb-4"><b>Delete {{ model_name }}</b></h1>
18
+
19
+ <div class="card border-danger shadow-sm">
20
+ <div class="card-body">
21
+ <p class="card-text mb-3">
22
+ You are about to permanently delete this <strong>{{ model_name }}</strong> record. This action cannot be undone.
23
+ </p>
24
+
25
+ <table class="table table-sm mb-4">
26
+ <tbody>
27
+ {% for col in columns %}
28
+ <tr>
29
+ <th class="text-muted" style="width: 200px;">{{ col.name }}</th>
30
+ <td>
31
+ {% set val = record.__dict__.get(col.name, '') %}
32
+ {% if val is none %}
33
+ <span class="text-muted fst-italic">null</span>
34
+ {% else %}
35
+ {{ val }}
36
+ {% endif %}
37
+ </td>
38
+ </tr>
39
+ {% endfor %}
40
+ </tbody>
41
+ </table>
42
+
43
+ <form method="POST" class="d-inline">
44
+ <button type="submit" class="btn btn-danger">Confirm Delete</button>
45
+ <a href="{{ url_for('admin.model_list', model_name=model_name) }}" class="btn btn-outline-secondary ms-2">Cancel</a>
46
+ </form>
47
+ </div>
48
+ </div>
49
+
50
+ {% endblock %}
@@ -0,0 +1,88 @@
1
+ {% extends "base_template.html" %}
2
+
3
+ {% block title %}{{ action }} {{ model_name }} - Admin{% endblock %}
4
+
5
+ {% block breadcrumb %}
6
+ <nav aria-label="breadcrumb">
7
+ <ol class="breadcrumb">
8
+ <li class="breadcrumb-item"><a href="{{ url_for('admin.dashboard') }}">Admin</a></li>
9
+ <li class="breadcrumb-item"><a href="{{ url_for('admin.model_list', model_name=model_name) }}">{{ model_name }}</a></li>
10
+ <li class="breadcrumb-item active">{{ action }}</li>
11
+ </ol>
12
+ </nav>
13
+ {% endblock %}
14
+
15
+ {% block content %}
16
+
17
+ <h1 class="h2 mb-4"><b>{{ action }} {{ model_name }}</b></h1>
18
+
19
+ {% with messages = get_flashed_messages(with_categories=true) %}
20
+ {% if messages %}
21
+ {% for category, message in messages %}
22
+ <div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
23
+ {{ message }}
24
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
25
+ </div>
26
+ {% endfor %}
27
+ {% endif %}
28
+ {% endwith %}
29
+
30
+ <form method="POST">
31
+ <div class="row">
32
+ {% for col in columns %}
33
+ <div class="col-md-6 mb-3">
34
+ <label for="{{ col.name }}" class="form-label fw-semibold">
35
+ {{ col.name }}
36
+ <span class="text-muted fw-normal small">({{ col.type }})</span>
37
+ {% if col.foreign_key %}
38
+ <span class="badge bg-info text-dark">FK: {{ col.foreign_key }}</span>
39
+ {% endif %}
40
+ {% if not col.nullable %}
41
+ <span class="text-danger">*</span>
42
+ {% endif %}
43
+ </label>
44
+
45
+ {% set col_type = col.type.upper().split('(')[0] %}
46
+ {% if col_type in ('TEXT', 'LONGTEXT', 'MEDIUMTEXT') %}
47
+ <textarea
48
+ id="{{ col.name }}"
49
+ name="{{ col.name }}"
50
+ class="form-control"
51
+ rows="4"
52
+ {{ 'required' if not col.nullable }}
53
+ >{{ record[col.name] if record and record.__dict__.get(col.name) is not none else '' }}</textarea>
54
+ {% elif col_type == 'BOOLEAN' or col_type == 'TINYINT' %}
55
+ <div class="form-check form-switch mt-2">
56
+ <input
57
+ type="checkbox"
58
+ id="{{ col.name }}"
59
+ name="{{ col.name }}"
60
+ value="1"
61
+ class="form-check-input"
62
+ {{ 'checked' if record and record.__dict__.get(col.name) }}
63
+ >
64
+ </div>
65
+ {% else %}
66
+ <input
67
+ type="{{ 'number' if col_type in ('INTEGER', 'INT', 'BIGINT', 'SMALLINT', 'FLOAT', 'DECIMAL', 'NUMERIC', 'DOUBLE') else 'text' }}"
68
+ id="{{ col.name }}"
69
+ name="{{ col.name }}"
70
+ class="form-control"
71
+ value="{{ record.__dict__.get(col.name, '') if record and record.__dict__.get(col.name) is not none else '' }}"
72
+ {{ 'required' if not col.nullable }}
73
+ {{ 'step=any' if col_type in ('FLOAT', 'DECIMAL', 'NUMERIC', 'DOUBLE') }}
74
+ >
75
+ {% endif %}
76
+ </div>
77
+ {% endfor %}
78
+ </div>
79
+
80
+ <div class="mt-3">
81
+ <button type="submit" class="btn btn-primary">
82
+ {{ 'Save changes' if action == 'Edit' else 'Create' }}
83
+ </button>
84
+ <a href="{{ url_for('admin.model_list', model_name=model_name) }}" class="btn btn-outline-secondary ms-2">Cancel</a>
85
+ </div>
86
+ </form>
87
+
88
+ {% endblock %}
@@ -0,0 +1,101 @@
1
+ {% extends "base_template.html" %}
2
+
3
+ {% block title %}{{ model_name }} - Admin{% endblock %}
4
+
5
+ {% block breadcrumb %}
6
+ <nav aria-label="breadcrumb">
7
+ <ol class="breadcrumb">
8
+ <li class="breadcrumb-item"><a href="{{ url_for('admin.dashboard') }}">Admin</a></li>
9
+ <li class="breadcrumb-item active">{{ model_name }}</li>
10
+ </ol>
11
+ </nav>
12
+ {% endblock %}
13
+
14
+ {% block content %}
15
+
16
+ <div class="d-flex justify-content-between align-items-center mb-4">
17
+ <h1 class="h2 mb-0"><b>{{ model_name }}</b></h1>
18
+ <a href="{{ url_for('admin.create', model_name=model_name) }}" class="btn btn-success">+ New {{ model_name }}</a>
19
+ </div>
20
+
21
+ {% with messages = get_flashed_messages(with_categories=true) %}
22
+ {% if messages %}
23
+ {% for category, message in messages %}
24
+ <div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
25
+ {{ message }}
26
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
27
+ </div>
28
+ {% endfor %}
29
+ {% endif %}
30
+ {% endwith %}
31
+
32
+ {% if records %}
33
+ <div class="table-responsive">
34
+ <table class="table table-striped table-hover align-middle">
35
+ <thead class="table-dark">
36
+ <tr>
37
+ {% for col in columns %}
38
+ <th>
39
+ {{ col.name }}
40
+ {% if col.primary_key %}<span class="badge bg-warning text-dark">PK</span>{% endif %}
41
+ {% if col.foreign_key %}<span class="badge bg-info text-dark">FK</span>{% endif %}
42
+ </th>
43
+ {% endfor %}
44
+ <th class="text-end">Actions</th>
45
+ </tr>
46
+ </thead>
47
+ <tbody>
48
+ {% for record in records %}
49
+ <tr>
50
+ {% for col in columns %}
51
+ <td>
52
+ {% set val = record.__dict__.get(col.name, '') %}
53
+ {% if val is none %}
54
+ <span class="text-muted fst-italic">null</span>
55
+ {% elif val|string|length > 80 %}
56
+ {{ val|string|truncate(80) }}
57
+ {% else %}
58
+ {{ val }}
59
+ {% endif %}
60
+ </td>
61
+ {% endfor %}
62
+ <td class="text-end text-nowrap">
63
+ <a href="{{ url_for('admin.edit', model_name=model_name, id=record.id) }}" class="btn btn-sm btn-outline-primary">Edit</a>
64
+ <a href="{{ url_for('admin.delete', model_name=model_name, id=record.id) }}" class="btn btn-sm btn-outline-danger">Delete</a>
65
+ </td>
66
+ </tr>
67
+ {% endfor %}
68
+ </tbody>
69
+ </table>
70
+ </div>
71
+
72
+ {% if pagination.pages > 1 %}
73
+ <nav aria-label="Pagination">
74
+ <ul class="pagination justify-content-center">
75
+ <li class="page-item {{ 'disabled' if not pagination.has_prev }}">
76
+ <a class="page-link" href="{{ url_for('admin.model_list', model_name=model_name, page=pagination.prev_num) }}">Previous</a>
77
+ </li>
78
+ {% for p in pagination.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
79
+ {% if p %}
80
+ <li class="page-item {{ 'active' if p == pagination.page }}">
81
+ <a class="page-link" href="{{ url_for('admin.model_list', model_name=model_name, page=p) }}">{{ p }}</a>
82
+ </li>
83
+ {% else %}
84
+ <li class="page-item disabled"><span class="page-link">...</span></li>
85
+ {% endif %}
86
+ {% endfor %}
87
+ <li class="page-item {{ 'disabled' if not pagination.has_next }}">
88
+ <a class="page-link" href="{{ url_for('admin.model_list', model_name=model_name, page=pagination.next_num) }}">Next</a>
89
+ </li>
90
+ </ul>
91
+ </nav>
92
+ {% endif %}
93
+
94
+ {% else %}
95
+ <div class="alert alert-info">
96
+ No records found for <strong>{{ model_name }}</strong>.
97
+ <a href="{{ url_for('admin.create', model_name=model_name) }}">Create the first one.</a>
98
+ </div>
99
+ {% endif %}
100
+
101
+ {% endblock %}