Merge pull request 'Auto-commit local changes before build (2026-01-13 14:12:58)' (#111) from v20260113-06-overrides-error-match-modes into main
Reviewed-on: #111
This commit is contained in:
commit
d2f7618772
@ -1 +1 @@
|
||||
v20260113-05-reporter-menu-restrict
|
||||
v20260113-06-overrides-error-match-modes
|
||||
|
||||
@ -75,7 +75,16 @@ def overrides():
|
||||
if ov.match_status:
|
||||
crit.append(f"status == {ov.match_status}")
|
||||
if ov.match_error_contains:
|
||||
crit.append(f"error contains '{ov.match_error_contains}'")
|
||||
mode = (getattr(ov, "match_error_mode", None) or "contains").strip().lower()
|
||||
if mode == "exact":
|
||||
label = "error exact"
|
||||
elif mode == "starts_with":
|
||||
label = "error starts with"
|
||||
elif mode == "ends_with":
|
||||
label = "error ends with"
|
||||
else:
|
||||
label = "error contains"
|
||||
crit.append(f"{label} '{ov.match_error_contains}'")
|
||||
if crit:
|
||||
scope = scope + " [" + ", ".join(crit) + "]"
|
||||
|
||||
@ -95,6 +104,13 @@ def overrides():
|
||||
"comment": ov.comment or "",
|
||||
"match_status": ov.match_status or "",
|
||||
"match_error_contains": ov.match_error_contains or "",
|
||||
"match_error_mode": getattr(ov, "match_error_mode", None) or "",
|
||||
"backup_software": ov.backup_software or "",
|
||||
"backup_type": ov.backup_type or "",
|
||||
"job_id": ov.job_id or "",
|
||||
"object_name": ov.object_name or "",
|
||||
"start_at_raw": (ov.start_at.strftime("%Y-%m-%dT%H:%M") if ov.start_at else ""),
|
||||
"end_at_raw": (ov.end_at.strftime("%Y-%m-%dT%H:%M") if ov.end_at else ""),
|
||||
}
|
||||
)
|
||||
|
||||
@ -126,6 +142,12 @@ def overrides_create():
|
||||
|
||||
match_status = (request.form.get("match_status") or "").strip() or None
|
||||
match_error_contains = (request.form.get("match_error_contains") or "").strip() or None
|
||||
match_error_mode = (request.form.get("match_error_mode") or "").strip().lower() or None
|
||||
if match_error_contains:
|
||||
if match_error_mode not in ("contains", "exact", "starts_with", "ends_with"):
|
||||
match_error_mode = "contains"
|
||||
else:
|
||||
match_error_mode = None
|
||||
|
||||
start_at_str = request.form.get("start_at") or ""
|
||||
end_at_str = request.form.get("end_at") or ""
|
||||
@ -159,6 +181,7 @@ def overrides_create():
|
||||
object_name=object_name if level == "object" else None,
|
||||
match_status=match_status,
|
||||
match_error_contains=match_error_contains,
|
||||
match_error_mode=match_error_mode,
|
||||
treat_as_success=treat_as_success,
|
||||
active=True,
|
||||
comment=comment,
|
||||
@ -218,6 +241,12 @@ def overrides_update(override_id: int):
|
||||
|
||||
match_status = (request.form.get("match_status") or "").strip() or None
|
||||
match_error_contains = (request.form.get("match_error_contains") or "").strip() or None
|
||||
match_error_mode = (request.form.get("match_error_mode") or "").strip().lower() or None
|
||||
if match_error_contains:
|
||||
if match_error_mode not in ("contains", "exact", "starts_with", "ends_with"):
|
||||
match_error_mode = "contains"
|
||||
else:
|
||||
match_error_mode = None
|
||||
|
||||
start_at_str = request.form.get("start_at") or ""
|
||||
end_at_str = request.form.get("end_at") or ""
|
||||
@ -252,6 +281,7 @@ def overrides_update(override_id: int):
|
||||
ov.object_name = object_name if level == "object" else None
|
||||
ov.match_status = match_status
|
||||
ov.match_error_contains = match_error_contains
|
||||
ov.match_error_mode = match_error_mode
|
||||
ov.treat_as_success = treat_as_success
|
||||
ov.comment = comment
|
||||
ov.start_at = start_at
|
||||
|
||||
@ -968,6 +968,7 @@ def api_run_checks_mark_success_override():
|
||||
object_name=obj_name,
|
||||
match_status=(obj_status or None),
|
||||
match_error_contains=(err[:255] if err else None),
|
||||
match_error_mode=("contains" if err else None),
|
||||
treat_as_success=True,
|
||||
active=True,
|
||||
comment=comment,
|
||||
@ -999,6 +1000,7 @@ def api_run_checks_mark_success_override():
|
||||
backup_type=job.backup_type or None,
|
||||
match_status=(getattr(run, "status", None) or None),
|
||||
match_error_contains=(match_error_contains[:255] if match_error_contains else None),
|
||||
match_error_mode=("contains" if match_error_contains else None),
|
||||
treat_as_success=True,
|
||||
active=True,
|
||||
comment=comment,
|
||||
|
||||
@ -293,7 +293,8 @@ def _apply_overrides_to_run(job: Job, run: JobRun):
|
||||
try:
|
||||
mec = (getattr(ov, "match_error_contains", None) or "").strip()
|
||||
if mec:
|
||||
parts.append(f"contains={mec}")
|
||||
mem = (getattr(ov, "match_error_mode", None) or "contains").strip()
|
||||
parts.append(f"error_{mem}={mec}")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
@ -342,6 +343,40 @@ def _apply_overrides_to_run(job: Job, run: JobRun):
|
||||
return False
|
||||
return needle.lower() in haystack.lower()
|
||||
|
||||
def _matches_error_text(haystack: str | None, needle: str | None, mode: str | None) -> bool:
|
||||
"""Match error text using a configured mode.
|
||||
|
||||
Modes:
|
||||
- contains (default)
|
||||
- exact
|
||||
- starts_with
|
||||
- ends_with
|
||||
|
||||
Matching is case-insensitive and trims surrounding whitespace.
|
||||
"""
|
||||
if not needle:
|
||||
return True
|
||||
if not haystack:
|
||||
return False
|
||||
|
||||
hs = (haystack or "").strip()
|
||||
nd = (needle or "").strip()
|
||||
if not hs:
|
||||
return False
|
||||
|
||||
hs_l = hs.lower()
|
||||
nd_l = nd.lower()
|
||||
m = (mode or "contains").strip().lower()
|
||||
|
||||
if m == "exact":
|
||||
return hs_l == nd_l
|
||||
if m in ("starts_with", "startswith", "start"):
|
||||
return hs_l.startswith(nd_l)
|
||||
if m in ("ends_with", "endswith", "end"):
|
||||
return hs_l.endswith(nd_l)
|
||||
# Default/fallback
|
||||
return nd_l in hs_l
|
||||
|
||||
def _matches_status(candidate: str | None, expected: str | None) -> bool:
|
||||
if not expected:
|
||||
return True
|
||||
@ -410,12 +445,12 @@ def _apply_overrides_to_run(job: Job, run: JobRun):
|
||||
|
||||
# Global overrides should match both the run-level remark and any object-level error messages.
|
||||
if ov.match_error_contains:
|
||||
if _contains(run.remark, ov.match_error_contains):
|
||||
if _matches_error_text(run.remark, ov.match_error_contains, getattr(ov, "match_error_mode", None)):
|
||||
return True
|
||||
|
||||
# Check persisted run-object error messages.
|
||||
for row in run_object_rows or []:
|
||||
if _contains(row.get("error_message"), ov.match_error_contains):
|
||||
if _matches_error_text(row.get("error_message"), ov.match_error_contains, getattr(ov, "match_error_mode", None)):
|
||||
return True
|
||||
|
||||
objs = []
|
||||
@ -424,7 +459,7 @@ def _apply_overrides_to_run(job: Job, run: JobRun):
|
||||
except Exception:
|
||||
objs = []
|
||||
for obj in objs or []:
|
||||
if _contains(getattr(obj, "error_message", None), ov.match_error_contains):
|
||||
if _matches_error_text(getattr(obj, "error_message", None), ov.match_error_contains, getattr(ov, "match_error_mode", None)):
|
||||
return True
|
||||
return False
|
||||
|
||||
@ -438,7 +473,7 @@ def _apply_overrides_to_run(job: Job, run: JobRun):
|
||||
continue
|
||||
if not _matches_status(row.get("status"), ov.match_status):
|
||||
continue
|
||||
if not _contains(row.get("error_message"), ov.match_error_contains):
|
||||
if not _matches_error_text(row.get("error_message"), ov.match_error_contains, getattr(ov, "match_error_mode", None)):
|
||||
continue
|
||||
return True
|
||||
|
||||
@ -453,7 +488,7 @@ def _apply_overrides_to_run(job: Job, run: JobRun):
|
||||
continue
|
||||
if not _matches_status(getattr(obj, "status", None), ov.match_status):
|
||||
continue
|
||||
if not _contains(getattr(obj, "error_message", None), ov.match_error_contains):
|
||||
if not _matches_error_text(getattr(obj, "error_message", None), ov.match_error_contains, getattr(ov, "match_error_mode", None)):
|
||||
continue
|
||||
return True
|
||||
|
||||
|
||||
@ -378,7 +378,7 @@ def migrate_remarks_active_from_date() -> None:
|
||||
|
||||
|
||||
def migrate_overrides_match_columns() -> None:
|
||||
"""Add match_status and match_error_contains columns to overrides table if missing."""
|
||||
"""Add match_status / match_error columns to overrides table if missing."""
|
||||
engine = db.get_engine()
|
||||
inspector = inspect(engine)
|
||||
try:
|
||||
@ -397,6 +397,25 @@ def migrate_overrides_match_columns() -> None:
|
||||
print("[migrations] Adding overrides.match_error_contains column...")
|
||||
conn.execute(text('ALTER TABLE "overrides" ADD COLUMN match_error_contains VARCHAR(255)'))
|
||||
|
||||
if "match_error_mode" not in existing_columns:
|
||||
print("[migrations] Adding overrides.match_error_mode column...")
|
||||
conn.execute(text('ALTER TABLE "overrides" ADD COLUMN match_error_mode VARCHAR(20)'))
|
||||
|
||||
# Backfill mode for existing overrides that already have a match string.
|
||||
try:
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE "overrides"
|
||||
SET match_error_mode = 'contains'
|
||||
WHERE (match_error_mode IS NULL OR match_error_mode = '')
|
||||
AND (match_error_contains IS NOT NULL AND match_error_contains <> '');
|
||||
"""
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
print("[migrations] migrate_overrides_match_columns completed.")
|
||||
|
||||
|
||||
|
||||
@ -156,6 +156,8 @@ class Override(db.Model):
|
||||
# Matching criteria on object status / error message
|
||||
match_status = db.Column(db.String(32), nullable=True)
|
||||
match_error_contains = db.Column(db.String(255), nullable=True)
|
||||
# Matching mode for error text: contains (default), exact, starts_with, ends_with
|
||||
match_error_mode = db.Column(db.String(20), nullable=True)
|
||||
|
||||
# Behaviour flags
|
||||
treat_as_success = db.Column(db.Boolean, nullable=False, default=True)
|
||||
|
||||
@ -62,7 +62,16 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label for="ov_match_error_contains" class="form-label">Error contains</label>
|
||||
<label for="ov_match_error_mode" class="form-label">Error match type</label>
|
||||
<select class="form-select" id="ov_match_error_mode" name="match_error_mode">
|
||||
<option value="contains">Contains</option>
|
||||
<option value="exact">Exact</option>
|
||||
<option value="starts_with">Starts with</option>
|
||||
<option value="ends_with">Ends with</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label for="ov_match_error_contains" class="form-label">Error text</label>
|
||||
<input type="text" class="form-control" id="ov_match_error_contains" name="match_error_contains" placeholder="Text to match in error message">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
@ -142,6 +151,7 @@
|
||||
data-ov-object-name="{{ ov.object_name or '' }}"
|
||||
data-ov-match-status="{{ ov.match_status or '' }}"
|
||||
data-ov-match-error-contains="{{ ov.match_error_contains or '' }}"
|
||||
data-ov-match-error-mode="{{ ov.match_error_mode or 'contains' }}"
|
||||
data-ov-treat-as-success="{{ 1 if ov.treat_as_success else 0 }}"
|
||||
data-ov-comment="{{ ov.comment or '' }}"
|
||||
data-ov-start-at="{{ ov.start_at_raw or '' }}"
|
||||
@ -190,6 +200,7 @@
|
||||
const jobField = document.getElementById('ov_job_id');
|
||||
const objectNameField = document.getElementById('ov_object_name');
|
||||
const matchStatusField = document.getElementById('ov_match_status');
|
||||
const matchErrorModeField = document.getElementById('ov_match_error_mode');
|
||||
const matchErrorContainsField = document.getElementById('ov_match_error_contains');
|
||||
const treatAsSuccessField = document.getElementById('ov_treat_success');
|
||||
const commentField = document.getElementById('ov_comment');
|
||||
@ -228,6 +239,7 @@
|
||||
setValue(jobField, btn.dataset.ovJobId || '');
|
||||
setValue(objectNameField, btn.dataset.ovObjectName || '');
|
||||
setValue(matchStatusField, btn.dataset.ovMatchStatus || '');
|
||||
setValue(matchErrorModeField, btn.dataset.ovMatchErrorMode || 'contains');
|
||||
setValue(matchErrorContainsField, btn.dataset.ovMatchErrorContains || '');
|
||||
if (treatAsSuccessField) treatAsSuccessField.checked = (btn.dataset.ovTreatAsSuccess === '1');
|
||||
setValue(commentField, btn.dataset.ovComment || '');
|
||||
|
||||
@ -35,6 +35,14 @@
|
||||
- Updated menu rendering to hide all unauthorized menu items for Reporter users.
|
||||
- Adjusted route access to ensure Feedback pages are accessible for the Reporter role.
|
||||
|
||||
---
|
||||
|
||||
## v20260113-06-overrides-error-match-modes
|
||||
- Added configurable error text matching modes for overrides: contains, exact, starts with, ends with
|
||||
- Updated override evaluation logic to apply the selected match mode across run remarks and object error messages
|
||||
- Extended overrides UI with a match type selector and improved edit support for existing overrides
|
||||
- Added database migration to create and backfill overrides.match_error_mode for existing records
|
||||
|
||||
***
|
||||
|
||||
## v0.1.20
|
||||
|
||||
Loading…
Reference in New Issue
Block a user