Various bug fixes. Still trying to fix cartesian issue on search.

This commit is contained in:
Yaro Kasear 2025-10-09 09:27:54 -05:00
parent 0dbf246bdb
commit 3c07741500
9 changed files with 412 additions and 94 deletions

View file

@ -1,6 +1,6 @@
from __future__ import annotations
import os
import os, logging, sys
from flask import Flask
from jinja_markdown import MarkdownExtension
@ -27,7 +27,7 @@ def create_app(config_cls=crudkit.DevConfig) -> Flask:
init_pretty(app)
runtime = init_app(app, config=crudkit.ProdConfig)
runtime = init_app(app, config=crudkit.DevConfig)
from sqlalchemy import event
engine = runtime.engine

View file

@ -1,14 +1,12 @@
from flask import Blueprint, render_template, abort, request
from flask import Blueprint, render_template, abort, request, url_for
import crudkit
from crudkit.api._cursor import decode_cursor, encode_cursor
from crudkit.ui.fragments import render_table, register_template_globals
bp_listing = Blueprint("listing", __name__)
def init_listing_routes(app):
# Make helpers available in all templates
register_template_globals(app)
@bp_listing.get("/listing/<model>")
@ -21,12 +19,15 @@ def init_listing_routes(app):
abort(404)
# read query args
limit = request.args.get("limit", None)
limit = int(limit) if (limit is not None and str(limit).isdigit()) else 15
sort = request.args.get("sort")
fields_qs = request.args.get("fields")
cursor = request.args.get("cursor")
key, _desc, backward = decode_cursor(cursor)
# accept both per_page and limit; per_page wins if both provided
per_page_qs = request.args.get("per_page")
limit_qs = request.args.get("limit")
page = int(request.args.get("page", 1) or 1)
per_page = int(per_page_qs) if (per_page_qs and per_page_qs.isdigit()) else (
int(limit_qs) if (limit_qs and limit_qs.isdigit()) else 15
)
sort = request.args.get("sort")
fields_qs = request.args.get("fields")
# base spec per model
spec = {}
@ -34,16 +35,8 @@ def init_listing_routes(app):
row_classes = []
if model.lower() == 'inventory':
spec = {"fields": [
"label",
"name",
"barcode",
"serial",
"brand.name",
"model",
"device_type.description",
"condition",
"owner.label",
"location.label",
"label", "name", "barcode", "serial", "brand.name", "model",
"device_type.description", "condition", "owner.label", "location.label",
]}
columns = [
{"field": "label"},
@ -60,14 +53,9 @@ def init_listing_routes(app):
]
elif model.lower() == 'user':
spec = {"fields": [
"label",
"last_name",
"first_name",
"supervisor.label",
"robot.overlord",
"staff",
"active",
], "sort": "first_name,last_name"} # default for users
"label", "last_name", "first_name", "supervisor.label",
"robot.overlord", "staff", "active",
], "sort": "first_name,last_name"}
columns = [
{"field": "label", "label": "Full Name"},
{"field": "last_name"},
@ -86,11 +74,7 @@ def init_listing_routes(app):
]
elif model.lower() == 'worklog':
spec = {"fields": [
"work_item.label",
"contact.label",
"start_time",
"end_time",
"complete",
"work_item.label", "contact.label", "start_time", "end_time", "complete",
]}
columns = [
{"field": "work_item.label", "label": "Work Item",
@ -106,44 +90,106 @@ def init_listing_routes(app):
{"when": {"field": "complete", "is": False}, "class": "table-danger"}
]
# Build params to feed CRUDService (flat dict; parse_filters expects flat keys)
# Build params to feed CRUDService
params = dict(spec)
# overlay fields from query (?fields=...)
if fields_qs:
params["fields"] = [p.strip() for p in fields_qs.split(",") if p.strip()]
# overlay sort from query (?sort=...)
if sort:
params["sort"] = sort
# limit semantics: 0 means "unlimited" in your service layer
params["limit"] = limit
# forward *all other* query params as filters (flat), excluding known control keys
CONTROL_KEYS = {"limit", "cursor", "sort", "fields"}
# forward remaining query params as filters (flat), excluding control keys
CONTROL_KEYS = {"page", "per_page", "limit", "sort", "fields"}
for k, v in request.args.items():
if k in CONTROL_KEYS:
continue
if v is None or v == "":
if k in CONTROL_KEYS or v in (None, ""):
continue
params[k] = v
service = crudkit.crud.get_service(cls)
window = service.seek_window(params, key=key, backward=backward, include_total=True)
# Use page-based pagination from the service
result = service.page(params, page=page, per_page=per_page, include_total=True)
items = result["items"]
table = render_table(window.items, columns=columns,
table = render_table(items, columns=columns,
opts={"object_class": model, "row_classes": row_classes})
def _base_params():
keep = {}
for k, v in request.args.items():
if k == "page" or v in (None, ""):
continue
keep[k] = v
# keep both for compatibility; per_page wins in the service anyway
keep["per_page"] = per_page
keep["limit"] = per_page
return keep
total = int(result["total"] or 0)
pages = int(result["pages"] or 1)
page = int(result["page"] or 1)
has_prev = page > 1
has_next = page < pages
base = _base_params()
prev_url = url_for("listing.show_list", model=model, **{**base, "page": max(1, page - 1)})
next_url = url_for("listing.show_list", model=model, **{**base, "page": min(pages, page + 1)})
def page_url(n: int) -> str:
return url_for("listing.show_list", model=model, **{**base, "page": n})
def build_nav(page: int, pages: int, window: int = 2):
"""
Returns a list like:
[{'type':'page','n':1,'url':'...','active':False}, {'type':'ellipsis'}, ...]
Shows first, last, current±window, with ellipses where gaps exist.
"""
if pages <= 1:
return [{'type': 'page', 'n': 1, 'url': page_url(1), 'active': True}]
show = set([1, pages])
for n in range(max(1, page - window), min(pages, page + window) + 1):
show.add(n)
out = []
last = 0
for n in range(1, pages + 1):
if n in show:
out.append({'type': 'page', 'n': n, 'url': page_url(n), 'active': (n == page)})
last = n
else:
# insert a single ellipsis per gap
if last != -1:
out.append({'type': 'ellipsis'})
last = -1
# skip the interior of the gap
# we let the for loop continue
# collapse any duplicate ellipses at ends (paranoia)
cleaned = []
for i, item in enumerate(out):
if item['type'] == 'ellipsis' and (i == 0 or out[i-1]['type'] == 'ellipsis'):
continue
cleaned.append(item)
if cleaned and cleaned[-1]['type'] == 'ellipsis':
cleaned.pop()
if cleaned and cleaned[0]['type'] == 'ellipsis':
cleaned.pop(0)
return cleaned
pagination_ctx = {
"limit": window.limit,
"total": window.total,
"next_cursor": encode_cursor(window.last_key, list(window.order.desc), backward=False),
"prev_cursor": encode_cursor(window.first_key, list(window.order.desc), backward=True),
"sort": params.get("sort") # expose current sort to the template
"page": page,
"per_page": per_page,
"total": total,
"pages": pages,
"has_prev": has_prev,
"has_next": has_next,
"prev_url": prev_url,
"next_url": next_url,
"nav": build_nav(page, pages, window=2), # tweak window=2..3 to taste
"sort": params.get("sort")
}
return render_template("listing.html", model=model, table=table, pagination=pagination_ctx)
app.register_blueprint(bp_listing)
app.register_blueprint(bp_listing)

View file

@ -76,7 +76,7 @@ def init_search_routes(app):
{"field": "updates", "format": lambda x: len(x)},
]
worklog_results = worklog_service.list({
'contact.label|work_item.label__icontains': q,
'contact.label|work_item.label|updates.content__icontains': q,
'fields': [
"contact.label",
"work_item.label",

View file

@ -8,15 +8,49 @@ Inventory Manager - {{ model|title }} Listing
<div class="container-fluid">
<h1 class="display-4 text-center mt-5">{{ model|title }} Listing</h1>
<div class="btn-group">
<button type="button" class="btn btn-primary mb-3" onclick="location.href='{{ url_for('entry.entry_new', model=model) }}'">New</button>
<button type="button" class="btn btn-primary mb-3"
onclick="location.href='{{ url_for('entry.entry_new', model=model) }}'">New</button>
</div>
</div>
{{ table | safe }}
<div class="d-flex justify-content-evenly mx-5">
<button onclick="location.href='{{ url_for('listing.show_list', model=model, cursor=pagination['prev_cursor']) }}'" class="btn btn-primary" type="buttom">Prev</button>
{{ pagination['total'] }} records
<button onclick="location.href='{{ url_for('listing.show_list', model=model, cursor=pagination['next_cursor']) }}'" class="btn btn-primary" type="buttom">Next</button>
</div>
<nav class="d-flex justify-content-center my-4" aria-label="Pagination">
<ul class="pagination mb-0">
{# Prev #}
<li class="page-item {% if not pagination.has_prev %}disabled{% endif %}">
<a class="page-link"
href="{{ pagination.prev_url if pagination.has_prev else '#' }}"
aria-label="Previous">Previous</a>
</li>
{# Numbered pages with ellipses #}
{% for item in pagination.nav %}
{% if item.type == 'page' %}
<li class="page-item {% if item.active %}active{% endif %}">
<a class="page-link"
href="{{ item.url }}"
{% if item.active %}aria-current="page"{% endif %}>{{ item.n }}</a>
</li>
{% elif item.type == 'ellipsis' %}
<li class="page-item disabled">
<a class="page-link" tabindex="-1" aria-disabled="true"></a>
</li>
{% endif %}
{% endfor %}
{# Next #}
<li class="page-item {% if not pagination.has_next %}disabled{% endif %}">
<a class="page-link"
href="{{ pagination.next_url if pagination.has_next else '#' }}"
aria-label="Next">Next</a>
</li>
</ul>
</nav>
<p class="text-center text-muted small mb-5">
Page {{ pagination.page }} of {{ pagination.pages }} · {{ pagination.total }} records
</p>
{% endblock %}

View file

@ -90,7 +90,9 @@
if (reply.status === 'success') {
if (!hasId && reply.id) {
window.queueToast('Created successfully.', 'success');
window.location.href - `/entry/${model}/${reply.id}`;
window.newDrafts = [];
window.deletedIds = [];
window.location.assign(`/entry/${model}/${reply.id}`);
return;
} else {
window.queueToast('Updated successfully.', 'success');
@ -101,15 +103,12 @@
if (li) li.remove();
}
}
window.newDrafts = [];
window.deletedIds = [];
window.location.replace(window.location.href);
return;
}
window.newDrafts = [];
window.deletedIds = [];
window.location.replace(window.location.href);
return;
} else {
toastMessage(`Unable to save entry: ${reply.error}`, 'danger');
}
} catch (err) {
toastMessage(`Network error: ${String(err)}`, 'danger');