Pager fixes and non-leaky session behavior.

This commit is contained in:
Yaro Kasear 2025-08-28 11:55:58 -05:00
parent faf5b10e56
commit 7de7f8df86
2 changed files with 59 additions and 11 deletions

View file

@ -1,4 +1,5 @@
{% macro options(items, value_attr="id", label_path="name", getp=None) -%}
{%- for obj in items -%}
<option value="{{ getp(obj, value_attr) }}">{{ getp(obj, label_path) }}</option>
{%- endfor -%}
@ -32,14 +33,31 @@
{%- endmacro %}
{% macro pager(model, page, pages, per_page, sort, filters, fields_csv) -%}
<nav id="pager" hx-swap-oob="true">
<a hx-get="/ui/{{ model }}/frag/rows?page={{ page-1 }}&per_page={{ per_page }}{% if sort %}&sort={{ sort }}{% endif %}{% if fields_csv %}&fields_csv={{ fields_csv|urlencode }}{% endif %}{% for k,v in filters.items() %}&{{k}}={{ v|urlencode }}{% endfor %}"
hx-target="#rows" rel="prev" hx-swap="innerHTML">Prev</a>
{% set p = page|int %}
{% set pg = pages|int %}
{% set prev = [1, p-1]|max %}
{% set nxt = [pg, p+1]|min %}
<nav id="pager">
{% if page > 1 %}
<button type="button"
hx-get="/ui/{{ model }}/frag/rows?page=1&per_page={{ per_page }}{% if sort %}&sort={{ sort }}{% endif %}{% if fields_csv %}&fields_csv={{ fields_csv|urlencode }}{% endif %}{% for k,v in filters.items() %}&{{k}}={{ v|urlencode }}{% endfor %}"
hx-target="#rows" hx-swap="innerHTML">First</button>
<button type="button"
hx-get="/ui/{{ model }}/frag/rows?page={{ prev }}&per_page={{ per_page }}{% if sort %}&sort={{ sort }}{% endif %}{% if fields_csv %}&fields_csv={{ fields_csv|urlencode }}{% endif %}{% for k,v in (filters or {}).items() %}&{{k}}={{ v|urlencode }}{% endfor %}"
hx-target="#rows" hx-swap="innerHTML"
hx-on:click="document.querySelector('#pager-state input[name=page]').value='{{ prev }}'">Prev</button>
{% endif %}
<span>Page {{ page }} / {{ pages }}</span>
<a hx-get="/ui/{{ model }}/frag/rows?page={{ page+1 }}&per_page={{ per_page }}{% if sort %}&sort={{ sort }}{% endif %}{% if fields_csv %}&fields_csv={{ fields_csv|urlencode }}{% endif %}{% for k,v in filters.items() %}&{{k}}={{ v|urlencode }}{% endfor %}"
hx-target="#rows" rel="next" hx-swap="innerHTML">Next</a>
{% if page < pages %} <button type="button"
hx-get="/ui/{{ model }}/frag/rows?page={{ page+1 }}&per_page={{ per_page }}{% if sort %}&sort={{ sort }}{% endif %}{% if fields_csv %}&fields_csv={{ fields_csv|urlencode }}{% endif %}{% for k,v in filters.items() %}&{{k}}={{ v|urlencode }}{% endfor %}"
hx-target="#rows" rel="next" hx-swap="innerHTML">Next</button>
<button type="button"
hx-get="/ui/{{ model }}/frag/rows?page={{ nxt }}&per_page={{ per_page }}{% if sort %}&sort={{ sort }}{% endif %}{% if fields_csv %}&fields_csv={{ fields_csv|urlencode }}{% endif %}{% for k,v in (filters or {}).items() %}&{{k}}={{ v|urlencode }}{% endfor %}"
hx-target="#rows" hx-swap="innerHTML"
hx-on:click="document.querySelector('#pager-state input[name=page]').value='{{ nxt }}'">Last</button>
{% endif %}
</nav>
{%- endmacro %}

View file

@ -12,6 +12,8 @@ from ..service import CrudService
from ..eager import default_eager_policy
from .type_map import build_form_schema
Session = None
def make_fragments_blueprint(db_session_factory, registry: Dict[str, Any], *, name="frags"):
"""
HTML fragments for HTMX/Alpine. No base pages. Pure partials:
@ -20,8 +22,18 @@ def make_fragments_blueprint(db_session_factory, registry: Dict[str, Any], *, na
GET /<model>/frag/rows -> <tr>...</tr> + pager markup if wanted
GET /<model>/frag/form -> <form>...</form> (auto-generated)
"""
global Session
if Session is None:
Session = scoped_session(db_session_factory)
bp = Blueprint(name, __name__, template_folder="templates")
def session(): return scoped_session(db_session_factory)()
def session():
return Session
@bp.teardown_app_request
def remove_session(exc=None):
Session.remove()
def _parse_filters(args):
reserved = {"page", "per_page", "sort", "expand", "fields", "value", "label", "label_tpl", "fields_csv", "li_label", "li_sublabel"}
@ -106,14 +118,32 @@ def make_fragments_blueprint(db_session_factory, registry: Dict[str, Any], *, na
expand = _collect_expand_from_paths(fields)
spec = QuerySpec(filters=filters, order_by=[sort] if sort else [], page=page, per_page=per_page, expand=expand)
s = session(); svc = CrudService(s, default_eager_policy)
rows, total = svc.list(Model, spec)
rows, _ = svc.list(Model, spec)
html = render_template("crudkit/rows.html", items=rows, fields=fields, getp=_getp)
return html
@bp.get("/<model>/frag/pager")
def pager(model):
Model = registry.get(model) or abort(404)
page = request.args.get("page", type=int) or 1
print(page)
per_page = request.args.get("per_page", type=int) or 20
filters = _parse_filters(request.args)
sort = request.args.get("sort")
fields_csv = request.args.get("fields_csv") or "id,name"
fields = _paths_from_csv(fields_csv)
expand = _collect_expand_from_paths(fields)
spec = QuerySpec(filters=filters, order_by=[sort] if sort else [], page=page, per_page=per_page, expand=expand)
s = session(); svc = CrudService(s, default_eager_policy)
_, total = svc.list(Model, spec)
pages = max(1, ceil(total / per_page))
rows_html = render_template("crudkit/rows.html", items=rows, fields=fields, getp=_getp)
pager_html = render_template("crudkit/pager.html", model=model, page=page, pages=pages,
html = render_template("crudkit/pager.html", model=model, page=page, pages=pages,
per_page=per_page, sort=sort, filters=filters, fields_csv=fields_csv)
return rows_html + pager_html
return html
@bp.get("/<model>/frag/form")
def form(model):