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:
|
if ov.match_status:
|
||||||
crit.append(f"status == {ov.match_status}")
|
crit.append(f"status == {ov.match_status}")
|
||||||
if ov.match_error_contains:
|
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:
|
if crit:
|
||||||
scope = scope + " [" + ", ".join(crit) + "]"
|
scope = scope + " [" + ", ".join(crit) + "]"
|
||||||
|
|
||||||
@ -95,6 +104,13 @@ def overrides():
|
|||||||
"comment": ov.comment or "",
|
"comment": ov.comment or "",
|
||||||
"match_status": ov.match_status or "",
|
"match_status": ov.match_status or "",
|
||||||
"match_error_contains": ov.match_error_contains 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_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_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 ""
|
start_at_str = request.form.get("start_at") or ""
|
||||||
end_at_str = request.form.get("end_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,
|
object_name=object_name if level == "object" else None,
|
||||||
match_status=match_status,
|
match_status=match_status,
|
||||||
match_error_contains=match_error_contains,
|
match_error_contains=match_error_contains,
|
||||||
|
match_error_mode=match_error_mode,
|
||||||
treat_as_success=treat_as_success,
|
treat_as_success=treat_as_success,
|
||||||
active=True,
|
active=True,
|
||||||
comment=comment,
|
comment=comment,
|
||||||
@ -218,6 +241,12 @@ def overrides_update(override_id: int):
|
|||||||
|
|
||||||
match_status = (request.form.get("match_status") or "").strip() or None
|
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_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 ""
|
start_at_str = request.form.get("start_at") or ""
|
||||||
end_at_str = request.form.get("end_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.object_name = object_name if level == "object" else None
|
||||||
ov.match_status = match_status
|
ov.match_status = match_status
|
||||||
ov.match_error_contains = match_error_contains
|
ov.match_error_contains = match_error_contains
|
||||||
|
ov.match_error_mode = match_error_mode
|
||||||
ov.treat_as_success = treat_as_success
|
ov.treat_as_success = treat_as_success
|
||||||
ov.comment = comment
|
ov.comment = comment
|
||||||
ov.start_at = start_at
|
ov.start_at = start_at
|
||||||
|
|||||||
@ -968,6 +968,7 @@ def api_run_checks_mark_success_override():
|
|||||||
object_name=obj_name,
|
object_name=obj_name,
|
||||||
match_status=(obj_status or None),
|
match_status=(obj_status or None),
|
||||||
match_error_contains=(err[:255] if err else None),
|
match_error_contains=(err[:255] if err else None),
|
||||||
|
match_error_mode=("contains" if err else None),
|
||||||
treat_as_success=True,
|
treat_as_success=True,
|
||||||
active=True,
|
active=True,
|
||||||
comment=comment,
|
comment=comment,
|
||||||
@ -999,6 +1000,7 @@ def api_run_checks_mark_success_override():
|
|||||||
backup_type=job.backup_type or None,
|
backup_type=job.backup_type or None,
|
||||||
match_status=(getattr(run, "status", None) 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_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,
|
treat_as_success=True,
|
||||||
active=True,
|
active=True,
|
||||||
comment=comment,
|
comment=comment,
|
||||||
|
|||||||
@ -293,7 +293,8 @@ def _apply_overrides_to_run(job: Job, run: JobRun):
|
|||||||
try:
|
try:
|
||||||
mec = (getattr(ov, "match_error_contains", None) or "").strip()
|
mec = (getattr(ov, "match_error_contains", None) or "").strip()
|
||||||
if mec:
|
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:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
@ -342,6 +343,40 @@ def _apply_overrides_to_run(job: Job, run: JobRun):
|
|||||||
return False
|
return False
|
||||||
return needle.lower() in haystack.lower()
|
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:
|
def _matches_status(candidate: str | None, expected: str | None) -> bool:
|
||||||
if not expected:
|
if not expected:
|
||||||
return True
|
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.
|
# Global overrides should match both the run-level remark and any object-level error messages.
|
||||||
if ov.match_error_contains:
|
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
|
return True
|
||||||
|
|
||||||
# Check persisted run-object error messages.
|
# Check persisted run-object error messages.
|
||||||
for row in run_object_rows or []:
|
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
|
return True
|
||||||
|
|
||||||
objs = []
|
objs = []
|
||||||
@ -424,7 +459,7 @@ def _apply_overrides_to_run(job: Job, run: JobRun):
|
|||||||
except Exception:
|
except Exception:
|
||||||
objs = []
|
objs = []
|
||||||
for obj in objs or []:
|
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 True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -438,7 +473,7 @@ def _apply_overrides_to_run(job: Job, run: JobRun):
|
|||||||
continue
|
continue
|
||||||
if not _matches_status(row.get("status"), ov.match_status):
|
if not _matches_status(row.get("status"), ov.match_status):
|
||||||
continue
|
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
|
continue
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -453,7 +488,7 @@ def _apply_overrides_to_run(job: Job, run: JobRun):
|
|||||||
continue
|
continue
|
||||||
if not _matches_status(getattr(obj, "status", None), ov.match_status):
|
if not _matches_status(getattr(obj, "status", None), ov.match_status):
|
||||||
continue
|
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
|
continue
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@ -378,7 +378,7 @@ def migrate_remarks_active_from_date() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def migrate_overrides_match_columns() -> 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()
|
engine = db.get_engine()
|
||||||
inspector = inspect(engine)
|
inspector = inspect(engine)
|
||||||
try:
|
try:
|
||||||
@ -397,6 +397,25 @@ def migrate_overrides_match_columns() -> None:
|
|||||||
print("[migrations] Adding overrides.match_error_contains column...")
|
print("[migrations] Adding overrides.match_error_contains column...")
|
||||||
conn.execute(text('ALTER TABLE "overrides" ADD COLUMN match_error_contains VARCHAR(255)'))
|
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.")
|
print("[migrations] migrate_overrides_match_columns completed.")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -156,6 +156,8 @@ class Override(db.Model):
|
|||||||
# Matching criteria on object status / error message
|
# Matching criteria on object status / error message
|
||||||
match_status = db.Column(db.String(32), nullable=True)
|
match_status = db.Column(db.String(32), nullable=True)
|
||||||
match_error_contains = db.Column(db.String(255), 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
|
# Behaviour flags
|
||||||
treat_as_success = db.Column(db.Boolean, nullable=False, default=True)
|
treat_as_success = db.Column(db.Boolean, nullable=False, default=True)
|
||||||
|
|||||||
@ -62,7 +62,16 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<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">
|
<input type="text" class="form-control" id="ov_match_error_contains" name="match_error_contains" placeholder="Text to match in error message">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
@ -142,6 +151,7 @@
|
|||||||
data-ov-object-name="{{ ov.object_name or '' }}"
|
data-ov-object-name="{{ ov.object_name or '' }}"
|
||||||
data-ov-match-status="{{ ov.match_status or '' }}"
|
data-ov-match-status="{{ ov.match_status or '' }}"
|
||||||
data-ov-match-error-contains="{{ ov.match_error_contains 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-treat-as-success="{{ 1 if ov.treat_as_success else 0 }}"
|
||||||
data-ov-comment="{{ ov.comment or '' }}"
|
data-ov-comment="{{ ov.comment or '' }}"
|
||||||
data-ov-start-at="{{ ov.start_at_raw or '' }}"
|
data-ov-start-at="{{ ov.start_at_raw or '' }}"
|
||||||
@ -190,6 +200,7 @@
|
|||||||
const jobField = document.getElementById('ov_job_id');
|
const jobField = document.getElementById('ov_job_id');
|
||||||
const objectNameField = document.getElementById('ov_object_name');
|
const objectNameField = document.getElementById('ov_object_name');
|
||||||
const matchStatusField = document.getElementById('ov_match_status');
|
const matchStatusField = document.getElementById('ov_match_status');
|
||||||
|
const matchErrorModeField = document.getElementById('ov_match_error_mode');
|
||||||
const matchErrorContainsField = document.getElementById('ov_match_error_contains');
|
const matchErrorContainsField = document.getElementById('ov_match_error_contains');
|
||||||
const treatAsSuccessField = document.getElementById('ov_treat_success');
|
const treatAsSuccessField = document.getElementById('ov_treat_success');
|
||||||
const commentField = document.getElementById('ov_comment');
|
const commentField = document.getElementById('ov_comment');
|
||||||
@ -228,6 +239,7 @@
|
|||||||
setValue(jobField, btn.dataset.ovJobId || '');
|
setValue(jobField, btn.dataset.ovJobId || '');
|
||||||
setValue(objectNameField, btn.dataset.ovObjectName || '');
|
setValue(objectNameField, btn.dataset.ovObjectName || '');
|
||||||
setValue(matchStatusField, btn.dataset.ovMatchStatus || '');
|
setValue(matchStatusField, btn.dataset.ovMatchStatus || '');
|
||||||
|
setValue(matchErrorModeField, btn.dataset.ovMatchErrorMode || 'contains');
|
||||||
setValue(matchErrorContainsField, btn.dataset.ovMatchErrorContains || '');
|
setValue(matchErrorContainsField, btn.dataset.ovMatchErrorContains || '');
|
||||||
if (treatAsSuccessField) treatAsSuccessField.checked = (btn.dataset.ovTreatAsSuccess === '1');
|
if (treatAsSuccessField) treatAsSuccessField.checked = (btn.dataset.ovTreatAsSuccess === '1');
|
||||||
setValue(commentField, btn.dataset.ovComment || '');
|
setValue(commentField, btn.dataset.ovComment || '');
|
||||||
|
|||||||
@ -35,6 +35,14 @@
|
|||||||
- Updated menu rendering to hide all unauthorized menu items for Reporter users.
|
- 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.
|
- 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
|
## v0.1.20
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user