2026-10-18 15:08:34 -06:00
|
|
|
"""The find bar's search and filter terms, shared by every list: the boards, GET /api/packets and the PM reports.
|
|
|
|
|
|
|
|
|
|
A port of the old filters.js (FLT_STATUS, FLT_FLAGS, FLT_FLAGKIND, fltHay, mineFilter, fltFn, FLT_SORT), applied by
|
|
|
|
|
the server so a board never fetches more than it shows. Every term is SQL over CARD_SQL's aliases: p (packets),
|
|
|
|
|
u (units), pr (projects), d (departments), st (stations) and wu (the assigned welder). Request text only ever goes
|
|
|
|
|
in as a parameter.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import uuid
|
|
|
|
|
from collections.abc import Iterable, Mapping
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
from datetime import date, timedelta
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
from .. import policy
|
|
|
|
|
from ..auth.groups import SCREENS
|
|
|
|
|
from ..text import clean_text
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------- vocabulary
|
|
|
|
|
|
|
|
|
|
OPEN_STATUSES = ("draft", "warehouse", "ready", "assigned", "in_progress", "review", "qc", "complete")
|
|
|
|
|
|
|
|
|
|
# The find bar's status words. A raw status (or a comma list of them) keeps working.
|
|
|
|
|
FLT_STATUS: dict[str, tuple[str, ...]] = {
|
|
|
|
|
"draft": ("draft",),
|
|
|
|
|
"warehouse": ("warehouse",),
|
|
|
|
|
"ready": ("ready",),
|
|
|
|
|
"shop": ("ready", "assigned", "in_progress", "review", "qc"),
|
|
|
|
|
"station": ("assigned", "in_progress"),
|
|
|
|
|
"review": ("review",),
|
|
|
|
|
"qc": ("qc",),
|
|
|
|
|
"complete": ("complete",),
|
|
|
|
|
"filed": ("filed",),
|
|
|
|
|
"open": OPEN_STATUSES,
|
|
|
|
|
}
|
|
|
|
|
FLT_STATUS_LABELS: dict[str, str] = {
|
|
|
|
|
"draft": "Ready to release",
|
|
|
|
|
"warehouse": "With warehouse (any stage)",
|
|
|
|
|
"ready": "Ready to assign",
|
|
|
|
|
"shop": "In the shop (ready to Q.C.)",
|
|
|
|
|
"station": "At a station (assigned / working)",
|
|
|
|
|
"review": "Supervisor review",
|
|
|
|
|
"qc": "At Q.C.",
|
|
|
|
|
"complete": "Accepted / routed",
|
|
|
|
|
"filed": "Filed",
|
|
|
|
|
"open": "Everything not filed",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FLT_FLAGS = ("co", "short", "overdue", "week", "mine")
|
|
|
|
|
FLT_FLAG_LABELS: dict[str, str] = {
|
|
|
|
|
"co": "Change order open",
|
|
|
|
|
"short": "Shorts open",
|
|
|
|
|
"overdue": "Project overdue",
|
|
|
|
|
"week": "Due within 7 days",
|
|
|
|
|
"mine": "Mine",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# The Flag select (drafts only): value -> rule name.
|
|
|
|
|
FLT_FLAGKIND: dict[str, str] = {
|
|
|
|
|
"any": "flagged",
|
|
|
|
|
"rev": "rev_mismatch",
|
|
|
|
|
"unconf": "rev_unconfirmed",
|
|
|
|
|
"hand": "hand",
|
|
|
|
|
"none": "not_released",
|
|
|
|
|
"vault": "not_found",
|
|
|
|
|
"read": "unreadable",
|
|
|
|
|
"wait": "drawing_needed",
|
|
|
|
|
"no": "not_flagged",
|
|
|
|
|
}
|
|
|
|
|
FLT_FLAGKIND_LABELS: dict[str, str] = {
|
|
|
|
|
"any": "Flagged (any reason)",
|
|
|
|
|
"rev": "Rev mismatch with M-Files",
|
|
|
|
|
"unconf": "Rev not confirmed yet",
|
|
|
|
|
"hand": "Made or uploaded by hand",
|
|
|
|
|
"none": "Still in drafting",
|
|
|
|
|
"vault": "Drawing not found in M-Files",
|
|
|
|
|
"read": "Drawing couldn't be read",
|
|
|
|
|
"wait": "Drawing being looked up",
|
|
|
|
|
"no": "Not flagged",
|
|
|
|
|
}
|
|
|
|
|
# The old raw values (phase 2 to 7) that keep working.
|
|
|
|
|
FLAG_ON = ("1", "true", "yes", "flagged")
|
|
|
|
|
FLAG_OFF = ("0", "false")
|
|
|
|
|
|
|
|
|
|
# The status texts of the frontend's lib/packets.ts STATUS_LABEL (search matches these; the PM history groups by
|
|
|
|
|
# them).
|
|
|
|
|
STATUS_LABEL: dict[str, str] = {
|
|
|
|
|
"draft": "Ready to release",
|
|
|
|
|
"warehouse": "With warehouse",
|
|
|
|
|
"ready": "Ready to assign",
|
|
|
|
|
"assigned": "Sent to station",
|
|
|
|
|
"in_progress": "Working",
|
|
|
|
|
"review": "Supervisor review",
|
|
|
|
|
"qc": "Q.C. inspection",
|
|
|
|
|
"complete": "Routed",
|
|
|
|
|
"filed": "Filed in data book",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TIE = "p.updated_at DESC, p.id DESC"
|
|
|
|
|
SORTS: dict[str, str] = {
|
|
|
|
|
"updated": TIE,
|
|
|
|
|
"project": 'pr.number COLLATE "C", u.lot COLLATE "C", p.op COLLATE "C", p.id',
|
|
|
|
|
"due": f"coalesce(u.due, pr.due) ASC NULLS LAST, {TIE}",
|
|
|
|
|
"status": f"p.status, {TIE}",
|
|
|
|
|
"dept": f"p.dept_code, {TIE}",
|
|
|
|
|
}
|
|
|
|
|
SORT_LABELS: dict[str, str] = {
|
|
|
|
|
"updated": "Last updated",
|
|
|
|
|
"project": "Project / lot",
|
|
|
|
|
"due": "Due date",
|
|
|
|
|
"status": "Status",
|
|
|
|
|
"dept": "Department",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
WORDS_MAX = 8
|
|
|
|
|
WORD_MAX = 60
|
|
|
|
|
# Words that match everything, so "lot 52" and "rev B" work as the old hay text did.
|
|
|
|
|
WORDS_ALWAYS = ("lot", "rev")
|
|
|
|
|
MINE_SCREENS = ("clerical", "warehouse", "supervisor", "stall", "qc", "pm", "admin")
|
|
|
|
|
LEAD = 2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def like(word: str) -> str:
|
|
|
|
|
"""An ILIKE pattern for a word anywhere, with %, _ and the backslash escaped."""
|
|
|
|
|
return "%" + word.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") + "%"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------- status
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def status_terms(value: str) -> list[str]:
|
|
|
|
|
"""The raw statuses a status value stands for: a find bar word expands, a raw status stays, in a comma list."""
|
|
|
|
|
out: list[str] = []
|
|
|
|
|
for part in str(value or "").split(","):
|
|
|
|
|
s = part.strip().lower()
|
|
|
|
|
if not s:
|
|
|
|
|
continue
|
|
|
|
|
for st in FLT_STATUS.get(s, (s,)):
|
|
|
|
|
if st not in out:
|
|
|
|
|
out.append(st)
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------- search
|
|
|
|
|
|
|
|
|
|
_STATUS_CASE = (
|
|
|
|
|
"(CASE p.status::text "
|
|
|
|
|
+ " ".join(f"WHEN '{k}' THEN '{v}'" for k, v in STATUS_LABEL.items())
|
|
|
|
|
+ " ELSE p.status::text END)"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# One fragment per field, each with exactly one %s (the word's pattern).
|
|
|
|
|
_FIELDS: tuple[str, ...] = (
|
|
|
|
|
"pr.number ILIKE %s",
|
|
|
|
|
"pr.name ILIKE %s",
|
|
|
|
|
"(pr.number || u.lot) ILIKE %s",
|
|
|
|
|
"(pr.number || ltrim(u.lot, '0')) ILIKE %s",
|
|
|
|
|
"u.lot ILIKE %s",
|
|
|
|
|
"ltrim(u.lot, '0') ILIKE %s",
|
|
|
|
|
"u.name ILIKE %s",
|
|
|
|
|
"p.op ILIKE %s",
|
|
|
|
|
"p.title ILIKE %s",
|
|
|
|
|
"p.drawing ILIKE %s",
|
|
|
|
|
"p.rev ILIKE %s",
|
|
|
|
|
"p.wps ILIKE %s",
|
|
|
|
|
"p.customer ILIKE %s",
|
|
|
|
|
"pr.customer ILIKE %s",
|
|
|
|
|
"pr.po ILIKE %s",
|
|
|
|
|
"d.name ILIKE %s",
|
|
|
|
|
"st.name ILIKE %s",
|
|
|
|
|
"wu.display_name ILIKE %s",
|
|
|
|
|
"p.welder_stamp ILIKE %s",
|
|
|
|
|
"wu.welder_stamp ILIKE %s",
|
|
|
|
|
_STATUS_CASE + " ILIKE %s",
|
|
|
|
|
"array_to_string(p.flags, ' ') ILIKE %s",
|
|
|
|
|
"EXISTS (SELECT 1 FROM project_aliases pa WHERE pa.project_id = pr.id"
|
|
|
|
|
" AND (pa.unit_id IS NULL OR pa.unit_id = u.id) AND pa.alias ILIKE %s)",
|
|
|
|
|
"EXISTS (SELECT 1 FROM packet_work_orders wo WHERE wo.packet_id = p.id"
|
|
|
|
|
" AND (wo.wo_base || '/' || wo.lot || '/' || wo.split || '/' || wo.sub) ILIKE %s)",
|
|
|
|
|
"EXISTS (SELECT 1 FROM bom_lines b WHERE b.packet_id = p.id"
|
|
|
|
|
" AND concat_ws(' ', b.heat, b.part_id, b.description, b.material) ILIKE %s)",
|
|
|
|
|
"EXISTS (SELECT 1 FROM change_orders c WHERE c.packet_id = p.id"
|
|
|
|
|
" AND concat_ws(' ', 'CO-' || c.co_no, c.ecn) ILIKE %s)",
|
|
|
|
|
"EXISTS (SELECT 1 FROM welds w LEFT JOIN users ww ON ww.id = w.welder_id WHERE w.packet_id = p.id"
|
|
|
|
|
" AND w.removed_at IS NULL AND concat_ws(' ', w.stamp, ww.display_name) ILIKE %s)",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def search_words(q: str) -> list[str]:
|
|
|
|
|
"""The words of a search: at most 8, each cut at 60 characters, with "lot" and "rev" left out."""
|
|
|
|
|
words = [w[:WORD_MAX] for w in clean_text(q or "", 1000).split()[:WORDS_MAX]]
|
|
|
|
|
return [w for w in words if w.lower() not in WORDS_ALWAYS]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def search_terms(q: str) -> tuple[list[str], list[Any]]:
|
|
|
|
|
"""One "(...)" term per word; every word must match some field. Child matches are EXISTS terms, so a packet
|
|
|
|
|
is never listed twice."""
|
|
|
|
|
from .packets import FLAG_SQL
|
|
|
|
|
|
|
|
|
|
where: list[str] = []
|
|
|
|
|
params: list[Any] = []
|
|
|
|
|
for word in search_words(q):
|
|
|
|
|
pat = like(word)
|
|
|
|
|
parts = list(_FIELDS)
|
|
|
|
|
# The word "flagged" (or a part of it) finds every flagged draft, as the old hay text did.
|
|
|
|
|
parts.append(f"({FLAG_SQL} AND 'flagged' ILIKE %s)")
|
|
|
|
|
where.append("(" + " OR ".join(parts) + ")")
|
|
|
|
|
params += [pat] * len(parts)
|
|
|
|
|
return where, params
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------- Mine
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class MineScope:
|
|
|
|
|
screen: str
|
|
|
|
|
principal: policy.Principal
|
|
|
|
|
station_id: int | None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def screens_of(pp: policy.Principal) -> list[str]:
|
|
|
|
|
"""The screens a policy principal may open, in SCREENS order (admin: every screen)."""
|
|
|
|
|
if pp.admin:
|
|
|
|
|
return list(SCREENS)
|
|
|
|
|
have: set[str] = set()
|
|
|
|
|
if "CL" in pp.support:
|
|
|
|
|
have.add("clerical")
|
|
|
|
|
if "WH" in pp.support:
|
|
|
|
|
have.add("warehouse")
|
|
|
|
|
if "QC" in pp.support:
|
|
|
|
|
have.add("qc")
|
|
|
|
|
if pp.pm:
|
|
|
|
|
have.add("pm")
|
|
|
|
|
for lvl in pp.shop.values():
|
|
|
|
|
have.add("stall")
|
|
|
|
|
if int(lvl) >= LEAD:
|
|
|
|
|
have.add("supervisor")
|
|
|
|
|
return [s for s in SCREENS if s in have]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def mine_scope(pp: policy.Principal, screen: str | None, station_id: int | None) -> MineScope:
|
|
|
|
|
"""The screen Mine is decided for: the one the caller names, else the first screen the roles give."""
|
|
|
|
|
s = str(screen or "").strip().lower()
|
|
|
|
|
if s not in MINE_SCREENS:
|
|
|
|
|
found = screens_of(pp)
|
|
|
|
|
s = found[0] if found else "pm"
|
|
|
|
|
return MineScope(screen=s, principal=pp, station_id=station_id)
|
|
|
|
|
|
|
|
|
|
|
Build Seth's changes C1 to C11 (Gitea issues 1, 8, 18, 23, 24, 25, 27, 31, 32, 33, 36)
2026-10-30 13:33:51 -06:00
|
|
|
# The statuses a packet shows on the Stall screen: assigned, in progress and submitted for review.
|
|
|
|
|
STALL_STATUSES = ("assigned", "in_progress", "review")
|
|
|
|
|
|
|
|
|
|
|
2026-10-18 15:08:34 -06:00
|
|
|
def mine_term(m: MineScope) -> tuple[str | None, list[Any]]:
|
|
|
|
|
"""The Mine rule (the old mineFilter) for the screen, or None when it is everything (PM, admin)."""
|
|
|
|
|
pp = m.principal
|
|
|
|
|
if m.screen == "clerical":
|
|
|
|
|
return "(p.status = 'draft' OR (p.status = 'complete' AND p.routed_to = 'databook'))", []
|
|
|
|
|
if m.screen == "warehouse":
|
|
|
|
|
return (
|
|
|
|
|
"(p.status = 'warehouse' OR EXISTS (SELECT 1 FROM bom_shorts ms WHERE ms.packet_id = p.id"
|
|
|
|
|
" AND ms.closed_at IS NULL))",
|
|
|
|
|
[],
|
|
|
|
|
)
|
|
|
|
|
if m.screen == "qc":
|
|
|
|
|
return "p.status = 'qc'", []
|
|
|
|
|
if m.screen == "supervisor":
|
|
|
|
|
depts = sorted(code for code, lvl in pp.shop.items() if int(lvl) >= LEAD)
|
|
|
|
|
if not depts:
|
|
|
|
|
return (None, []) if pp.admin else ("false", [])
|
|
|
|
|
return "p.dept_code = ANY(%s)", [depts]
|
|
|
|
|
if m.screen == "stall":
|
Build Seth's changes C1 to C11 (Gitea issues 1, 8, 18, 23, 24, 25, 27, 31, 32, 33, 36)
2026-10-30 13:33:51 -06:00
|
|
|
# Change C11 (Gitea issue 36): the packets assigned to me, at whatever station the supervisor picked. No
|
|
|
|
|
# picked station is needed; `station_id` is ignored here.
|
|
|
|
|
if not pp.user_id:
|
2026-10-18 15:08:34 -06:00
|
|
|
return "false", []
|
Build Seth's changes C1 to C11 (Gitea issues 1, 8, 18, 23, 24, 25, 27, 31, 32, 33, 36)
2026-10-30 13:33:51 -06:00
|
|
|
return "(p.welder_id = %s::uuid AND p.status::text = ANY(%s))", [str(pp.user_id), list(STALL_STATUSES)]
|
2026-10-18 15:08:34 -06:00
|
|
|
return None, []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------- quick flags and flag kinds
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def flag_terms(flags: Iterable[str], *, today: date, mine: MineScope | None) -> tuple[list[str], list[Any]]:
|
|
|
|
|
"""The quick flag chips: every chip on must hold."""
|
|
|
|
|
where: list[str] = []
|
|
|
|
|
params: list[Any] = []
|
|
|
|
|
for f in flags:
|
|
|
|
|
if f == "co":
|
|
|
|
|
where.append("EXISTS (SELECT 1 FROM v_open_cos fc WHERE fc.packet_id = p.id)")
|
|
|
|
|
elif f == "short":
|
|
|
|
|
where.append("EXISTS (SELECT 1 FROM bom_shorts fs WHERE fs.packet_id = p.id AND fs.closed_at IS NULL)")
|
|
|
|
|
elif f == "overdue":
|
|
|
|
|
where.append("coalesce(u.due, pr.due) < %s")
|
|
|
|
|
params.append(today)
|
|
|
|
|
elif f == "week":
|
|
|
|
|
where.append("coalesce(u.due, pr.due) <= %s")
|
|
|
|
|
params.append(today + timedelta(days=7))
|
|
|
|
|
elif f == "mine" and mine is not None:
|
|
|
|
|
term, p = mine_term(mine)
|
|
|
|
|
if term is not None:
|
|
|
|
|
where.append(term)
|
|
|
|
|
params += p
|
|
|
|
|
return where, params
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _kind_term(kinds: list[str]) -> tuple[str, list[Any]]:
|
|
|
|
|
"""Drafts carrying one of the flag kinds. A visual draft whose flags are empty but whose drawing state still
|
|
|
|
|
names the problem counts too, the same way flag_of reads it."""
|
|
|
|
|
from .packets import STATE_KINDS
|
|
|
|
|
|
|
|
|
|
states = [st for st, k in STATE_KINDS.items() if k in kinds]
|
|
|
|
|
return (
|
|
|
|
|
"(p.status = 'draft' AND (p.flags && %s::text[] OR (cardinality(p.flags) = 0 AND p.source_kind = 'visual'"
|
|
|
|
|
" AND p.drawing_state::text = ANY(%s))))",
|
|
|
|
|
[kinds, states],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def flagkind_term(value: str) -> tuple[str, list[Any]] | None:
|
|
|
|
|
"""The Flag select's term, or None for no filter. The old raw values keep working."""
|
|
|
|
|
from .packets import FLAG_SQL, HAND_UPLOAD_FLAGS, RX_FLAG_KIND
|
|
|
|
|
|
|
|
|
|
v = str(value or "").strip().lower()
|
|
|
|
|
if not v or v == "all":
|
|
|
|
|
return None
|
|
|
|
|
if v == "any" or v in FLAG_ON:
|
|
|
|
|
return FLAG_SQL, []
|
|
|
|
|
if v == "no" or v in FLAG_OFF:
|
|
|
|
|
return "NOT " + FLAG_SQL, []
|
|
|
|
|
if v == "hand":
|
|
|
|
|
return "(p.status = 'draft' AND (p.source_kind = 'hand' OR p.flags && %s::text[]))", [list(HAND_UPLOAD_FLAGS)]
|
|
|
|
|
if v in ("hand_upload", "hand_uploads"):
|
|
|
|
|
return "(p.status = 'draft' AND p.flags && %s::text[])", [list(HAND_UPLOAD_FLAGS)]
|
|
|
|
|
if v == "unconf":
|
|
|
|
|
# The drawing is attached but the drawing job has not confirmed the rev with M-Files yet (the read model
|
|
|
|
|
# keeps that in visual->>'rev_confirmed').
|
|
|
|
|
return (
|
|
|
|
|
"(p.status = 'draft' AND p.source_kind = 'visual' AND p.drawing_state = 'attached'"
|
|
|
|
|
" AND (p.visual->>'rev_confirmed') IS DISTINCT FROM 'true')",
|
|
|
|
|
[],
|
|
|
|
|
)
|
|
|
|
|
if v == "rev":
|
|
|
|
|
return _kind_term(["rev_mismatch"])
|
|
|
|
|
if v == "none":
|
|
|
|
|
return _kind_term(["not_released"])
|
|
|
|
|
if v == "vault":
|
|
|
|
|
return _kind_term(["not_found"])
|
|
|
|
|
if v == "read":
|
|
|
|
|
return _kind_term(["lookup_failed", "bad_rev"])
|
|
|
|
|
if v == "wait":
|
|
|
|
|
return _kind_term(["drawing_needed"])
|
|
|
|
|
if RX_FLAG_KIND.match(v):
|
|
|
|
|
return _kind_term([v])
|
|
|
|
|
return "false", []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------- sort and facets
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def order_by(sort: str | None) -> str:
|
|
|
|
|
return SORTS.get(str(sort or "").strip().lower(), TIE)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# {scope} is fixed terms from _scope, never request text.
|
|
|
|
|
FACET_STATIONS_SQL = """
|
|
|
|
|
SELECT DISTINCT st.id, st.name, st.dept_code FROM packets p JOIN units u ON u.id = p.unit_id
|
|
|
|
|
JOIN stations st ON st.id = p.station_id WHERE p.removed_at IS NULL{scope} ORDER BY st.name, st.id
|
|
|
|
|
"""
|
|
|
|
|
FACET_WORKERS_SQL = """
|
|
|
|
|
SELECT DISTINCT wu.id, wu.display_name FROM packets p JOIN units u ON u.id = p.unit_id
|
2026-10-21 18:27:09 -06:00
|
|
|
JOIN users wu ON wu.id = p.welder_id AND wu.profile_active
|
|
|
|
|
WHERE p.removed_at IS NULL{scope} ORDER BY wu.display_name, wu.id
|
2026-10-18 15:08:34 -06:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _scope(project_id: int | None, unit_id: int | None, ids: list[int] | None) -> tuple[str, list[Any]]:
|
|
|
|
|
terms = ""
|
|
|
|
|
params: list[Any] = []
|
|
|
|
|
if project_id is not None:
|
|
|
|
|
terms += " AND u.project_id = %s"
|
|
|
|
|
params.append(int(project_id))
|
|
|
|
|
if unit_id is not None:
|
|
|
|
|
terms += " AND p.unit_id = %s"
|
|
|
|
|
params.append(int(unit_id))
|
|
|
|
|
if ids is not None:
|
|
|
|
|
terms += " AND p.id = ANY(%s)"
|
|
|
|
|
params.append([int(i) for i in ids])
|
|
|
|
|
return terms, params
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def facets(
|
|
|
|
|
conn: Any, *, project_id: int | None = None, unit_id: int | None = None, ids: Iterable[int] | None = None
|
|
|
|
|
) -> dict[str, list[dict[str, Any]]]:
|
|
|
|
|
"""The distinct stations and assigned welders among the packets in the project and unit scope (and, for a
|
|
|
|
|
board, among the board's own packets), ordered by name. The other filters do not narrow them."""
|
|
|
|
|
from .packets import rows
|
|
|
|
|
|
|
|
|
|
scope, params = _scope(project_id, unit_id, None if ids is None else list(ids))
|
|
|
|
|
stations = rows(conn, FACET_STATIONS_SQL.format(scope=scope), params)
|
|
|
|
|
workers = rows(conn, FACET_WORKERS_SQL.format(scope=scope), params)
|
|
|
|
|
return {
|
|
|
|
|
"stations": [{"id": r["id"], "name": r["name"], "dept_code": r["dept_code"]} for r in stations],
|
|
|
|
|
"workers": [{"id": str(r["id"]), "display_name": r["display_name"]} for r in workers],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------- the filters dict
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _int_or_none(v: Any) -> int | None:
|
|
|
|
|
if v is None or v == "":
|
|
|
|
|
return None
|
|
|
|
|
try:
|
|
|
|
|
return int(v)
|
|
|
|
|
except (TypeError, ValueError):
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _uuid_text(v: Any) -> str | None:
|
|
|
|
|
"""A welder id as uuid text; anything that is not a uuid becomes a value no packet has."""
|
|
|
|
|
s = str(v or "").strip()
|
|
|
|
|
if not s:
|
|
|
|
|
return None
|
|
|
|
|
try:
|
|
|
|
|
return str(uuid.UUID(s))
|
|
|
|
|
except ValueError:
|
|
|
|
|
return "00000000-0000-0000-0000-000000000000"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_flags(value: Any) -> list[str]:
|
|
|
|
|
out: list[str] = []
|
|
|
|
|
for part in str(value or "").split(","):
|
|
|
|
|
f = part.strip().lower()
|
|
|
|
|
if f in FLT_FLAGS and f not in out:
|
|
|
|
|
out.append(f)
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_filters(
|
|
|
|
|
params: Mapping[str, Any], pp: policy.Principal, *, screen: str | None, today: date | None = None
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
"""The filters dict list_cards takes, from the find bar's query values. `screen` is the default for Mine (the
|
|
|
|
|
board's own screen); `mine_screen` in the params wins when given."""
|
|
|
|
|
flags = parse_flags(params.get("flags"))
|
|
|
|
|
mine_station = _int_or_none(params.get("mine_station_id"))
|
|
|
|
|
mine = mine_scope(pp, str(params.get("mine_screen") or "") or screen, mine_station) if "mine" in flags else None
|
|
|
|
|
out: dict[str, Any] = {
|
|
|
|
|
"status": str(params.get("status") or "").strip(),
|
|
|
|
|
"dept": (str(params.get("dept") or "").strip() or None),
|
|
|
|
|
"unit_id": _int_or_none(params.get("unit_id")),
|
|
|
|
|
"project_id": _int_or_none(params.get("project_id")),
|
|
|
|
|
"q": clean_text(params.get("q") or "", 200),
|
|
|
|
|
"flag": str(params.get("flag") or "").strip().lower(),
|
|
|
|
|
"flags": flags,
|
|
|
|
|
"station_id": _int_or_none(params.get("station_id")),
|
|
|
|
|
"welder_id": _uuid_text(params.get("welder_id")),
|
|
|
|
|
"sort": str(params.get("sort") or "updated").strip().lower(),
|
|
|
|
|
"mine": mine,
|
|
|
|
|
"today": today or date.today(),
|
|
|
|
|
}
|
|
|
|
|
if out["dept"] and out["dept"].lower() == "all":
|
|
|
|
|
out["dept"] = None
|
|
|
|
|
if out["status"].lower() == "all":
|
|
|
|
|
out["status"] = ""
|
|
|
|
|
if out["flag"] == "all":
|
|
|
|
|
out["flag"] = ""
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def narrowing(filters: Mapping[str, Any]) -> bool:
|
|
|
|
|
"""True when a filter other than the project and unit pick is set (the old fset)."""
|
|
|
|
|
return bool(
|
|
|
|
|
filters.get("q")
|
|
|
|
|
or filters.get("status")
|
|
|
|
|
or filters.get("also_status")
|
|
|
|
|
or filters.get("dept")
|
|
|
|
|
or filters.get("flag")
|
|
|
|
|
or filters.get("flags")
|
|
|
|
|
or filters.get("station_id") is not None
|
|
|
|
|
or filters.get("welder_id")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def filter_where(filters: Mapping[str, Any]) -> tuple[list[str], list[Any]]:
|
|
|
|
|
"""The find bar terms of a filters dict: q, the user's status (`also_status` on a board, whose own status is
|
|
|
|
|
fixed), station, welder, the quick flags (Mine included) and the flag kind."""
|
|
|
|
|
where: list[str] = []
|
|
|
|
|
params: list[Any] = []
|
|
|
|
|
also = status_terms(str(filters.get("also_status") or ""))
|
|
|
|
|
if also:
|
|
|
|
|
where.append("p.status::text = ANY(%s)")
|
|
|
|
|
params.append(also)
|
|
|
|
|
if filters.get("station_id") is not None:
|
|
|
|
|
where.append("p.station_id = %s")
|
|
|
|
|
params.append(int(filters["station_id"]))
|
|
|
|
|
if filters.get("welder_id"):
|
|
|
|
|
where.append("p.welder_id = %s::uuid")
|
|
|
|
|
params.append(str(filters["welder_id"]))
|
|
|
|
|
flags = list(filters.get("flags") or [])
|
|
|
|
|
if flags:
|
|
|
|
|
today = filters.get("today") or date.today()
|
|
|
|
|
mine = filters.get("mine")
|
|
|
|
|
w, p = flag_terms(flags, today=today, mine=mine if isinstance(mine, MineScope) else None)
|
|
|
|
|
where += w
|
|
|
|
|
params += p
|
|
|
|
|
fk = flagkind_term(str(filters.get("flag") or ""))
|
|
|
|
|
if fk is not None:
|
|
|
|
|
where.append(fk[0])
|
|
|
|
|
params += fk[1]
|
|
|
|
|
w, p = search_terms(str(filters.get("q") or ""))
|
|
|
|
|
where += w
|
|
|
|
|
params += p
|
|
|
|
|
return where, params
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def board_filters(filters: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
|
|
"""The filters for a board whose columns fix their own statuses: the user's status moves to `also_status` and
|
|
|
|
|
the project, unit and limit are left to the board."""
|
|
|
|
|
out = dict(filters)
|
|
|
|
|
out["also_status"] = out.pop("status", "") or ""
|
|
|
|
|
out.pop("sort", None)
|
|
|
|
|
return out
|