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')
|
||||
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_id_snap = area.id
|
||||
db.session.delete(area)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
from app.utils.time_utils import now_eastern
|
||||
from flask import (Blueprint, render_template, redirect, url_for,
|
||||
flash, request, current_app, jsonify)
|
||||
@@ -550,3 +551,54 @@ def verification_queue():
|
||||
sla_status = sla_status,
|
||||
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
|
||||
</button>
|
||||
<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">
|
||||
<i class="bi bi-trash-fill"></i> Delete Permanently
|
||||
</button>
|
||||
|
||||
@@ -130,9 +130,16 @@
|
||||
<a href="{{ url_for('facilities.edit_area', area_id=area.id) }}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<form method="POST" action="{{ url_for('facilities.delete_area', area_id=area.id) }}" class="d-inline" onsubmit="return confirm('Delete this area?');">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||
{% set area_issue_count = area.issues.count() %}
|
||||
{% set area_insp_count = area.inspections.count() %}
|
||||
<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>
|
||||
</button>
|
||||
</form>
|
||||
@@ -185,7 +192,7 @@
|
||||
<i class="bi bi-x-circle"></i> Cancel
|
||||
</button>
|
||||
<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"
|
||||
class="btn btn-danger"
|
||||
{% if facility.inspections.count() > 0 %}disabled{% endif %}>
|
||||
|
||||
@@ -106,6 +106,17 @@
|
||||
<i class="bi bi-eye"></i> View
|
||||
{% endif %}
|
||||
</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>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@@ -220,9 +220,54 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="{{ url_for('issues.index') }}" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> Back to Issues
|
||||
</a>
|
||||
<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
|
||||
</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 %}
|
||||
<script>
|
||||
(function () {
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
<form method="POST"
|
||||
action="{{ url_for('templates.duplicate_template', template_id=template.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" class="btn btn-sm btn-outline-secondary"
|
||||
title="Duplicate template">
|
||||
<i class="bi bi-copy"></i> Duplicate
|
||||
@@ -103,7 +103,7 @@
|
||||
</div>
|
||||
<form id="renameTemplateForm" method="POST" action="">
|
||||
<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">
|
||||
<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
|
||||
</button>
|
||||
<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">
|
||||
<i class="bi bi-trash-fill"></i> Delete Permanently
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user