Feb 27 2026: add inspection delete button
This commit is contained in:
@@ -65,7 +65,7 @@ class Inspection(db.Model):
|
|||||||
completed_at = db.Column(db.DateTime)
|
completed_at = db.Column(db.DateTime)
|
||||||
|
|
||||||
results = db.relationship('InspectionResult', backref='inspection', lazy='dynamic', cascade='all, delete-orphan')
|
results = db.relationship('InspectionResult', backref='inspection', lazy='dynamic', cascade='all, delete-orphan')
|
||||||
issues = db.relationship('Issue', backref='inspection', lazy='dynamic')
|
issues = db.relationship('Issue', backref='inspection', lazy='dynamic', cascade='all, delete-orphan')
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<Inspection {self.id} - {self.inspection_date}>'
|
return f'<Inspection {self.id} - {self.inspection_date}>'
|
||||||
|
|||||||
@@ -404,7 +404,55 @@ def flag_issue(inspection_id):
|
|||||||
@supervisor_required
|
@supervisor_required
|
||||||
def delete(inspection_id):
|
def delete(inspection_id):
|
||||||
inspection = Inspection.query.get_or_404(inspection_id)
|
inspection = Inspection.query.get_or_404(inspection_id)
|
||||||
|
|
||||||
|
# Capture identifiers for the log before deletion
|
||||||
|
insp_id = inspection.id
|
||||||
|
insp_date = inspection.inspection_date.strftime('%Y-%m-%d %H:%M')
|
||||||
|
facility_name = inspection.facility.name
|
||||||
|
template_name = inspection.template.name
|
||||||
|
inspector_name = inspection.inspector.username
|
||||||
|
|
||||||
|
# ── Clean up uploaded photos from disk ────────────────────────────────
|
||||||
|
# Collect photo paths from form data (inspection_photos) and issues
|
||||||
|
photo_paths = []
|
||||||
|
if inspection.notes:
|
||||||
|
try:
|
||||||
|
notes_data = json.loads(inspection.notes)
|
||||||
|
form_data = notes_data.get('_form_data', {}) if isinstance(notes_data, dict) else {}
|
||||||
|
for val in form_data.values():
|
||||||
|
if isinstance(val, str) and val.startswith('uploads/'):
|
||||||
|
photo_paths.append(val)
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
for issue in inspection.issues.all():
|
||||||
|
if issue.photo_path:
|
||||||
|
photo_paths.append(issue.photo_path)
|
||||||
|
|
||||||
|
# Delete the inspection record (cascade removes results and issues)
|
||||||
db.session.delete(inspection)
|
db.session.delete(inspection)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Inspection deleted.', 'success')
|
|
||||||
|
# Remove photo files after successful DB commit
|
||||||
|
for rel_path in photo_paths:
|
||||||
|
abs_path = os.path.join(current_app.config['UPLOAD_FOLDER'], '..', 'static', rel_path)
|
||||||
|
abs_path = os.path.normpath(abs_path)
|
||||||
|
try:
|
||||||
|
if os.path.isfile(abs_path):
|
||||||
|
os.remove(abs_path)
|
||||||
|
except OSError:
|
||||||
|
pass # Non-fatal: log but don't block the response
|
||||||
|
|
||||||
|
current_app.logger.info(
|
||||||
|
'INSPECTION DELETED | id=%s | facility="%s" | template="%s" | '
|
||||||
|
'date=%s | inspector=%s | deleted_by=%s',
|
||||||
|
insp_id, facility_name, template_name,
|
||||||
|
insp_date, inspector_name, current_user.username
|
||||||
|
)
|
||||||
|
|
||||||
|
flash(
|
||||||
|
f'Inspection #{insp_id} ({template_name} — {facility_name}, {insp_date}) '
|
||||||
|
f'has been permanently deleted.',
|
||||||
|
'success'
|
||||||
|
)
|
||||||
return redirect(url_for('inspections.index'))
|
return redirect(url_for('inspections.index'))
|
||||||
@@ -70,12 +70,23 @@
|
|||||||
{{ ins.status|replace('_',' ')|title }}
|
{{ ins.status|replace('_',' ')|title }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="text-nowrap">
|
||||||
{% if ins.status == 'in_progress' or ins.status == 'flagged' %}
|
{% if ins.status == 'in_progress' or ins.status == 'flagged' %}
|
||||||
<a href="{{ url_for('inspections.execute', inspection_id=ins.id) }}" class="btn btn-sm btn-outline-primary">Continue</a>
|
<a href="{{ url_for('inspections.execute', inspection_id=ins.id) }}" class="btn btn-sm btn-outline-primary">Continue</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ url_for('inspections.view', inspection_id=ins.id) }}" class="btn btn-sm btn-outline-secondary">View</a>
|
<a href="{{ url_for('inspections.view', inspection_id=ins.id) }}" class="btn btn-sm btn-outline-secondary">View</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if current_user.role in ['admin', 'supervisor'] %}
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm btn-outline-danger ms-1"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#deleteInspectionModal"
|
||||||
|
data-inspection-id="{{ ins.id }}"
|
||||||
|
data-inspection-label="{{ ins.template.name }} — {{ ins.facility.name }} ({{ ins.inspection_date.strftime('%Y-%m-%d') }})"
|
||||||
|
title="Delete inspection">
|
||||||
|
<i class="bi bi-trash3"></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -103,4 +114,52 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if current_user.role in ['admin', 'supervisor'] %}
|
||||||
|
<!-- Delete Inspection Confirmation Modal -->
|
||||||
|
<div class="modal fade" id="deleteInspectionModal" tabindex="-1" aria-labelledby="deleteInspectionModalLabel" 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="deleteInspectionModalLabel">
|
||||||
|
<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 the following inspection:</p>
|
||||||
|
<p class="fw-bold" id="deleteInspectionLabel"></p>
|
||||||
|
<p class="text-muted mb-0">This will also remove all associated results, flagged issues, and uploaded photos. This action is <strong>irreversible</strong>.</p>
|
||||||
|
</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 id="deleteInspectionForm" method="POST" action="" class="d-inline">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<button type="submit" class="btn btn-danger">
|
||||||
|
<i class="bi bi-trash3-fill"></i> Delete Permanently
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
{% if current_user.role in ['admin', 'supervisor'] %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const modal = document.getElementById('deleteInspectionModal');
|
||||||
|
modal.addEventListener('show.bs.modal', function (event) {
|
||||||
|
const btn = event.relatedTarget;
|
||||||
|
const id = btn.getAttribute('data-inspection-id');
|
||||||
|
const label = btn.getAttribute('data-inspection-label');
|
||||||
|
document.getElementById('deleteInspectionLabel').textContent = label;
|
||||||
|
document.getElementById('deleteInspectionForm').action = '/inspections/' + id + '/delete';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user