Still making little progress fixing my stray engine problem.
This commit is contained in:
parent
c6165af40e
commit
085905557d
6 changed files with 171 additions and 53 deletions
|
|
@ -132,8 +132,19 @@ class CRUDService(Generic[T]):
|
||||||
self._session_factory = session_factory
|
self._session_factory = session_factory
|
||||||
self.polymorphic = polymorphic
|
self.polymorphic = polymorphic
|
||||||
self.supports_soft_delete = hasattr(model, 'is_deleted')
|
self.supports_soft_delete = hasattr(model, 'is_deleted')
|
||||||
# Cache backend info once. If not provided, derive from session bind.
|
|
||||||
bind = session_factory().get_bind()
|
# Derive engine WITHOUT leaking a session/connection
|
||||||
|
bind = getattr(session_factory, "bind", None)
|
||||||
|
if bind is None:
|
||||||
|
tmp_sess = session_factory()
|
||||||
|
try:
|
||||||
|
bind = tmp_sess.get_bind()
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
tmp_sess.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
eng: Engine = bind.engine if isinstance(bind, Connection) else cast(Engine, bind)
|
eng: Engine = bind.engine if isinstance(bind, Connection) else cast(Engine, bind)
|
||||||
self.backend = backend or make_backend_info(eng)
|
self.backend = backend or make_backend_info(eng)
|
||||||
|
|
||||||
|
|
@ -147,6 +158,14 @@ class CRUDService(Generic[T]):
|
||||||
return self.session.query(poly), poly
|
return self.session.query(poly), poly
|
||||||
return self.session.query(self.model), self.model
|
return self.session.query(self.model), self.model
|
||||||
|
|
||||||
|
def _debug_bind(self, where: str):
|
||||||
|
try:
|
||||||
|
bind = self.session.get_bind()
|
||||||
|
eng = getattr(bind, "engine", bind)
|
||||||
|
print(f"SERVICE BIND [{where}]: engine_id={id(eng)} url={getattr(eng, 'url', '?')} session={type(self.session).__name__}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"SERVICE BIND [{where}]: failed to introspect bind: {e}")
|
||||||
|
|
||||||
def _apply_not_deleted(self, query, root_alias, params) -> Any:
|
def _apply_not_deleted(self, query, root_alias, params) -> Any:
|
||||||
if self.supports_soft_delete and not _is_truthy((params or {}).get("include_deleted")):
|
if self.supports_soft_delete and not _is_truthy((params or {}).get("include_deleted")):
|
||||||
return query.filter(getattr(root_alias, "is_deleted") == False)
|
return query.filter(getattr(root_alias, "is_deleted") == False)
|
||||||
|
|
@ -224,6 +243,7 @@ class CRUDService(Generic[T]):
|
||||||
- forward/backward seek via `key` and `backward`
|
- forward/backward seek via `key` and `backward`
|
||||||
Returns a SeekWindow with items, first/last keys, order spec, limit, and optional total.
|
Returns a SeekWindow with items, first/last keys, order spec, limit, and optional total.
|
||||||
"""
|
"""
|
||||||
|
self._debug_bind("seek_window")
|
||||||
session = self.session
|
session = self.session
|
||||||
query, root_alias = self.get_query()
|
query, root_alias = self.get_query()
|
||||||
|
|
||||||
|
|
@ -455,6 +475,7 @@ class CRUDService(Generic[T]):
|
||||||
|
|
||||||
def get(self, id: int, params=None) -> T | None:
|
def get(self, id: int, params=None) -> T | None:
|
||||||
"""Fetch a single row by id with conflict-free eager loading and clean projection."""
|
"""Fetch a single row by id with conflict-free eager loading and clean projection."""
|
||||||
|
self._debug_bind("get")
|
||||||
query, root_alias = self.get_query()
|
query, root_alias = self.get_query()
|
||||||
|
|
||||||
# Defaults so we can build a projection even if params is None
|
# Defaults so we can build a projection even if params is None
|
||||||
|
|
@ -549,6 +570,7 @@ class CRUDService(Generic[T]):
|
||||||
|
|
||||||
def list(self, params=None) -> list[T]:
|
def list(self, params=None) -> list[T]:
|
||||||
"""Offset/limit listing with smart relationship loading and clean projection."""
|
"""Offset/limit listing with smart relationship loading and clean projection."""
|
||||||
|
self._debug_bind("list")
|
||||||
query, root_alias = self.get_query()
|
query, root_alias = self.get_query()
|
||||||
|
|
||||||
# Defaults so we can reference them later even if params is None
|
# Defaults so we can reference them later even if params is None
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,58 @@
|
||||||
|
# crudkit/integrations/flask.py
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from sqlalchemy.orm import scoped_session
|
from sqlalchemy.orm import scoped_session, sessionmaker
|
||||||
from ..engines import CRUDKitRuntime
|
from ..engines import CRUDKitRuntime
|
||||||
from ..config import Config
|
from ..config import Config
|
||||||
|
|
||||||
def init_app(app: Flask, *, runtime: CRUDKitRuntime | None = None, config: type[Config] | None == None):
|
def init_app(app: Flask, *, runtime: CRUDKitRuntime | None = None, config: type[Config] | None = None):
|
||||||
"""
|
"""
|
||||||
Initializes CRUDKit for a Flask app. Provies `app.extensions['crudkit']`
|
Initializes CRUDKit for a Flask app. Provides `app.extensions['crudkit']`
|
||||||
with a runtime (engine + session_factory). Caller manages session lifecycle.
|
with a runtime (engine + session_factory). Caller manages session lifecycle.
|
||||||
"""
|
"""
|
||||||
runtime = runtime or CRUDKitRuntime(config=config)
|
runtime = runtime or CRUDKitRuntime(config=config)
|
||||||
app.extensions.setdefault("crudkit", {})
|
app.extensions.setdefault("crudkit", {})
|
||||||
app.extensions["crudkit"]["runtime"] = runtime
|
app.extensions["crudkit"]["runtime"] = runtime
|
||||||
|
|
||||||
Session = runtime.session_factory
|
# Build ONE sessionmaker bound to the ONE true engine object
|
||||||
if Session is not None:
|
# so engine id == sessionmaker.bind id, always.
|
||||||
app.extensions["crudkit"]["Session"] = scoped_session(Session)
|
engine = runtime.engine
|
||||||
|
SessionFactory = runtime.session_factory or sessionmaker(bind=engine, **runtime._config.session_kwargs())
|
||||||
|
app.extensions["crudkit"]["SessionFactory"] = SessionFactory
|
||||||
|
app.extensions["crudkit"]["Session"] = scoped_session(SessionFactory)
|
||||||
|
|
||||||
|
# Attach pool listeners to the *same* engine the SessionFactory is bound to.
|
||||||
|
# Don’t guess. Don’t hope. Inspect.
|
||||||
|
try:
|
||||||
|
bound_engine = getattr(SessionFactory, "bind", None) or getattr(SessionFactory, "kw", {}).get("bind") or engine
|
||||||
|
pool = bound_engine.pool
|
||||||
|
|
||||||
|
from sqlalchemy import event
|
||||||
|
|
||||||
|
@event.listens_for(pool, "checkout")
|
||||||
|
def _on_checkout(dbapi_conn, conn_record, conn_proxy):
|
||||||
|
sz = pool.size()
|
||||||
|
chk = pool.checkedout()
|
||||||
|
try:
|
||||||
|
conns_in_pool = pool.checkedin()
|
||||||
|
except Exception:
|
||||||
|
conns_in_pool = "?"
|
||||||
|
print(f"POOL CHECKOUT: Pool size: {sz} Connections in pool: {conns_in_pool} "
|
||||||
|
f"Current Overflow: {pool.overflow()} Current Checked out connections: {chk} "
|
||||||
|
f"engine id= {id(bound_engine)}")
|
||||||
|
|
||||||
|
@event.listens_for(pool, "checkin")
|
||||||
|
def _on_checkin(dbapi_conn, conn_record):
|
||||||
|
sz = pool.size()
|
||||||
|
chk = pool.checkedout()
|
||||||
|
try:
|
||||||
|
conns_in_pool = pool.checkedin()
|
||||||
|
except Exception:
|
||||||
|
conns_in_pool = "?"
|
||||||
|
print(f"POOL CHECKIN: Pool size: {sz} Connections in pool: {conns_in_pool} "
|
||||||
|
f"Current Overflow: {pool.overflow()} Current Checked out connections: {chk} "
|
||||||
|
f"engine id= {id(bound_engine)}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[crudkit.init_app] Failed to attach pool listeners: {e}")
|
||||||
|
|
||||||
return runtime
|
return runtime
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
from sqlalchemy.engine import Engine
|
||||||
|
from sqlalchemy import event
|
||||||
|
from sqlalchemy.pool import Pool
|
||||||
|
|
||||||
import crudkit
|
import crudkit
|
||||||
|
|
||||||
|
|
@ -12,12 +15,45 @@ from .routes.index import init_index_routes
|
||||||
from .routes.listing import init_listing_routes
|
from .routes.listing import init_listing_routes
|
||||||
from .routes.entry import init_entry_routes
|
from .routes.entry import init_entry_routes
|
||||||
|
|
||||||
|
def _bind_pool_debug(engine):
|
||||||
|
pool = engine.pool
|
||||||
|
eid = id(engine)
|
||||||
|
|
||||||
|
@event.listens_for(pool, "checkout")
|
||||||
|
def _on_checkout(dbapi_con, con_record, con_proxy):
|
||||||
|
try:
|
||||||
|
print(f"POOL CHECKOUT: {pool.status()} engine id= {eid}")
|
||||||
|
except Exception:
|
||||||
|
# pool.status is safe on SQLA 2.x, but let's be defensive
|
||||||
|
print(f"POOL CHECKOUT (no status) engine id= {eid}")
|
||||||
|
|
||||||
|
@event.listens_for(pool, "checkin")
|
||||||
|
def _on_checkin(dbapi_con, con_record):
|
||||||
|
try:
|
||||||
|
print(f"POOL CHECKIN: {pool.status()} engine id= {eid}")
|
||||||
|
except Exception:
|
||||||
|
print(f"POOL CHECKIN (no status) engine id= {eid}")
|
||||||
|
|
||||||
def create_app(config_cls=crudkit.DevConfig) -> Flask:
|
def create_app(config_cls=crudkit.DevConfig) -> Flask:
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
init_pretty(app)
|
init_pretty(app)
|
||||||
|
|
||||||
runtime = init_app(app, config=crudkit.ProdConfig)
|
runtime = init_app(app, config=crudkit.ProdConfig)
|
||||||
|
from sqlalchemy import event
|
||||||
|
|
||||||
|
engine = runtime.engine
|
||||||
|
print(f"CRUDKit engine id={id(runtime.engine)} url={runtime.engine.url}")
|
||||||
|
_bind_pool_debug(runtime.engine) # ← attach to the real engine’s pool
|
||||||
|
|
||||||
|
# quick status endpoint you can hit while clicking around
|
||||||
|
@app.get("/_db_status")
|
||||||
|
def _db_status():
|
||||||
|
try:
|
||||||
|
return {"pool": engine.pool.status()}
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": str(e)}, 500
|
||||||
|
|
||||||
crudkit.init_crud(app)
|
crudkit.init_crud(app)
|
||||||
print(f"Effective DB URL: {str(runtime.engine.url)}")
|
print(f"Effective DB URL: {str(runtime.engine.url)}")
|
||||||
|
|
||||||
|
|
@ -25,12 +61,18 @@ def create_app(config_cls=crudkit.DevConfig) -> Flask:
|
||||||
from .routes.reports import bp_reports
|
from .routes.reports import bp_reports
|
||||||
_models.Base.metadata.create_all(bind=runtime.engine)
|
_models.Base.metadata.create_all(bind=runtime.engine)
|
||||||
|
|
||||||
|
# ensure extensions carries the scoped_session from integrations.init_app
|
||||||
Session = app.extensions["crudkit"].get("Session")
|
Session = app.extensions["crudkit"].get("Session")
|
||||||
if Session is None:
|
if Session is not None:
|
||||||
SessionFactory = runtime.session_factory
|
# tell crud layer to use it for any future services
|
||||||
Session = SessionFactory
|
if hasattr(crudkit, "crud") and hasattr(crudkit.crud, "configure_session_factory"):
|
||||||
|
crudkit.crud.configure_session_factory(Session)
|
||||||
session = Session
|
# patch any services already created before this point
|
||||||
|
try:
|
||||||
|
for svc in getattr(crudkit.crud, "_services", {}).values():
|
||||||
|
svc._session_factory = Session
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
crudkit.crud.register_many([
|
crudkit.crud.register_many([
|
||||||
_models.Area,
|
_models.Area,
|
||||||
|
|
@ -54,7 +96,10 @@ def create_app(config_cls=crudkit.DevConfig) -> Flask:
|
||||||
@app.teardown_appcontext
|
@app.teardown_appcontext
|
||||||
def _remove_session(_exc):
|
def _remove_session(_exc):
|
||||||
sess = app.extensions["crudkit"].get("Session")
|
sess = app.extensions["crudkit"].get("Session")
|
||||||
if sess is not None:
|
if hasattr(sess, "remove"):
|
||||||
|
try:
|
||||||
sess.remove()
|
sess.remove()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
@ -12,6 +12,7 @@ _engine = None
|
||||||
SessionLocal = None
|
SessionLocal = None
|
||||||
|
|
||||||
def init_db(database_url: str, engine_kwargs: Dict[str, Any], session_kwargs: Dict[str, Any]) -> None:
|
def init_db(database_url: str, engine_kwargs: Dict[str, Any], session_kwargs: Dict[str, Any]) -> None:
|
||||||
|
print("AM I EVEN BEING RUN?!")
|
||||||
global _engine, SessionLocal
|
global _engine, SessionLocal
|
||||||
print(database_url)
|
print(database_url)
|
||||||
_engine = create_engine(database_url, **engine_kwargs)
|
_engine = create_engine(database_url, **engine_kwargs)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
from flask import Blueprint, render_template, abort, request, jsonify
|
from flask import Blueprint, render_template, abort, request, jsonify, current_app
|
||||||
|
|
||||||
import crudkit
|
import crudkit
|
||||||
|
|
||||||
from crudkit.api._cursor import decode_cursor, encode_cursor
|
|
||||||
from crudkit.ui.fragments import render_form
|
from crudkit.ui.fragments import render_form
|
||||||
|
|
||||||
bp_entry = Blueprint("entry", __name__)
|
bp_entry = Blueprint("entry", __name__)
|
||||||
|
|
@ -12,34 +10,31 @@ def init_entry_routes(app):
|
||||||
@bp_entry.get("/entry/<model>/<int:id>")
|
@bp_entry.get("/entry/<model>/<int:id>")
|
||||||
def entry(model: str, id: int):
|
def entry(model: str, id: int):
|
||||||
cls = crudkit.crud.get_model(model)
|
cls = crudkit.crud.get_model(model)
|
||||||
if cls is None:
|
if cls is None or model not in ["inventory", "worklog", "user"]:
|
||||||
abort(404)
|
|
||||||
if model not in ["inventory", "worklog", "user"]:
|
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
fields = {}
|
fields = {}
|
||||||
fields_spec = []
|
fields_spec = []
|
||||||
layout = []
|
layout = []
|
||||||
|
|
||||||
if model == "inventory":
|
if model == "inventory":
|
||||||
fields["fields"] = ["label", "name", "serial", "barcode", "brand", "model", "device_type", "owner", "location", "condition", "image", "notes"]
|
fields["fields"] = ["label", "name", "serial", "barcode", "brand", "model",
|
||||||
|
"device_type", "owner", "location", "condition", "image", "notes"]
|
||||||
fields_spec = [
|
fields_spec = [
|
||||||
{"name": "label", "type": "display", "label": "", "row": "label",
|
{"name": "label", "type": "display", "label": "", "row": "label",
|
||||||
"attrs": {"class": "display-6 mb-3"}},
|
"attrs": {"class": "display-6 mb-3"}},
|
||||||
|
|
||||||
{"name": "name", "row": "names", "label": "Name", "wrap": {"class": "col-3"},
|
{"name": "name", "row": "names", "label": "Name", "wrap": {"class": "col-3"},
|
||||||
"attrs": {"class": "form-control"}, "label_attrs": {"class": "form-label"}},
|
"attrs": {"class": "form-control"}, "label_attrs": {"class": "form-label"}},
|
||||||
{"name": "serial", "row": "names", "label": "Serial #", "wrap": {"class": "col"},
|
{"name": "serial", "row": "names", "label": "Serial #", "wrap": {"class": "col"},
|
||||||
"attrs": {"class": "form-control"}, "label_attrs": {"class": "form-label"}},
|
"attrs": {"class": "form-control"}, "label_attrs": {"class": "form-label"}},
|
||||||
{"name": "barcode", "row": "names", "label": "Barcode #", "wrap": {"class": "col"},
|
{"name": "barcode", "row": "names", "label": "Barcode #", "wrap": {"class": "col"},
|
||||||
"attrs": {"class": "form-control"}, "label_attrs": {"class": "form-label"}},
|
"attrs": {"class": "form-control"}, "label_attrs": {"class": "form-label"}},
|
||||||
|
|
||||||
{"name": "brand", "label_spec": "{name}", "row": "device", "wrap": {"class": "col"},
|
{"name": "brand", "label_spec": "{name}", "row": "device", "wrap": {"class": "col"},
|
||||||
"attrs": {"class": "form-control"}, "label": "Brand", "label_attrs": {"class": "form-label"}},
|
"attrs": {"class": "form-control"}, "label": "Brand", "label_attrs": {"class": "form-label"}},
|
||||||
{"name": "model", "row": "device", "wrap": {"class": "col"}, "attrs": {"class": "form-control"},
|
{"name": "model", "row": "device", "wrap": {"class": "col"},
|
||||||
"label": "Model #", "label_attrs": {"class": "form-label"}},
|
"attrs": {"class": "form-control"}, "label": "Model #", "label_attrs": {"class": "form-label"}},
|
||||||
{"name": "device_type", "label_spec": "{description}", "row": "device", "wrap": {"class": "col"},
|
{"name": "device_type", "label_spec": "{description}", "row": "device", "wrap": {"class": "col"},
|
||||||
"attrs": {"class": "form-control"}, "label": "Device Type", "label_attrs": {"class": "form-label"}},
|
"attrs": {"class": "form-control"}, "label": "Device Type", "label_attrs": {"class": "form-label"}},
|
||||||
|
|
||||||
{"name": "owner", "row": "status", "label": "Contact", "wrap": {"class": "col"},
|
{"name": "owner", "row": "status", "label": "Contact", "wrap": {"class": "col"},
|
||||||
"attrs": {"class": "form-control"}, "label_attrs": {"class": "form-label"},
|
"attrs": {"class": "form-control"}, "label_attrs": {"class": "form-label"},
|
||||||
"label_spec": "{label}"},
|
"label_spec": "{label}"},
|
||||||
|
|
@ -48,13 +43,12 @@ def init_entry_routes(app):
|
||||||
"label_spec": "{name} - {room_function.description}"},
|
"label_spec": "{name} - {room_function.description}"},
|
||||||
{"name": "condition", "row": "status", "label": "Condition", "wrap": {"class": "col"},
|
{"name": "condition", "row": "status", "label": "Condition", "wrap": {"class": "col"},
|
||||||
"attrs": {"class": "form-control"}, "label_attrs": {"class": "form-label"}},
|
"attrs": {"class": "form-control"}, "label_attrs": {"class": "form-label"}},
|
||||||
|
|
||||||
{"name": "image", "label": "", "row": "image", "type": "template", "label_spec": "{filename}",
|
{"name": "image", "label": "", "row": "image", "type": "template", "label_spec": "{filename}",
|
||||||
"template": "image_display.html", "attrs": {"class": "img-fluid img-thumbnail h-auto"},
|
"template": "image_display.html", "attrs": {"class": "img-fluid img-thumbnail h-auto"},
|
||||||
"wrap": {"class": "h-100 w-100"}},
|
"wrap": {"class": "h-100 w-100"}},
|
||||||
|
|
||||||
{"name": "notes", "type": "textarea", "label": "Notes", "row": "notes", "wrap": {"class": "col"},
|
{"name": "notes", "type": "textarea", "label": "Notes", "row": "notes", "wrap": {"class": "col"},
|
||||||
"attrs": {"class": "form-control", "rows": 10, "style": "resize: none;"}, "label_attrs": {"class": "form-label"}},
|
"attrs": {"class": "form-control", "rows": 10, "style": "resize: none;"},
|
||||||
|
"label_attrs": {"class": "form-label"}},
|
||||||
]
|
]
|
||||||
layout = [
|
layout = [
|
||||||
{"name": "label", "order": 5},
|
{"name": "label", "order": 5},
|
||||||
|
|
@ -66,85 +60,79 @@ def init_entry_routes(app):
|
||||||
{"name": "notes", "order": 45, "attrs": {"class": "row mt-2"}, "parent": "everything"},
|
{"name": "notes", "order": 45, "attrs": {"class": "row mt-2"}, "parent": "everything"},
|
||||||
{"name": "image", "order": 50, "attrs": {"class": "col-4"}, "parent": "kitchen_sink"},
|
{"name": "image", "order": 50, "attrs": {"class": "col-4"}, "parent": "kitchen_sink"},
|
||||||
]
|
]
|
||||||
|
|
||||||
elif model.lower() == 'user':
|
elif model.lower() == 'user':
|
||||||
fields["fields"] = ["label", "first_name", "last_name", "title", "active", "staff", "location", "supervisor"]
|
fields["fields"] = ["label", "first_name", "last_name", "title", "active", "staff", "location", "supervisor"]
|
||||||
fields_spec = [
|
fields_spec = [
|
||||||
{"name": "label", "row": "label", "label": "", "type": "display",
|
{"name": "label", "row": "label", "label": "", "type": "display",
|
||||||
"attrs": {"class": "display-6 mb-3"}},
|
"attrs": {"class": "display-6 mb-3"}},
|
||||||
|
|
||||||
{"name": "last_name", "label": "Last Name", "label_attrs": {"class": "form-label"},
|
{"name": "last_name", "label": "Last Name", "label_attrs": {"class": "form-label"},
|
||||||
"attrs": {"placeholder": "Doe", "class": "form-control"},
|
"attrs": {"placeholder": "Doe", "class": "form-control"}, "row": "name", "wrap": {"class": "col-3"}},
|
||||||
"row": "name", "wrap": {"class": "col-3"}},
|
|
||||||
|
|
||||||
{"name": "first_name", "label": "First Name", "label_attrs": {"class": "form-label"},
|
{"name": "first_name", "label": "First Name", "label_attrs": {"class": "form-label"},
|
||||||
"attrs": {"placeholder": "John", "class": "form-control"},
|
"attrs": {"placeholder": "John", "class": "form-control"}, "row": "name", "wrap": {"class": "col-3"}},
|
||||||
"row": "name", "wrap": {"class": "col-3"}},
|
|
||||||
|
|
||||||
{"name": "title", "label": "Title", "label_attrs": {"class": "form-label"},
|
{"name": "title", "label": "Title", "label_attrs": {"class": "form-label"},
|
||||||
"attrs": {"placeholder": "President of the Universe", "class": "form-control"},
|
"attrs": {"placeholder": "President of the Universe", "class": "form-control"},
|
||||||
"row": "name", "wrap": {"class": "col-3"}},
|
"row": "name", "wrap": {"class": "col-3"}},
|
||||||
|
|
||||||
{"name": "supervisor", "label": "Supervisor", "label_attrs": {"class": "form-label"},
|
{"name": "supervisor", "label": "Supervisor", "label_attrs": {"class": "form-label"},
|
||||||
"label_spec": "{label}", "row": "details", "wrap": {"class": "col-3"},
|
"label_spec": "{label}", "row": "details", "wrap": {"class": "col-3"},
|
||||||
"attrs": {"class": "form-control"}},
|
"attrs": {"class": "form-control"}},
|
||||||
|
|
||||||
{"name": "location", "label": "Room", "label_attrs": {"class": "form-label"},
|
{"name": "location", "label": "Room", "label_attrs": {"class": "form-label"},
|
||||||
"label_spec": "{name} - {room_function.description}",
|
"label_spec": "{name} - {room_function.description}",
|
||||||
"row": "details", "wrap": {"class": "col-3"}, "attrs": {"class": "form-control"}},
|
"row": "details", "wrap": {"class": "col-3"}, "attrs": {"class": "form-control"}},
|
||||||
|
|
||||||
{"name": "active", "label": "Active", "label_attrs": {"class": "form-check-label"},
|
{"name": "active", "label": "Active", "label_attrs": {"class": "form-check-label"},
|
||||||
"row": "checkboxes", "attrs": {"class": "form-check-input"}, "wrap": {"class": "form-check"}},
|
"row": "checkboxes", "attrs": {"class": "form-check-input"}, "wrap": {"class": "form-check"}},
|
||||||
|
|
||||||
{"name": "staff", "label": "Staff Member", "label_attrs": {"class": "form-check-label"},
|
{"name": "staff", "label": "Staff Member", "label_attrs": {"class": "form-check-label"},
|
||||||
"row": "checkboxes", "attrs": {"class": "form-check-input"}, "wrap": {"class": "form-check"}}
|
"row": "checkboxes", "attrs": {"class": "form-check-input"}, "wrap": {"class": "form-check"}},
|
||||||
]
|
]
|
||||||
layout = [
|
layout = [
|
||||||
{"name": "label", "order": 0},
|
{"name": "label", "order": 0},
|
||||||
{"name": "name", "order": 10, "attrs": {"class": "row"}},
|
{"name": "name", "order": 10, "attrs": {"class": "row"}},
|
||||||
{"name": "details", "order": 20, "attrs": {"class": "row mt-2"}},
|
{"name": "details", "order": 20, "attrs": {"class": "row mt-2"}},
|
||||||
{"name": "checkboxes", "order": 30, "parent": "name", "attrs": {"class": "col d-flex flex-column justify-content-end"}}
|
{"name": "checkboxes", "order": 30, "parent": "name",
|
||||||
|
"attrs": {"class": "col d-flex flex-column justify-content-end"}},
|
||||||
]
|
]
|
||||||
|
|
||||||
elif model == "worklog":
|
elif model == "worklog":
|
||||||
fields["fields"] = ["id", "contact", "work_item", "start_time", "end_time", "complete"]
|
fields["fields"] = ["id", "contact", "work_item", "start_time", "end_time", "complete"]
|
||||||
fields_spec = [
|
fields_spec = [
|
||||||
{"name": "id", "label": "", "type": "display", "label_spec": "Work Item #{id}",
|
{"name": "id", "label": "", "type": "display", "label_spec": "Work Item #{id}",
|
||||||
"attrs": {"class": "display-6 mb-3"}, "row": "label"},
|
"attrs": {"class": "display-6 mb-3"}, "row": "label"},
|
||||||
|
|
||||||
{"name": "contact", "row": "ownership", "wrap": {"class": "col"}, "label": "Contact",
|
{"name": "contact", "row": "ownership", "wrap": {"class": "col"}, "label": "Contact",
|
||||||
"label_spec": "{label}", "attrs": {"class": "form-control"},
|
"label_spec": "{label}", "attrs": {"class": "form-control"}, "label_attrs": {"class": "form-label"}},
|
||||||
"label_attrs": {"class": "form-label"}},
|
|
||||||
{"name": "work_item", "row": "ownership", "wrap": {"class": "col"}, "label": "Work Item",
|
{"name": "work_item", "row": "ownership", "wrap": {"class": "col"}, "label": "Work Item",
|
||||||
"label_spec": "{label}", "attrs": {"class": "form-control"}, "label_attrs": {"class": "form-label"}},
|
"label_spec": "{label}", "attrs": {"class": "form-control"}, "label_attrs": {"class": "form-label"}},
|
||||||
|
|
||||||
{"name": "start_time", "type": "datetime", "attrs": {"class": "form-control"}, "row": "timestamps",
|
{"name": "start_time", "type": "datetime", "attrs": {"class": "form-control"}, "row": "timestamps",
|
||||||
"wrap": {"class": "col"}, "label_attrs": {"class": "form-label"}, "label": "Start"},
|
"wrap": {"class": "col"}, "label_attrs": {"class": "form-label"}, "label": "Start"},
|
||||||
{"name": "end_time", "type": "datetime", "attrs": {"class": "form-control"}, "row": "timestamps",
|
{"name": "end_time", "type": "datetime", "attrs": {"class": "form-control"}, "row": "timestamps",
|
||||||
"wrap": {"class": "col"}, "label_attrs": {"class": "form-label"}, "label": "End"},
|
"wrap": {"class": "col"}, "label_attrs": {"class": "form-label"}, "label": "End"},
|
||||||
{"name": "complete", "label": "Complete", "label_attrs": {"class": "form-check-label"},
|
{"name": "complete", "label": "Complete", "label_attrs": {"class": "form-check-label"},
|
||||||
"attrs": {"class": "form-check-input"}, "row": "timestamps", "wrap": {"class": "col form-check"}},
|
"attrs": {"class": "form-check-input"}, "row": "timestamps", "wrap": {"class": "col form-check"}},
|
||||||
|
|
||||||
{"name": "updates", "label": "Updates", "row": "updates", "label_attrs": {"class": "form-label"},
|
{"name": "updates", "label": "Updates", "row": "updates", "label_attrs": {"class": "form-label"},
|
||||||
"type": "template", "template": "update_list.html"}
|
"type": "template", "template": "update_list.html"},
|
||||||
]
|
]
|
||||||
layout = [
|
layout = [
|
||||||
{"name": "label", "order": 0},
|
{"name": "label", "order": 0},
|
||||||
{"name": "ownership", "order": 10, "attrs": {"class": "row mb-2"}},
|
{"name": "ownership", "order": 10, "attrs": {"class": "row mb-2"}},
|
||||||
{"name": "timestamps", "order": 20, "attrs": {"class": "row d-flex align-items-center"}},
|
{"name": "timestamps", "order": 20, "attrs": {"class": "row d-flex align-items-center"}},
|
||||||
{"name": "updates", "order": 30, "attrs": {"class": "row"}}
|
{"name": "updates", "order": 30, "attrs": {"class": "row"}},
|
||||||
]
|
]
|
||||||
|
|
||||||
obj = crudkit.crud.get_service(cls).get(id, fields)
|
svc = crudkit.crud.get_service(cls)
|
||||||
|
obj = svc.get(id, fields)
|
||||||
if obj is None:
|
if obj is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
|
# Use the scoped_session proxy so teardown .remove() cleans it up
|
||||||
|
ScopedSession = current_app.extensions["crudkit"]["Session"]
|
||||||
|
|
||||||
form = render_form(
|
form = render_form(
|
||||||
cls,
|
cls,
|
||||||
obj.as_dict(),
|
obj.as_dict(),
|
||||||
crudkit.crud.get_service(cls).session,
|
session=ScopedSession, # ← fixed: pass scoped proxy, not svc.session
|
||||||
instance=obj,
|
instance=obj,
|
||||||
fields_spec=fields_spec,
|
fields_spec=fields_spec,
|
||||||
layout=layout,
|
layout=layout,
|
||||||
submit_attrs={"class": "btn btn-primary mt-3"}
|
submit_attrs={"class": "btn btn-primary mt-3"},
|
||||||
)
|
)
|
||||||
return render_template("entry.html", form=form)
|
return render_template("entry.html", form=form)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,30 @@ def init_listing_routes(app):
|
||||||
spec["sort"] = sort
|
spec["sort"] = sort
|
||||||
|
|
||||||
service = crudkit.crud.get_service(cls)
|
service = crudkit.crud.get_service(cls)
|
||||||
|
try:
|
||||||
|
rt = app.extensions["crudkit"]["runtime"]
|
||||||
|
rt_engine = rt.engine
|
||||||
|
except Exception:
|
||||||
|
rt_engine = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
SessionFactory = app.extensions["crudkit"].get("SessionFactory")
|
||||||
|
sf_engine = getattr(SessionFactory, "bind", None) or getattr(SessionFactory, "kw", {}).get("bind")
|
||||||
|
except Exception:
|
||||||
|
sf_engine = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
bind = service.session.get_bind()
|
||||||
|
svc_engine = getattr(bind, "engine", bind)
|
||||||
|
except Exception:
|
||||||
|
svc_engine = None
|
||||||
|
|
||||||
|
print(
|
||||||
|
"LISTING ENGINES: "
|
||||||
|
f"runtime={id(rt_engine) if rt_engine else None} "
|
||||||
|
f"session_factory.bind={id(sf_engine) if sf_engine else None} "
|
||||||
|
f"service.bind={id(svc_engine) if svc_engine else None}"
|
||||||
|
)
|
||||||
# include limit and go
|
# include limit and go
|
||||||
window = service.seek_window(spec | {"limit": limit}, key=key, backward=backward, include_total=True)
|
window = service.seek_window(spec | {"limit": limit}, key=key, backward=backward, include_total=True)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue