Feb 27 2026: update Issue view details
This commit is contained in:
@@ -2,6 +2,23 @@ from app import db
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class IssueComment(db.Model):
|
||||||
|
__tablename__ = 'issue_comments'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
issue_id = db.Column(db.Integer, db.ForeignKey('issues.id'), nullable=False)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||||
|
status_at_time = db.Column(db.String(20)) # snapshot of issue status when comment was made
|
||||||
|
body = db.Column(db.Text, nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
author = db.relationship('User', foreign_keys=[user_id])
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<IssueComment {self.id} issue={self.issue_id}>'
|
||||||
|
|
||||||
|
|
||||||
class Issue(db.Model):
|
class Issue(db.Model):
|
||||||
__tablename__ = 'issues'
|
__tablename__ = 'issues'
|
||||||
|
|
||||||
@@ -20,6 +37,9 @@ class Issue(db.Model):
|
|||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
assigned_user = db.relationship('User', foreign_keys=[assigned_to], backref='assigned_issues')
|
assigned_user = db.relationship('User', foreign_keys=[assigned_to], backref='assigned_issues')
|
||||||
|
comments = db.relationship('IssueComment', backref='issue', lazy='dynamic',
|
||||||
|
order_by='IssueComment.created_at',
|
||||||
|
cascade='all, delete-orphan')
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<Issue {self.id} - {self.severity}>'
|
return f'<Issue {self.id} - {self.severity}>'
|
||||||
+16
-4
@@ -3,7 +3,7 @@ from flask import (Blueprint, render_template, redirect, url_for,
|
|||||||
flash, request, current_app)
|
flash, request, current_app)
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
from app import db
|
from app import db
|
||||||
from app.models.issue import Issue
|
from app.models.issue import Issue, IssueComment
|
||||||
from app.models.facility import Facility, Area
|
from app.models.facility import Facility, Area
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.utils.forms import IssueForm, IssueUpdateForm
|
from app.utils.forms import IssueForm, IssueUpdateForm
|
||||||
@@ -84,15 +84,27 @@ def view(issue_id):
|
|||||||
existing = issue.result_photos or []
|
existing = issue.result_photos or []
|
||||||
issue.result_photos = existing + new_photos
|
issue.result_photos = existing + new_photos
|
||||||
|
|
||||||
|
# Persist a comment entry if the user wrote update notes
|
||||||
|
comment_body = form.comments.data.strip() if form.comments.data else ''
|
||||||
|
if comment_body:
|
||||||
|
comment = IssueComment(
|
||||||
|
issue_id = issue.id,
|
||||||
|
user_id = current_user.id,
|
||||||
|
status_at_time = issue.status,
|
||||||
|
body = comment_body,
|
||||||
|
)
|
||||||
|
db.session.add(comment)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
current_app.logger.info(
|
current_app.logger.info(
|
||||||
'ISSUE UPDATED | id=%s | status=%s | result_photos_added=%s | updated_by=%s',
|
'ISSUE UPDATED | id=%s | status=%s | result_photos_added=%s | comment=%s | updated_by=%s',
|
||||||
issue.id, issue.status, len(new_photos), current_user.username
|
issue.id, issue.status, len(new_photos), bool(comment_body), current_user.username
|
||||||
)
|
)
|
||||||
flash('Issue updated.', 'success')
|
flash('Issue updated.', 'success')
|
||||||
return redirect(url_for('issues.view', issue_id=issue_id))
|
return redirect(url_for('issues.view', issue_id=issue_id))
|
||||||
|
|
||||||
return render_template('issues/view.html', issue=issue, form=form)
|
comments = issue.comments.order_by(IssueComment.created_at.asc()).all()
|
||||||
|
return render_template('issues/view.html', issue=issue, form=form, comments=comments)
|
||||||
|
|
||||||
|
|
||||||
# ── Standalone create (not from an inspection) ────────────────────────────────
|
# ── Standalone create (not from an inspection) ────────────────────────────────
|
||||||
|
|||||||
@@ -46,7 +46,13 @@ def index():
|
|||||||
|
|
||||||
total_inspections = base.count()
|
total_inspections = base.count()
|
||||||
completed = base.filter(Inspection.status == 'completed').count()
|
completed = base.filter(Inspection.status == 'completed').count()
|
||||||
flagged = base.filter(Inspection.status == 'flagged').count()
|
# "Flagged" = open or in-progress issues logged within the date range,
|
||||||
|
# not inspections with status='flagged' (those get completed on submit).
|
||||||
|
flagged = Issue.query.filter(
|
||||||
|
Issue.reported_at >= start,
|
||||||
|
Issue.reported_at <= end,
|
||||||
|
Issue.status != 'resolved',
|
||||||
|
).count()
|
||||||
avg_score = db.session.query(func.avg(Inspection.overall_score)).filter(
|
avg_score = db.session.query(func.avg(Inspection.overall_score)).filter(
|
||||||
Inspection.inspection_date >= start,
|
Inspection.inspection_date >= start,
|
||||||
Inspection.inspection_date <= end,
|
Inspection.inspection_date <= end,
|
||||||
|
|||||||
@@ -69,6 +69,34 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{# ── Update History ─────────────────────────────────────────────────── #}
|
||||||
|
{% if comments %}
|
||||||
|
<div class="card shadow-sm mb-4">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h6 class="mb-0"><i class="bi bi-clock-history"></i> Update History</h6>
|
||||||
|
</div>
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
{% for c in comments %}
|
||||||
|
<li class="list-group-item">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||||
|
<span class="fw-semibold text-dark">
|
||||||
|
<i class="bi bi-person-circle"></i> {{ c.author.username }}
|
||||||
|
</span>
|
||||||
|
<span class="d-flex align-items-center gap-2">
|
||||||
|
<span class="badge bg-{{ 'success' if c.status_at_time == 'resolved' else 'warning text-dark' if c.status_at_time == 'in_progress' else 'danger' }} rounded-pill" style="font-size:.65rem;">
|
||||||
|
{{ c.status_at_time|replace('_',' ')|title }}
|
||||||
|
</span>
|
||||||
|
<small class="text-muted">{{ c.created_at.strftime('%Y-%m-%d %H:%M') }}</small>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="mb-0 text-secondary" style="white-space:pre-wrap; font-size:.9rem;">{{ c.body }}</p>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-lg-4">
|
<div class="col-lg-4">
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
{% for label, value, color, icon in [
|
{% for label, value, color, icon in [
|
||||||
('Total Inspections', total_inspections, 'primary', 'bi-clipboard-data'),
|
('Total Inspections', total_inspections, 'primary', 'bi-clipboard-data'),
|
||||||
('Completed', completed, 'success', 'bi-check-circle'),
|
('Completed', completed, 'success', 'bi-check-circle'),
|
||||||
('Flagged', flagged, 'danger', 'bi-flag'),
|
('Open Issues', flagged, 'danger', 'bi-flag'),
|
||||||
('Avg Score', (avg_score|string + '%') if avg_score else '—', 'info', 'bi-graph-up'),
|
('Avg Score', (avg_score|string + '%') if avg_score else '—', 'info', 'bi-graph-up'),
|
||||||
] %}
|
] %}
|
||||||
<div class="col-md-3 mb-3">
|
<div class="col-md-3 mb-3">
|
||||||
|
|||||||
Reference in New Issue
Block a user