Soem crudkit fixes aimed at eliminating session sharing.

This commit is contained in:
Yaro Kasear 2025-09-15 11:51:56 -05:00
parent d045a1a05f
commit cf795086f1
10 changed files with 107 additions and 47 deletions

View file

@ -123,6 +123,7 @@ def build_database_url(
"Trusted_Connection": "yes", "Trusted_Connection": "yes",
"Encrypt": "yes", "Encrypt": "yes",
"TrustServerCertificate": "yes", "TrustServerCertificate": "yes",
"MARS_Connection": "yes",
} }
base_opts.update(options) base_opts.update(options)
qs = "?" + "&".join(f"{k}={quote_plus(v)}" for k, v in base_opts.items()) qs = "?" + "&".join(f"{k}={quote_plus(v)}" for k, v in base_opts.items())
@ -135,6 +136,7 @@ def build_database_url(
"driver": driver, "driver": driver,
"Encrypt": "yes", "Encrypt": "yes",
"TrustServerCertificate": "yes", "TrustServerCertificate": "yes",
"MARS_Connection": "yes",
} }
base_opts.update(options) base_opts.update(options)
qs = "?" + "&".join(f"{k}={quote_plus(v)}" for k, v in base_opts.items()) qs = "?" + "&".join(f"{k}={quote_plus(v)}" for k, v in base_opts.items())

View file

@ -1,4 +1,4 @@
from typing import Any, Type, TypeVar, Generic, Optional, Protocol, runtime_checkable, cast from typing import Any, Callable, Type, TypeVar, Generic, Optional, Protocol, runtime_checkable, cast
from sqlalchemy.orm import Load, Session, raiseload, with_polymorphic, Mapper from sqlalchemy.orm import Load, Session, raiseload, with_polymorphic, Mapper
from sqlalchemy.orm.attributes import InstrumentedAttribute from sqlalchemy.orm.attributes import InstrumentedAttribute
from sqlalchemy.orm.util import AliasedClass from sqlalchemy.orm.util import AliasedClass
@ -37,13 +37,13 @@ class CRUDService(Generic[T]):
def __init__( def __init__(
self, self,
model: Type[T], model: Type[T],
session: Session, session_factory: Callable[[], Session],
polymorphic: bool = False, polymorphic: bool = False,
*, *,
backend: Optional[BackendInfo] = None backend: Optional[BackendInfo] = None
): ):
self.model = model self.model = model
self.session = session 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. # Cache backend info once. If not provided, derive from session bind.
@ -51,6 +51,10 @@ class CRUDService(Generic[T]):
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)
@property
def session(self) -> Session:
return self._session_factory()
def get_query(self): def get_query(self):
if self.polymorphic: if self.polymorphic:
poly = with_polymorphic(self.model, "*") poly = with_polymorphic(self.model, "*")
@ -69,7 +73,6 @@ class CRUDService(Generic[T]):
return cols or [text("1")] return cols or [text("1")]
def get(self, id: int, params=None) -> T | None: def get(self, id: int, params=None) -> T | None:
print(f"I AM GETTING A THING! A THINGS! {params}")
query, root_alias = self.get_query() query, root_alias = self.get_query()
include_deleted = False include_deleted = False

View file

@ -71,10 +71,10 @@ class CRUDRegistry:
SessionMaker = self._rt.session_factory SessionMaker = self._rt.session_factory
if SessionMaker is None: if SessionMaker is None:
raise RuntimeError("CRUDKitRuntime.session_factory is not initialized.") raise RuntimeError("CRUDKitRuntime.session_factory is not initialized.")
session: Session = SessionMaker()
svc = CRUDService( svc = CRUDService(
model, model,
session=session, session_factory=SessionMaker,
polymorphic=polymorphic, polymorphic=polymorphic,
backend=self._rt.backend, backend=self._rt.backend,
**(service_kwargs or {}), **(service_kwargs or {}),

View file

@ -1,12 +1,14 @@
from typing import List, Optional, TYPE_CHECKING from typing import List, Optional, TYPE_CHECKING
from sqlalchemy import Boolean, ForeignKey, Integer, Unicode from sqlalchemy import Boolean, ForeignKey, Integer, Unicode, func, case, literal
from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import expression as sql from sqlalchemy.sql import expression as sql
from crudkit.core.base import Base, CRUDMixin from crudkit.core.base import Base, CRUDMixin
from . import RoomFunction
if TYPE_CHECKING: if TYPE_CHECKING:
from .user import User from .user import User
@ -32,4 +34,14 @@ class Room(Base, CRUDMixin):
@hybrid_property @hybrid_property
def label(self): def label(self):
return f"{self.name} - {self.room_function.description}" name = self.name or "Unassigned"
desc = self.room_function.description if (self.room_function and self.room_function.description) else None
return f"{name} - {desc}"
@label.expression
def label(cls):
return func.concat(
func.coalesce(cls.name, literal("Unassigned")),
case((cls.function_id.isnot(None), literal(" - ")), else_=literal("")),
func.coalesce(RoomFunction.description, literal(""))
)

View file

@ -1,15 +1,12 @@
from flask import Blueprint, current_app, jsonify, render_template, request, send_file from flask import Blueprint, current_app, render_template, send_file
from pathlib import Path from pathlib import Path
from sqlalchemy import func, select
import pandas as pd import pandas as pd
from crudkit.core.service import CRUDService import crudkit
from crudkit.core.spec import CRUDSpec
from crudkit.ui.fragments import render_table from crudkit.ui.fragments import render_table
from ..db import get_session
from ..models.device_type import DeviceType
from ..models.inventory import Inventory from ..models.inventory import Inventory
from ..models.work_log import WorkLog from ..models.work_log import WorkLog
@ -18,9 +15,8 @@ bp_index = Blueprint("index", __name__)
def init_index_routes(app): def init_index_routes(app):
@bp_index.get("/") @bp_index.get("/")
def index(): def index():
session = get_session() inventory_service = crudkit.crud.get_service(Inventory)
inventory_service = CRUDService(Inventory, session) work_log_service = crudkit.crud.get_service(WorkLog)
work_log_service = CRUDService(WorkLog, session)
work_logs = work_log_service.list({ work_logs = work_log_service.list({
"complete__ne": 1, "complete__ne": 1,
"fields": [ "fields": [

View file

@ -1,4 +1,4 @@
from flask import Blueprint, render_template, abort from flask import Blueprint, render_template, abort, request
import crudkit import crudkit
@ -9,25 +9,65 @@ bp_listing = Blueprint("listing", __name__)
def init_listing_routes(app): def init_listing_routes(app):
@bp_listing.get("/listing/<model>") @bp_listing.get("/listing/<model>")
def show_list(model): def show_list(model):
page_num = int(request.args.get("page", 1))
if model.lower() not in {"inventory", "user", "worklog"}:
abort(404)
cls = crudkit.crud.get_model(model) cls = crudkit.crud.get_model(model)
if cls is None: if cls is None:
abort(404) abort(404)
spec = {} spec = {}
columns = []
if model.lower() == 'inventory': if model.lower() == 'inventory':
spec = {"fields": [ spec = {"fields": [
"label", "label",
"name", "name",
"barcode", "barcode",
"serial", "serial",
"device_type.description" "brand.name",
"model",
"device_type.description",
"condition",
"owner.label",
"location.label",
]} ]}
spec["limit"] = 0 columns = [
{"field": "label"},
{"field": "name"},
{"field": "barcode", "label": "Barcode #"},
{"field": "serial", "label": "Serial #"},
{"field": "brand.name", "label": "Brand"},
{"field": "model"},
{"field": "device_type.description", "label": "Device Type"},
{"field": "condition"},
{"field": "owner.label", "label": "Contact", "link": {"endpoint": "user.get_item", "params": {"id": "{owner.id}"}}},
{"field": "location.label", "label": "Room"},
]
if model.lower() == 'user':
spec = {"fields": [
"label",
"last_name",
"first_name",
"supervisor.label",
"staff",
"active",
]}
columns = [
{"field": "label", "label": "Full Name"},
{"field": "last_name"},
{"field": "first_name"},
{"field": "supervisor.label", "label": "Supervisor", "link": {"endpoint": "user.get_item", "params": {"id": "{supervisor.id}"}}},
{"field": "staff"},
{"field": "active"},
]
spec["limit"] = 15
spec["offset"] = (page_num - 1) * 15
service = crudkit.crud.get_service(cls) service = crudkit.crud.get_service(cls)
rows = service.list(spec) rows = service.list(spec)
table = render_table(rows, opts={"object_class": model}) table = render_table(rows, columns=columns, opts={"object_class": model})
return render_template("listing.html", model=model, table=table) return render_template("listing.html", model=model, table=table)

View file

@ -29,7 +29,7 @@
{% block premain %} {% block premain %}
{% endblock %} {% endblock %}
<main class="container mt-3"> <main class="container-fluid mt-3">
{% block main %} {% block main %}
{% endblock %} {% endblock %}
</main> </main>

View file

@ -1,4 +1,5 @@
<table class="table table-light table-striped table-hover table-bordered border-tertiary text-nowrap overflow-x-auto"> <div class="table-responsive mx-5" style="max-height: 80vh;">
<table class="table table-light table-striped table-hover table-bordered border-tertiary text-nowrap overflow-x-auto mx-auto">
<thead> <thead>
<tr> <tr>
{% for col in columns %} {% for col in columns %}
@ -23,4 +24,5 @@
<tr><td colspan="{{ columns|length }}">No data.</td></tr> <tr><td colspan="{{ columns|length }}">No data.</td></tr>
{% endif %} {% endif %}
</tbody> </tbody>
</table> </table>
</div>

View file

@ -14,13 +14,13 @@
<h1 class="display-2 text-center">{{ title or "Inventory Manager" }}</h1> <h1 class="display-2 text-center">{{ title or "Inventory Manager" }}</h1>
<p class="lead text-center">Find out about all of your assets.</p> <p class="lead text-center">Find out about all of your assets.</p>
<div class="row"> <div class="row mx-5">
<div class="col pivot-cell"> <div class="col pivot-cell ms-5">
<p class="display-6 text-center">Active Worklogs</p> <p class="display-6 text-center">Active Worklogs</p>
{{ logs | safe }} {{ logs | safe }}
</div> </div>
<div class="col pivot-cell"> <div class="col pivot-cell me-5">
<p class="display-6 text-center">Inventory Report</p> <p class="display-6 text-center">Inventory Report</p>
<div class="d-flex flex-wrap gap-2 align-items-end mb-2"> <div class="d-flex flex-wrap gap-2 align-items-end mb-2">

View file

@ -1,5 +1,10 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}
Inventory Manager - {{ model|title }} Listing
{% endblock %}
{% block main %} {% block main %}
<h1 class="display-4 text-center mt-5">{{ model|title }} Listing</h1>
{{ table | safe }} {{ table | safe }}
{% endblock %} {% endblock %}