Add per-section pagination to global search
This commit is contained in:
parent
8c29f527c6
commit
8a8f957c9f
@ -2,9 +2,20 @@ from .routes_shared import * # noqa: F401,F403
|
|||||||
from .routes_shared import _format_datetime
|
from .routes_shared import _format_datetime
|
||||||
|
|
||||||
from sqlalchemy import and_, cast, func, or_, String
|
from sqlalchemy import and_, cast, func, or_, String
|
||||||
|
import math
|
||||||
|
|
||||||
|
|
||||||
SEARCH_LIMIT_PER_SECTION = 10
|
SEARCH_LIMIT_PER_SECTION = 10
|
||||||
|
SEARCH_SECTION_KEYS = [
|
||||||
|
"inbox",
|
||||||
|
"customers",
|
||||||
|
"jobs",
|
||||||
|
"daily_jobs",
|
||||||
|
"run_checks",
|
||||||
|
"tickets",
|
||||||
|
"overrides",
|
||||||
|
"reports",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def _is_section_allowed(section: str) -> bool:
|
def _is_section_allowed(section: str) -> bool:
|
||||||
@ -47,13 +58,50 @@ def _contains_all_terms(columns: list, patterns: list[str]):
|
|||||||
return and_(*term_filters)
|
return and_(*term_filters)
|
||||||
|
|
||||||
|
|
||||||
def _build_inbox_results(patterns: list[str]) -> dict:
|
def _parse_page(value: str | None) -> int:
|
||||||
|
try:
|
||||||
|
page = int((value or "").strip())
|
||||||
|
except Exception:
|
||||||
|
page = 1
|
||||||
|
return page if page > 0 else 1
|
||||||
|
|
||||||
|
|
||||||
|
def _paginate_query(query, page: int, order_by_cols: list):
|
||||||
|
total = query.count()
|
||||||
|
total_pages = max(1, math.ceil(total / SEARCH_LIMIT_PER_SECTION)) if total else 1
|
||||||
|
current_page = min(max(page, 1), total_pages)
|
||||||
|
rows = (
|
||||||
|
query.order_by(*order_by_cols)
|
||||||
|
.offset((current_page - 1) * SEARCH_LIMIT_PER_SECTION)
|
||||||
|
.limit(SEARCH_LIMIT_PER_SECTION)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
return total, current_page, total_pages, rows
|
||||||
|
|
||||||
|
|
||||||
|
def _enrich_paging(section: dict, total: int, current_page: int, total_pages: int) -> None:
|
||||||
|
section["total"] = int(total or 0)
|
||||||
|
section["current_page"] = int(current_page or 1)
|
||||||
|
section["total_pages"] = int(total_pages or 1)
|
||||||
|
section["has_prev"] = section["current_page"] > 1
|
||||||
|
section["has_next"] = section["current_page"] < section["total_pages"]
|
||||||
|
section["prev_url"] = ""
|
||||||
|
section["next_url"] = ""
|
||||||
|
|
||||||
|
|
||||||
|
def _build_inbox_results(patterns: list[str], page: int) -> dict:
|
||||||
section = {
|
section = {
|
||||||
"key": "inbox",
|
"key": "inbox",
|
||||||
"title": "Inbox",
|
"title": "Inbox",
|
||||||
"view_all_url": url_for("main.inbox"),
|
"view_all_url": url_for("main.inbox"),
|
||||||
"total": 0,
|
"total": 0,
|
||||||
"items": [],
|
"items": [],
|
||||||
|
"current_page": 1,
|
||||||
|
"total_pages": 1,
|
||||||
|
"has_prev": False,
|
||||||
|
"has_next": False,
|
||||||
|
"prev_url": "",
|
||||||
|
"next_url": "",
|
||||||
}
|
}
|
||||||
if not _is_section_allowed("inbox"):
|
if not _is_section_allowed("inbox"):
|
||||||
return section
|
return section
|
||||||
@ -78,12 +126,12 @@ def _build_inbox_results(patterns: list[str]) -> dict:
|
|||||||
if match_expr is not None:
|
if match_expr is not None:
|
||||||
query = query.filter(match_expr)
|
query = query.filter(match_expr)
|
||||||
|
|
||||||
section["total"] = query.count()
|
total, current_page, total_pages, rows = _paginate_query(
|
||||||
rows = (
|
query,
|
||||||
query.order_by(MailMessage.received_at.desc().nullslast(), MailMessage.id.desc())
|
page,
|
||||||
.limit(SEARCH_LIMIT_PER_SECTION)
|
[MailMessage.received_at.desc().nullslast(), MailMessage.id.desc()],
|
||||||
.all()
|
|
||||||
)
|
)
|
||||||
|
_enrich_paging(section, total, current_page, total_pages)
|
||||||
|
|
||||||
for msg in rows:
|
for msg in rows:
|
||||||
parsed_flag = bool(getattr(msg, "parsed_at", None) or (msg.parse_result or ""))
|
parsed_flag = bool(getattr(msg, "parsed_at", None) or (msg.parse_result or ""))
|
||||||
@ -99,13 +147,19 @@ def _build_inbox_results(patterns: list[str]) -> dict:
|
|||||||
return section
|
return section
|
||||||
|
|
||||||
|
|
||||||
def _build_customers_results(patterns: list[str]) -> dict:
|
def _build_customers_results(patterns: list[str], page: int) -> dict:
|
||||||
section = {
|
section = {
|
||||||
"key": "customers",
|
"key": "customers",
|
||||||
"title": "Customers",
|
"title": "Customers",
|
||||||
"view_all_url": url_for("main.customers"),
|
"view_all_url": url_for("main.customers"),
|
||||||
"total": 0,
|
"total": 0,
|
||||||
"items": [],
|
"items": [],
|
||||||
|
"current_page": 1,
|
||||||
|
"total_pages": 1,
|
||||||
|
"has_prev": False,
|
||||||
|
"has_next": False,
|
||||||
|
"prev_url": "",
|
||||||
|
"next_url": "",
|
||||||
}
|
}
|
||||||
if not _is_section_allowed("customers"):
|
if not _is_section_allowed("customers"):
|
||||||
return section
|
return section
|
||||||
@ -115,8 +169,12 @@ def _build_customers_results(patterns: list[str]) -> dict:
|
|||||||
if match_expr is not None:
|
if match_expr is not None:
|
||||||
query = query.filter(match_expr)
|
query = query.filter(match_expr)
|
||||||
|
|
||||||
section["total"] = query.count()
|
total, current_page, total_pages, rows = _paginate_query(
|
||||||
rows = query.order_by(Customer.name.asc()).limit(SEARCH_LIMIT_PER_SECTION).all()
|
query,
|
||||||
|
page,
|
||||||
|
[Customer.name.asc()],
|
||||||
|
)
|
||||||
|
_enrich_paging(section, total, current_page, total_pages)
|
||||||
for c in rows:
|
for c in rows:
|
||||||
try:
|
try:
|
||||||
job_count = c.jobs.count()
|
job_count = c.jobs.count()
|
||||||
@ -134,13 +192,19 @@ def _build_customers_results(patterns: list[str]) -> dict:
|
|||||||
return section
|
return section
|
||||||
|
|
||||||
|
|
||||||
def _build_jobs_results(patterns: list[str]) -> dict:
|
def _build_jobs_results(patterns: list[str], page: int) -> dict:
|
||||||
section = {
|
section = {
|
||||||
"key": "jobs",
|
"key": "jobs",
|
||||||
"title": "Jobs",
|
"title": "Jobs",
|
||||||
"view_all_url": url_for("main.jobs"),
|
"view_all_url": url_for("main.jobs"),
|
||||||
"total": 0,
|
"total": 0,
|
||||||
"items": [],
|
"items": [],
|
||||||
|
"current_page": 1,
|
||||||
|
"total_pages": 1,
|
||||||
|
"has_prev": False,
|
||||||
|
"has_next": False,
|
||||||
|
"prev_url": "",
|
||||||
|
"next_url": "",
|
||||||
}
|
}
|
||||||
if not _is_section_allowed("jobs"):
|
if not _is_section_allowed("jobs"):
|
||||||
return section
|
return section
|
||||||
@ -171,17 +235,17 @@ def _build_jobs_results(patterns: list[str]) -> dict:
|
|||||||
if match_expr is not None:
|
if match_expr is not None:
|
||||||
query = query.filter(match_expr)
|
query = query.filter(match_expr)
|
||||||
|
|
||||||
section["total"] = query.count()
|
total, current_page, total_pages, rows = _paginate_query(
|
||||||
rows = (
|
query,
|
||||||
query.order_by(
|
page,
|
||||||
|
[
|
||||||
Customer.name.asc().nullslast(),
|
Customer.name.asc().nullslast(),
|
||||||
Job.backup_software.asc(),
|
Job.backup_software.asc(),
|
||||||
Job.backup_type.asc(),
|
Job.backup_type.asc(),
|
||||||
Job.job_name.asc(),
|
Job.job_name.asc(),
|
||||||
)
|
],
|
||||||
.limit(SEARCH_LIMIT_PER_SECTION)
|
|
||||||
.all()
|
|
||||||
)
|
)
|
||||||
|
_enrich_paging(section, total, current_page, total_pages)
|
||||||
for row in rows:
|
for row in rows:
|
||||||
section["items"].append(
|
section["items"].append(
|
||||||
{
|
{
|
||||||
@ -195,13 +259,19 @@ def _build_jobs_results(patterns: list[str]) -> dict:
|
|||||||
return section
|
return section
|
||||||
|
|
||||||
|
|
||||||
def _build_daily_jobs_results(patterns: list[str]) -> dict:
|
def _build_daily_jobs_results(patterns: list[str], page: int) -> dict:
|
||||||
section = {
|
section = {
|
||||||
"key": "daily_jobs",
|
"key": "daily_jobs",
|
||||||
"title": "Daily Jobs",
|
"title": "Daily Jobs",
|
||||||
"view_all_url": url_for("main.daily_jobs"),
|
"view_all_url": url_for("main.daily_jobs"),
|
||||||
"total": 0,
|
"total": 0,
|
||||||
"items": [],
|
"items": [],
|
||||||
|
"current_page": 1,
|
||||||
|
"total_pages": 1,
|
||||||
|
"has_prev": False,
|
||||||
|
"has_next": False,
|
||||||
|
"prev_url": "",
|
||||||
|
"next_url": "",
|
||||||
}
|
}
|
||||||
if not _is_section_allowed("daily_jobs"):
|
if not _is_section_allowed("daily_jobs"):
|
||||||
return section
|
return section
|
||||||
@ -232,17 +302,17 @@ def _build_daily_jobs_results(patterns: list[str]) -> dict:
|
|||||||
if match_expr is not None:
|
if match_expr is not None:
|
||||||
query = query.filter(match_expr)
|
query = query.filter(match_expr)
|
||||||
|
|
||||||
section["total"] = query.count()
|
total, current_page, total_pages, rows = _paginate_query(
|
||||||
rows = (
|
query,
|
||||||
query.order_by(
|
page,
|
||||||
|
[
|
||||||
Customer.name.asc().nullslast(),
|
Customer.name.asc().nullslast(),
|
||||||
Job.backup_software.asc(),
|
Job.backup_software.asc(),
|
||||||
Job.backup_type.asc(),
|
Job.backup_type.asc(),
|
||||||
Job.job_name.asc(),
|
Job.job_name.asc(),
|
||||||
)
|
],
|
||||||
.limit(SEARCH_LIMIT_PER_SECTION)
|
|
||||||
.all()
|
|
||||||
)
|
)
|
||||||
|
_enrich_paging(section, total, current_page, total_pages)
|
||||||
for row in rows:
|
for row in rows:
|
||||||
section["items"].append(
|
section["items"].append(
|
||||||
{
|
{
|
||||||
@ -256,13 +326,19 @@ def _build_daily_jobs_results(patterns: list[str]) -> dict:
|
|||||||
return section
|
return section
|
||||||
|
|
||||||
|
|
||||||
def _build_run_checks_results(patterns: list[str]) -> dict:
|
def _build_run_checks_results(patterns: list[str], page: int) -> dict:
|
||||||
section = {
|
section = {
|
||||||
"key": "run_checks",
|
"key": "run_checks",
|
||||||
"title": "Run Checks",
|
"title": "Run Checks",
|
||||||
"view_all_url": url_for("main.run_checks_page"),
|
"view_all_url": url_for("main.run_checks_page"),
|
||||||
"total": 0,
|
"total": 0,
|
||||||
"items": [],
|
"items": [],
|
||||||
|
"current_page": 1,
|
||||||
|
"total_pages": 1,
|
||||||
|
"has_prev": False,
|
||||||
|
"has_next": False,
|
||||||
|
"prev_url": "",
|
||||||
|
"next_url": "",
|
||||||
}
|
}
|
||||||
if not _is_section_allowed("run_checks"):
|
if not _is_section_allowed("run_checks"):
|
||||||
return section
|
return section
|
||||||
@ -304,17 +380,17 @@ def _build_run_checks_results(patterns: list[str]) -> dict:
|
|||||||
if match_expr is not None:
|
if match_expr is not None:
|
||||||
query = query.filter(match_expr)
|
query = query.filter(match_expr)
|
||||||
|
|
||||||
section["total"] = query.count()
|
total, current_page, total_pages, rows = _paginate_query(
|
||||||
rows = (
|
query,
|
||||||
query.order_by(
|
page,
|
||||||
|
[
|
||||||
Customer.name.asc().nullslast(),
|
Customer.name.asc().nullslast(),
|
||||||
Job.backup_software.asc().nullslast(),
|
Job.backup_software.asc().nullslast(),
|
||||||
Job.backup_type.asc().nullslast(),
|
Job.backup_type.asc().nullslast(),
|
||||||
Job.job_name.asc().nullslast(),
|
Job.job_name.asc().nullslast(),
|
||||||
)
|
],
|
||||||
.limit(SEARCH_LIMIT_PER_SECTION)
|
|
||||||
.all()
|
|
||||||
)
|
)
|
||||||
|
_enrich_paging(section, total, current_page, total_pages)
|
||||||
for row in rows:
|
for row in rows:
|
||||||
section["items"].append(
|
section["items"].append(
|
||||||
{
|
{
|
||||||
@ -328,13 +404,19 @@ def _build_run_checks_results(patterns: list[str]) -> dict:
|
|||||||
return section
|
return section
|
||||||
|
|
||||||
|
|
||||||
def _build_tickets_results(patterns: list[str]) -> dict:
|
def _build_tickets_results(patterns: list[str], page: int) -> dict:
|
||||||
section = {
|
section = {
|
||||||
"key": "tickets",
|
"key": "tickets",
|
||||||
"title": "Tickets",
|
"title": "Tickets",
|
||||||
"view_all_url": url_for("main.tickets_page"),
|
"view_all_url": url_for("main.tickets_page"),
|
||||||
"total": 0,
|
"total": 0,
|
||||||
"items": [],
|
"items": [],
|
||||||
|
"current_page": 1,
|
||||||
|
"total_pages": 1,
|
||||||
|
"has_prev": False,
|
||||||
|
"has_next": False,
|
||||||
|
"prev_url": "",
|
||||||
|
"next_url": "",
|
||||||
}
|
}
|
||||||
if not _is_section_allowed("tickets"):
|
if not _is_section_allowed("tickets"):
|
||||||
return section
|
return section
|
||||||
@ -363,8 +445,12 @@ def _build_tickets_results(patterns: list[str]) -> dict:
|
|||||||
query = query.filter(match_expr)
|
query = query.filter(match_expr)
|
||||||
|
|
||||||
query = query.distinct()
|
query = query.distinct()
|
||||||
section["total"] = query.count()
|
total, current_page, total_pages, rows = _paginate_query(
|
||||||
rows = query.order_by(Ticket.start_date.desc().nullslast()).limit(SEARCH_LIMIT_PER_SECTION).all()
|
query,
|
||||||
|
page,
|
||||||
|
[Ticket.start_date.desc().nullslast()],
|
||||||
|
)
|
||||||
|
_enrich_paging(section, total, current_page, total_pages)
|
||||||
|
|
||||||
for t in rows:
|
for t in rows:
|
||||||
customer_display = "-"
|
customer_display = "-"
|
||||||
@ -418,13 +504,19 @@ def _build_tickets_results(patterns: list[str]) -> dict:
|
|||||||
return section
|
return section
|
||||||
|
|
||||||
|
|
||||||
def _build_overrides_results(patterns: list[str]) -> dict:
|
def _build_overrides_results(patterns: list[str], page: int) -> dict:
|
||||||
section = {
|
section = {
|
||||||
"key": "overrides",
|
"key": "overrides",
|
||||||
"title": "Existing overrides",
|
"title": "Existing overrides",
|
||||||
"view_all_url": url_for("main.overrides"),
|
"view_all_url": url_for("main.overrides"),
|
||||||
"total": 0,
|
"total": 0,
|
||||||
"items": [],
|
"items": [],
|
||||||
|
"current_page": 1,
|
||||||
|
"total_pages": 1,
|
||||||
|
"has_prev": False,
|
||||||
|
"has_next": False,
|
||||||
|
"prev_url": "",
|
||||||
|
"next_url": "",
|
||||||
}
|
}
|
||||||
if not _is_section_allowed("overrides"):
|
if not _is_section_allowed("overrides"):
|
||||||
return section
|
return section
|
||||||
@ -464,8 +556,12 @@ def _build_overrides_results(patterns: list[str]) -> dict:
|
|||||||
if match_expr is not None:
|
if match_expr is not None:
|
||||||
query = query.filter(match_expr)
|
query = query.filter(match_expr)
|
||||||
|
|
||||||
section["total"] = query.count()
|
total, current_page, total_pages, rows = _paginate_query(
|
||||||
rows = query.order_by(Override.level.asc(), Override.start_at.desc()).limit(SEARCH_LIMIT_PER_SECTION).all()
|
query,
|
||||||
|
page,
|
||||||
|
[Override.level.asc(), Override.start_at.desc()],
|
||||||
|
)
|
||||||
|
_enrich_paging(section, total, current_page, total_pages)
|
||||||
for row in rows:
|
for row in rows:
|
||||||
scope_bits = []
|
scope_bits = []
|
||||||
if row.customer_name:
|
if row.customer_name:
|
||||||
@ -492,13 +588,19 @@ def _build_overrides_results(patterns: list[str]) -> dict:
|
|||||||
return section
|
return section
|
||||||
|
|
||||||
|
|
||||||
def _build_reports_results(patterns: list[str]) -> dict:
|
def _build_reports_results(patterns: list[str], page: int) -> dict:
|
||||||
section = {
|
section = {
|
||||||
"key": "reports",
|
"key": "reports",
|
||||||
"title": "Reports",
|
"title": "Reports",
|
||||||
"view_all_url": url_for("main.reports"),
|
"view_all_url": url_for("main.reports"),
|
||||||
"total": 0,
|
"total": 0,
|
||||||
"items": [],
|
"items": [],
|
||||||
|
"current_page": 1,
|
||||||
|
"total_pages": 1,
|
||||||
|
"has_prev": False,
|
||||||
|
"has_next": False,
|
||||||
|
"prev_url": "",
|
||||||
|
"next_url": "",
|
||||||
}
|
}
|
||||||
if not _is_section_allowed("reports"):
|
if not _is_section_allowed("reports"):
|
||||||
return section
|
return section
|
||||||
@ -517,8 +619,12 @@ def _build_reports_results(patterns: list[str]) -> dict:
|
|||||||
if match_expr is not None:
|
if match_expr is not None:
|
||||||
query = query.filter(match_expr)
|
query = query.filter(match_expr)
|
||||||
|
|
||||||
section["total"] = query.count()
|
total, current_page, total_pages, rows = _paginate_query(
|
||||||
rows = query.order_by(ReportDefinition.created_at.desc()).limit(SEARCH_LIMIT_PER_SECTION).all()
|
query,
|
||||||
|
page,
|
||||||
|
[ReportDefinition.created_at.desc()],
|
||||||
|
)
|
||||||
|
_enrich_paging(section, total, current_page, total_pages)
|
||||||
|
|
||||||
can_edit = get_active_role() in ("admin", "operator", "reporter")
|
can_edit = get_active_role() in ("admin", "operator", "reporter")
|
||||||
for r in rows:
|
for r in rows:
|
||||||
@ -540,29 +646,57 @@ def search_page():
|
|||||||
query = (request.args.get("q") or "").strip()
|
query = (request.args.get("q") or "").strip()
|
||||||
patterns = _build_patterns(query)
|
patterns = _build_patterns(query)
|
||||||
|
|
||||||
|
requested_pages = {
|
||||||
|
key: _parse_page(request.args.get(f"p_{key}"))
|
||||||
|
for key in SEARCH_SECTION_KEYS
|
||||||
|
}
|
||||||
|
|
||||||
sections = []
|
sections = []
|
||||||
if patterns:
|
if patterns:
|
||||||
sections.append(_build_inbox_results(patterns))
|
sections.append(_build_inbox_results(patterns, requested_pages["inbox"]))
|
||||||
sections.append(_build_customers_results(patterns))
|
sections.append(_build_customers_results(patterns, requested_pages["customers"]))
|
||||||
sections.append(_build_jobs_results(patterns))
|
sections.append(_build_jobs_results(patterns, requested_pages["jobs"]))
|
||||||
sections.append(_build_daily_jobs_results(patterns))
|
sections.append(_build_daily_jobs_results(patterns, requested_pages["daily_jobs"]))
|
||||||
sections.append(_build_run_checks_results(patterns))
|
sections.append(_build_run_checks_results(patterns, requested_pages["run_checks"]))
|
||||||
sections.append(_build_tickets_results(patterns))
|
sections.append(_build_tickets_results(patterns, requested_pages["tickets"]))
|
||||||
sections.append(_build_overrides_results(patterns))
|
sections.append(_build_overrides_results(patterns, requested_pages["overrides"]))
|
||||||
sections.append(_build_reports_results(patterns))
|
sections.append(_build_reports_results(patterns, requested_pages["reports"]))
|
||||||
else:
|
else:
|
||||||
sections = [
|
sections = [
|
||||||
{"key": "inbox", "title": "Inbox", "view_all_url": url_for("main.inbox"), "total": 0, "items": []},
|
{"key": "inbox", "title": "Inbox", "view_all_url": url_for("main.inbox"), "total": 0, "items": [], "current_page": 1, "total_pages": 1, "has_prev": False, "has_next": False, "prev_url": "", "next_url": ""},
|
||||||
{"key": "customers", "title": "Customers", "view_all_url": url_for("main.customers"), "total": 0, "items": []},
|
{"key": "customers", "title": "Customers", "view_all_url": url_for("main.customers"), "total": 0, "items": [], "current_page": 1, "total_pages": 1, "has_prev": False, "has_next": False, "prev_url": "", "next_url": ""},
|
||||||
{"key": "jobs", "title": "Jobs", "view_all_url": url_for("main.jobs"), "total": 0, "items": []},
|
{"key": "jobs", "title": "Jobs", "view_all_url": url_for("main.jobs"), "total": 0, "items": [], "current_page": 1, "total_pages": 1, "has_prev": False, "has_next": False, "prev_url": "", "next_url": ""},
|
||||||
{"key": "daily_jobs", "title": "Daily Jobs", "view_all_url": url_for("main.daily_jobs"), "total": 0, "items": []},
|
{"key": "daily_jobs", "title": "Daily Jobs", "view_all_url": url_for("main.daily_jobs"), "total": 0, "items": [], "current_page": 1, "total_pages": 1, "has_prev": False, "has_next": False, "prev_url": "", "next_url": ""},
|
||||||
{"key": "run_checks", "title": "Run Checks", "view_all_url": url_for("main.run_checks_page"), "total": 0, "items": []},
|
{"key": "run_checks", "title": "Run Checks", "view_all_url": url_for("main.run_checks_page"), "total": 0, "items": [], "current_page": 1, "total_pages": 1, "has_prev": False, "has_next": False, "prev_url": "", "next_url": ""},
|
||||||
{"key": "tickets", "title": "Tickets", "view_all_url": url_for("main.tickets_page"), "total": 0, "items": []},
|
{"key": "tickets", "title": "Tickets", "view_all_url": url_for("main.tickets_page"), "total": 0, "items": [], "current_page": 1, "total_pages": 1, "has_prev": False, "has_next": False, "prev_url": "", "next_url": ""},
|
||||||
{"key": "overrides", "title": "Existing overrides", "view_all_url": url_for("main.overrides"), "total": 0, "items": []},
|
{"key": "overrides", "title": "Existing overrides", "view_all_url": url_for("main.overrides"), "total": 0, "items": [], "current_page": 1, "total_pages": 1, "has_prev": False, "has_next": False, "prev_url": "", "next_url": ""},
|
||||||
{"key": "reports", "title": "Reports", "view_all_url": url_for("main.reports"), "total": 0, "items": []},
|
{"key": "reports", "title": "Reports", "view_all_url": url_for("main.reports"), "total": 0, "items": [], "current_page": 1, "total_pages": 1, "has_prev": False, "has_next": False, "prev_url": "", "next_url": ""},
|
||||||
]
|
]
|
||||||
|
|
||||||
visible_sections = [s for s in sections if _is_section_allowed(s["key"])]
|
visible_sections = [s for s in sections if _is_section_allowed(s["key"])]
|
||||||
|
current_pages = {
|
||||||
|
s["key"]: int(s.get("current_page", 1) or 1)
|
||||||
|
for s in sections
|
||||||
|
}
|
||||||
|
|
||||||
|
def _build_search_url(page_overrides: dict[str, int]) -> str:
|
||||||
|
args = {"q": query}
|
||||||
|
for key in SEARCH_SECTION_KEYS:
|
||||||
|
args[f"p_{key}"] = int(page_overrides.get(key, current_pages.get(key, 1)))
|
||||||
|
return url_for("main.search_page", **args)
|
||||||
|
|
||||||
|
for s in visible_sections:
|
||||||
|
key = s["key"]
|
||||||
|
cur = int(s.get("current_page", 1) or 1)
|
||||||
|
if s.get("has_prev"):
|
||||||
|
prev_pages = dict(current_pages)
|
||||||
|
prev_pages[key] = cur - 1
|
||||||
|
s["prev_url"] = _build_search_url(prev_pages)
|
||||||
|
if s.get("has_next"):
|
||||||
|
next_pages = dict(current_pages)
|
||||||
|
next_pages[key] = cur + 1
|
||||||
|
s["next_url"] = _build_search_url(next_pages)
|
||||||
|
|
||||||
total_hits = sum(int(s.get("total", 0) or 0) for s in visible_sections)
|
total_hits = sum(int(s.get("total", 0) or 0) for s in visible_sections)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
|
|||||||
@ -15,8 +15,8 @@
|
|||||||
{% for section in sections %}
|
{% for section in sections %}
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<span>{{ section.title }} ({{ section.total }})</span>
|
<span>{{ section['title'] }} ({{ section['total'] }})</span>
|
||||||
<a href="{{ section.view_all_url }}" class="btn btn-sm btn-outline-secondary">Open {{ section.title }}</a>
|
<a href="{{ section['view_all_url'] }}" class="btn btn-sm btn-outline-secondary">Open {{ section['title'] }}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
{% if section['items'] %}
|
{% if section['items'] %}
|
||||||
@ -50,9 +50,19 @@
|
|||||||
<div class="p-3 text-muted">No results in this section.</div>
|
<div class="p-3 text-muted">No results in this section.</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if section.total > limit_per_section %}
|
{% if section['total_pages'] > 1 %}
|
||||||
<div class="card-footer small text-muted">
|
<div class="card-footer d-flex justify-content-between align-items-center small">
|
||||||
Showing first {{ limit_per_section }} of {{ section.total }} results.
|
<span class="text-muted">
|
||||||
|
Page {{ section['current_page'] }} of {{ section['total_pages'] }} ({{ section['total'] }} results)
|
||||||
|
</span>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
{% if section['has_prev'] %}
|
||||||
|
<a class="btn btn-sm btn-outline-secondary" href="{{ section['prev_url'] }}">Previous</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if section['has_next'] %}
|
||||||
|
<a class="btn btn-sm btn-outline-secondary" href="{{ section['next_url'] }}">Next</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -17,6 +17,7 @@ This file documents all changes made to this project via Claude Code.
|
|||||||
- Changed search matching to be case-insensitive with wildcard support (`*`) and automatic contains behavior (`*term*`) per search term
|
- Changed search matching to be case-insensitive with wildcard support (`*`) and automatic contains behavior (`*term*`) per search term
|
||||||
- Changed global search visibility to only include sections accessible to the currently active role
|
- Changed global search visibility to only include sections accessible to the currently active role
|
||||||
- Changed `docs/technical-notes-codex.md` with a dedicated Global Grouped Search section (route/UI/behavior/access rules) and latest test build digest for `v20260216-02-global-search`
|
- Changed `docs/technical-notes-codex.md` with a dedicated Global Grouped Search section (route/UI/behavior/access rules) and latest test build digest for `v20260216-02-global-search`
|
||||||
|
- Changed global search to support per-section pagination (previous/next) so results beyond the first 10 can be browsed per section while preserving the current query/state
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Fixed `/search` page crash (`TypeError: 'builtin_function_or_method' object is not iterable`) by replacing Jinja dict access from `section.items` to `section['items']` in `templates/main/search.html`
|
- Fixed `/search` page crash (`TypeError: 'builtin_function_or_method' object is not iterable`) by replacing Jinja dict access from `section.items` to `section['items']` in `templates/main/search.html`
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user