Apr 1 2026: update issues delete button
This commit is contained in:
@@ -167,6 +167,15 @@ def delete_area(area_id):
|
|||||||
flash('Cannot delete area with existing inspections.', 'danger')
|
flash('Cannot delete area with existing inspections.', 'danger')
|
||||||
return redirect(url_for('facilities.view_facility', facility_id=facility_id))
|
return redirect(url_for('facilities.view_facility', facility_id=facility_id))
|
||||||
|
|
||||||
|
issue_count = area.issues.count()
|
||||||
|
if issue_count > 0:
|
||||||
|
flash(
|
||||||
|
f'Cannot delete area "{area.name}" — it has {issue_count} issue record(s) on file. '
|
||||||
|
f'Resolve or reassign those issues first.',
|
||||||
|
'danger'
|
||||||
|
)
|
||||||
|
return redirect(url_for('facilities.view_facility', facility_id=facility_id))
|
||||||
|
|
||||||
area_name = area.name
|
area_name = area.name
|
||||||
area_id_snap = area.id
|
area_id_snap = area.id
|
||||||
db.session.delete(area)
|
db.session.delete(area)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import os
|
||||||
from app.utils.time_utils import now_eastern
|
from app.utils.time_utils import now_eastern
|
||||||
from flask import (Blueprint, render_template, redirect, url_for,
|
from flask import (Blueprint, render_template, redirect, url_for,
|
||||||
flash, request, current_app, jsonify)
|
flash, request, current_app, jsonify)
|
||||||
@@ -550,3 +551,54 @@ def verification_queue():
|
|||||||
sla_status = sla_status,
|
sla_status = sla_status,
|
||||||
sla_hours_remaining = sla_hours_remaining,
|
sla_hours_remaining = sla_hours_remaining,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ── Delete ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@bp.route('/<int:issue_id>/delete', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
@supervisor_required
|
||||||
|
def delete(issue_id):
|
||||||
|
"""Permanently delete an issue and its associated photos.
|
||||||
|
|
||||||
|
Restricted to admin and supervisor roles. The deletion is recorded in
|
||||||
|
the audit log before the record is removed so there is always a trace.
|
||||||
|
"""
|
||||||
|
issue = Issue.query.get_or_404(issue_id)
|
||||||
|
|
||||||
|
# Snapshot fields needed for logging before deletion
|
||||||
|
issue_id_snap = issue.id
|
||||||
|
issue_desc = issue.description[:80]
|
||||||
|
area_name = issue.area.name if issue.area else f'area_id={issue.area_id}'
|
||||||
|
facility_name = issue.area.facility.name if issue.area else '—'
|
||||||
|
severity = issue.severity
|
||||||
|
|
||||||
|
# Collect photo paths to clean up from disk after DB delete
|
||||||
|
photo_paths = []
|
||||||
|
if issue.photo_path:
|
||||||
|
photo_paths.append(issue.photo_path)
|
||||||
|
if issue.result_photos:
|
||||||
|
photo_paths.extend(issue.result_photos)
|
||||||
|
|
||||||
|
db.session.delete(issue)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Remove orphaned photo files — best-effort, never block on failure
|
||||||
|
static_folder = current_app.root_path
|
||||||
|
for rel_path in photo_paths:
|
||||||
|
abs_path = os.path.normpath(os.path.join(static_folder, 'static', rel_path))
|
||||||
|
try:
|
||||||
|
if os.path.isfile(abs_path):
|
||||||
|
os.remove(abs_path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
current_app.logger.info(
|
||||||
|
'ISSUE DELETED | id=%s | severity=%s | area=%s | facility=%s | deleted_by=%s',
|
||||||
|
issue_id_snap, severity, area_name, facility_name, current_user.username,
|
||||||
|
)
|
||||||
|
log_action(ACTION_DELETE, 'Issue', issue_id_snap,
|
||||||
|
f'#{issue_id_snap} {severity} in {area_name}',
|
||||||
|
f'facility={facility_name}; description={issue_desc}')
|
||||||
|
|
||||||
|
flash(f'Issue #{issue_id_snap} has been permanently deleted.', 'success')
|
||||||
|
return redirect(url_for('issues.index'))
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
<i class="bi bi-x-circle"></i> Cancel
|
<i class="bi bi-x-circle"></i> Cancel
|
||||||
</button>
|
</button>
|
||||||
<form id="deleteFacilityForm" method="POST" action="" class="d-inline">
|
<form id="deleteFacilityForm" method="POST" action="" class="d-inline">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<button type="submit" id="confirmDeleteBtn" class="btn btn-danger">
|
<button type="submit" id="confirmDeleteBtn" class="btn btn-danger">
|
||||||
<i class="bi bi-trash-fill"></i> Delete Permanently
|
<i class="bi bi-trash-fill"></i> Delete Permanently
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -130,9 +130,16 @@
|
|||||||
<a href="{{ url_for('facilities.edit_area', area_id=area.id) }}" class="btn btn-sm btn-outline-primary">
|
<a href="{{ url_for('facilities.edit_area', area_id=area.id) }}" class="btn btn-sm btn-outline-primary">
|
||||||
<i class="bi bi-pencil"></i>
|
<i class="bi bi-pencil"></i>
|
||||||
</a>
|
</a>
|
||||||
<form method="POST" action="{{ url_for('facilities.delete_area', area_id=area.id) }}" class="d-inline" onsubmit="return confirm('Delete this area?');">
|
{% set area_issue_count = area.issues.count() %}
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
{% set area_insp_count = area.inspections.count() %}
|
||||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
<form method="POST" action="{{ url_for('facilities.delete_area', area_id=area.id) }}" class="d-inline"
|
||||||
|
onsubmit="return confirm('Delete area '{{ area.name }}'? This cannot be undone.');">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-danger"
|
||||||
|
{% if area_insp_count > 0 or area_issue_count > 0 %}
|
||||||
|
disabled
|
||||||
|
title="Cannot delete — {{ area_insp_count }} inspection(s) and {{ area_issue_count }} issue(s) on file"
|
||||||
|
{% endif %}>
|
||||||
<i class="bi bi-trash"></i>
|
<i class="bi bi-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -185,7 +192,7 @@
|
|||||||
<i class="bi bi-x-circle"></i> Cancel
|
<i class="bi bi-x-circle"></i> Cancel
|
||||||
</button>
|
</button>
|
||||||
<form method="POST" action="{{ url_for('facilities.delete_facility', facility_id=facility.id) }}" class="d-inline">
|
<form method="POST" action="{{ url_for('facilities.delete_facility', facility_id=facility.id) }}" class="d-inline">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
class="btn btn-danger"
|
class="btn btn-danger"
|
||||||
{% if facility.inspections.count() > 0 %}disabled{% endif %}>
|
{% if facility.inspections.count() > 0 %}disabled{% endif %}>
|
||||||
|
|||||||
@@ -106,6 +106,17 @@
|
|||||||
<i class="bi bi-eye"></i> View
|
<i class="bi bi-eye"></i> View
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
|
{% if current_user.role in ['admin', 'supervisor'] %}
|
||||||
|
<form method="POST" action="{{ url_for('issues.delete', issue_id=issue.id) }}"
|
||||||
|
class="d-inline"
|
||||||
|
onsubmit="return confirm('Permanently delete Issue #{{ issue.id }}? This cannot be undone.');">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-danger"
|
||||||
|
title="Delete Issue #{{ issue.id }}">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -220,9 +220,54 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a href="{{ url_for('issues.index') }}" class="btn btn-outline-secondary btn-sm">
|
<div class="d-flex align-items-center gap-2 mt-2">
|
||||||
|
<a href="{{ url_for('issues.index') }}" class="btn btn-outline-secondary btn-sm">
|
||||||
<i class="bi bi-arrow-left"></i> Back to Issues
|
<i class="bi bi-arrow-left"></i> Back to Issues
|
||||||
</a>
|
</a>
|
||||||
|
{% if current_user.role in ['admin', 'supervisor'] %}
|
||||||
|
<button type="button" class="btn btn-outline-danger btn-sm"
|
||||||
|
data-bs-toggle="modal" data-bs-target="#deleteIssueModal">
|
||||||
|
<i class="bi bi-trash"></i> Delete Issue
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if current_user.role in ['admin', 'supervisor'] %}
|
||||||
|
<!-- Delete Confirmation Modal -->
|
||||||
|
<div class="modal fade" id="deleteIssueModal" tabindex="-1" aria-labelledby="deleteIssueModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-danger text-white">
|
||||||
|
<h5 class="modal-title" id="deleteIssueModalLabel">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill"></i> Confirm Deletion
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>You are about to permanently delete:</p>
|
||||||
|
<p class="fw-bold">Issue #{{ issue.id }} — {{ issue.severity|title }} severity in {{ issue.area.name }}</p>
|
||||||
|
<div class="alert alert-warning mb-0">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill"></i>
|
||||||
|
This action is <strong>irreversible</strong>. All comments, photos, and
|
||||||
|
follower records associated with this issue will also be deleted.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||||
|
<i class="bi bi-x-circle"></i> Cancel
|
||||||
|
</button>
|
||||||
|
<form method="POST" action="{{ url_for('issues.delete', issue_id=issue.id) }}" class="d-inline">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<button type="submit" class="btn btn-danger">
|
||||||
|
<i class="bi bi-trash-fill"></i> Delete Permanently
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
<form method="POST"
|
<form method="POST"
|
||||||
action="{{ url_for('templates.duplicate_template', template_id=template.id) }}"
|
action="{{ url_for('templates.duplicate_template', template_id=template.id) }}"
|
||||||
class="d-inline">
|
class="d-inline">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<button type="submit" class="btn btn-sm btn-outline-secondary"
|
<button type="submit" class="btn btn-sm btn-outline-secondary"
|
||||||
title="Duplicate template">
|
title="Duplicate template">
|
||||||
<i class="bi bi-copy"></i> Duplicate
|
<i class="bi bi-copy"></i> Duplicate
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<form id="renameTemplateForm" method="POST" action="">
|
<form id="renameTemplateForm" method="POST" action="">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="renameInput" class="form-label fw-semibold">Name <span class="text-danger">*</span></label>
|
<label for="renameInput" class="form-label fw-semibold">Name <span class="text-danger">*</span></label>
|
||||||
@@ -177,7 +177,7 @@
|
|||||||
<i class="bi bi-x-circle"></i> Cancel
|
<i class="bi bi-x-circle"></i> Cancel
|
||||||
</button>
|
</button>
|
||||||
<form id="deleteTemplateForm" method="POST" action="" class="d-inline">
|
<form id="deleteTemplateForm" method="POST" action="" class="d-inline">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
<button type="submit" id="confirmDeleteBtn" class="btn btn-danger">
|
<button type="submit" id="confirmDeleteBtn" class="btn btn-danger">
|
||||||
<i class="bi bi-trash-fill"></i> Delete Permanently
|
<i class="bi bi-trash-fill"></i> Delete Permanently
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user