05/04 Update code to replace Area related to Facility
This commit is contained in:
+10
-4
@@ -66,9 +66,12 @@ def index():
|
||||
open_issues_q = open_issues_q.filter(False)
|
||||
else:
|
||||
from app.models.facility import Area
|
||||
open_issues_q = open_issues_q.join(
|
||||
open_issues_q = open_issues_q.outerjoin(
|
||||
Area, Issue.area_id == Area.id
|
||||
).filter(Area.facility_id.in_(customer_facility_ids))
|
||||
).filter(db.or_(
|
||||
Issue.facility_id.in_(customer_facility_ids),
|
||||
Area.facility_id.in_(customer_facility_ids)
|
||||
))
|
||||
|
||||
# Single query — derive count from the list to avoid hitting the DB twice
|
||||
open_issues_all = open_issues_q.all()
|
||||
@@ -136,8 +139,11 @@ def index():
|
||||
sla_q = Issue.query.filter(Issue.status.in_(['open', 'in_progress']))
|
||||
if is_customer and customer_facility_ids:
|
||||
from app.models.facility import Area
|
||||
sla_q = sla_q.join(Area, Issue.area_id == Area.id).filter(
|
||||
Area.facility_id.in_(customer_facility_ids)
|
||||
sla_q = sla_q.outerjoin(Area, Issue.area_id == Area.id).filter(
|
||||
db.or_(
|
||||
Issue.facility_id.in_(customer_facility_ids),
|
||||
Area.facility_id.in_(customer_facility_ids)
|
||||
)
|
||||
)
|
||||
all_open_issues = sla_q.all() if not is_customer or customer_facility_ids else []
|
||||
sla_breached = sum(1 for i in all_open_issues if sla_status(i) == 'breached')
|
||||
|
||||
@@ -679,17 +679,16 @@ def flag_issue(inspection_id):
|
||||
return redirect(url_for('inspections.index'))
|
||||
|
||||
form = IssueForm()
|
||||
areas = Area.query.filter_by(facility_id=inspection.facility_id).order_by(Area.name).all()
|
||||
staff = User.query.filter(User.role.in_(['director', 'inspector'])).order_by(User.username).all()
|
||||
|
||||
form.area_id.choices = [(a.id, a.name) for a in areas]
|
||||
form.facility_id.choices = [(inspection.facility_id, inspection.facility.name)]
|
||||
form.assigned_to.choices = [(0, '— Unassigned —')] + [(u.id, u.username) for u in staff]
|
||||
|
||||
if form.validate_on_submit():
|
||||
photo_path = _save_photo(form.photo.data, subfolder='issue_photos')
|
||||
issue = Issue(
|
||||
inspection_id = inspection_id,
|
||||
area_id = form.area_id.data,
|
||||
facility_id = inspection.facility_id,
|
||||
severity = form.severity.data,
|
||||
description = form.description.data,
|
||||
photo_path = photo_path,
|
||||
@@ -733,7 +732,7 @@ def flag_issue(inspection_id):
|
||||
title = f'New Issue #{issue.id} at {inspection.facility.name}',
|
||||
body = (
|
||||
f'A new {issue.severity.title()}-severity issue has been logged '
|
||||
f'in {issue.area.name} at {inspection.facility.name} '
|
||||
f'at {inspection.facility.name} '
|
||||
f'during inspection #{inspection_id}. '
|
||||
f'Description: {issue.description[:120]}'
|
||||
f'{"…" if len(issue.description) > 120 else ""}'
|
||||
|
||||
+46
-43
@@ -80,9 +80,17 @@ def index():
|
||||
if not customer_facility_ids:
|
||||
q = q.filter(False)
|
||||
else:
|
||||
q = q.join(Area, Issue.area_id == Area.id).filter(
|
||||
Area.facility_id.in_(customer_facility_ids)
|
||||
)
|
||||
# Issues may have facility via direct facility_id (new) or via area_id (legacy/flagged)
|
||||
q = q.filter(
|
||||
db.or_(
|
||||
Issue.facility_id.in_(customer_facility_ids),
|
||||
db.and_(
|
||||
Issue.area_id.isnot(None),
|
||||
Issue.area_id == Area.id,
|
||||
Area.facility_id.in_(customer_facility_ids)
|
||||
)
|
||||
)
|
||||
).outerjoin(Area, Issue.area_id == Area.id)
|
||||
|
||||
severity_filter = request.args.get('severity', '')
|
||||
status_filter = request.args.get('status', '')
|
||||
@@ -94,10 +102,14 @@ def index():
|
||||
if status_filter:
|
||||
q = q.filter(Issue.status == status_filter)
|
||||
if facility_filter:
|
||||
q = q.join(Area, Issue.area_id == Area.id, isouter=True).filter(
|
||||
Area.facility_id == int(facility_filter)
|
||||
fid = int(facility_filter)
|
||||
q = q.filter(
|
||||
db.or_(
|
||||
Issue.facility_id == fid,
|
||||
db.and_(Issue.area_id.isnot(None),
|
||||
Area.facility_id == fid)
|
||||
)
|
||||
)
|
||||
|
||||
# SLA filter — SLA status is computed in Python (not a DB column).
|
||||
# When active: load all matching rows, filter in Python, wrap in a
|
||||
# single-page compatible object so the template interface is unchanged.
|
||||
@@ -159,10 +171,9 @@ def view(issue_id):
|
||||
flash('Access denied. You can only view issues assigned to you.', 'danger')
|
||||
return redirect(url_for('issues.index'))
|
||||
if current_user.role == 'customer':
|
||||
from app.models.facility import Area
|
||||
cids = get_customer_scope(current_user) or []
|
||||
area = db.session.get(Area, issue.area_id)
|
||||
if not area or area.facility_id not in cids:
|
||||
facility = issue.resolved_facility
|
||||
if not facility or facility.id not in cids:
|
||||
flash('Access denied.', 'danger')
|
||||
return redirect(url_for('issues.index'))
|
||||
|
||||
@@ -227,7 +238,7 @@ def view(issue_id):
|
||||
recipient = assignee,
|
||||
title = f'Issue #{issue.id} Status Updated',
|
||||
body = (
|
||||
f'Issue in {issue.area.name} was updated from '
|
||||
f'Issue in {issue.area.name if issue.area else issue.resolved_facility.name if issue.resolved_facility else '—'} was updated from '
|
||||
f'"{old_status.replace("_", " ").title()}" to '
|
||||
f'"{issue.status.replace("_", " ").title()}" '
|
||||
f'by {current_user.username}.'
|
||||
@@ -247,7 +258,7 @@ def view(issue_id):
|
||||
title = f'Issue #{issue.id} Assigned to You',
|
||||
body = (
|
||||
f'You have been assigned Issue #{issue.id} '
|
||||
f'({issue.severity.title()} severity) in {issue.area.name}. '
|
||||
f'({issue.severity.title()} severity) in {issue.area.name if issue.area else issue.resolved_facility.name if issue.resolved_facility else '—'}. '
|
||||
f'Current status: {issue.status.replace("_", " ").title()}.'
|
||||
),
|
||||
link = issue_link,
|
||||
@@ -265,7 +276,7 @@ def view(issue_id):
|
||||
title = f'Issue #{issue.id} Unassigned',
|
||||
body = (
|
||||
f'You have been removed from Issue #{issue.id} '
|
||||
f'in {issue.area.name} by {current_user.username}.'
|
||||
f'in {issue.area.name if issue.area else issue.resolved_facility.name if issue.resolved_facility else '—'} by {current_user.username}.'
|
||||
),
|
||||
link = issue_link,
|
||||
issue_id = issue.id,
|
||||
@@ -315,22 +326,22 @@ def view(issue_id):
|
||||
issue = issue,
|
||||
title = f'Issue #{issue.id} Updated',
|
||||
body = (
|
||||
f'Issue #{issue.id} in {issue.area.name} was updated by '
|
||||
f'Issue #{issue.id} in {issue.area.name if issue.area else issue.resolved_facility.name if issue.resolved_facility else '—'} was updated by '
|
||||
f'{current_user.username}: {"; ".join(changes)}.'
|
||||
),
|
||||
exclude_user_ids = exclude_ids,
|
||||
)
|
||||
|
||||
# ── Notify via matrix (issue_updated_customer) ───────────────
|
||||
facility_id = issue.area.facility_id if issue.area else None
|
||||
facility_id = issue.resolved_facility.id if issue.resolved_facility else None
|
||||
if facility_id and changes:
|
||||
changes_summary = '; '.join(changes)
|
||||
notify_by_matrix(
|
||||
event_type = 'issue_updated_customer',
|
||||
title = f'Issue #{issue.id} Updated at {issue.area.facility.name}',
|
||||
title = f'Issue #{issue.id} Updated at {issue.resolved_facility.name if issue.resolved_facility else '—'}',
|
||||
body = (
|
||||
f'Issue #{issue.id} ({issue.severity.title()} severity) '
|
||||
f'in {issue.area.name} was updated: {changes_summary}. '
|
||||
f'in {issue.area.name if issue.area else issue.resolved_facility.name if issue.resolved_facility else '—'} was updated: {changes_summary}. '
|
||||
f'Current status: {issue.status.replace("_", " ").title()}.'
|
||||
),
|
||||
link = url_for('issues.view', issue_id=issue.id),
|
||||
@@ -340,7 +351,7 @@ def view(issue_id):
|
||||
)
|
||||
db.session.commit() # Commit all notifications
|
||||
log_action(ACTION_UPDATE, 'Issue', issue.id,
|
||||
f'#{issue.id} in {issue.area.name}',
|
||||
f'#{issue.id} in {issue.area.name if issue.area else issue.resolved_facility.name if issue.resolved_facility else '—'}',
|
||||
f'status={issue.status}; assigned_to={issue.assigned_to}')
|
||||
flash('Issue updated.', 'success')
|
||||
return redirect(url_for('issues.view', issue_id=issue_id))
|
||||
@@ -419,18 +430,8 @@ def create():
|
||||
from app.routes.inspections import _save_photo
|
||||
photo_path = _save_photo(form.photo.data, subfolder='issue_photos')
|
||||
|
||||
# Derive area_id from the selected facility — use its first active area.
|
||||
# area_id is required on the Issue model so we must resolve it here.
|
||||
area = (Area.query
|
||||
.filter_by(facility_id=form.facility_id.data)
|
||||
.order_by(Area.name)
|
||||
.first())
|
||||
if area is None:
|
||||
form.facility_id.errors.append('Selected facility has no areas. Please add an area first.')
|
||||
return render_template('issues/form.html', form=form, title='Log New Issue')
|
||||
|
||||
issue = Issue(
|
||||
area_id = area.id,
|
||||
facility_id = form.facility_id.data,
|
||||
severity = form.severity.data,
|
||||
description = form.description.data,
|
||||
photo_path = photo_path,
|
||||
@@ -441,11 +442,11 @@ def create():
|
||||
db.session.add(issue)
|
||||
db.session.commit()
|
||||
current_app.logger.info(
|
||||
'ISSUE CREATED | id=%s | severity=%s | facility_id=%s | area_id=%s | assigned_to=%s | created_by=%s',
|
||||
issue.id, issue.severity, form.facility_id.data, issue.area_id, issue.assigned_to, current_user.username
|
||||
'ISSUE CREATED | id=%s | severity=%s | facility_id=%s | assigned_to=%s | created_by=%s',
|
||||
issue.id, issue.severity, issue.facility_id, issue.assigned_to, current_user.username
|
||||
)
|
||||
log_action(ACTION_CREATE, 'Issue', issue.id,
|
||||
f'#{issue.id} {issue.severity} in {issue.area.facility.name}',
|
||||
f'#{issue.id} {issue.severity} at {issue.facility.name}',
|
||||
f'severity={issue.severity}; assigned_to={issue.assigned_to}')
|
||||
|
||||
if issue.assigned_to:
|
||||
@@ -456,7 +457,7 @@ def create():
|
||||
title = f'New Issue #{issue.id} Assigned to You',
|
||||
body = (
|
||||
f'A new {issue.severity.title()}-severity issue has been logged '
|
||||
f'in {issue.area.name} and assigned to you. '
|
||||
f'at {issue.facility.name} and assigned to you. '
|
||||
f'Description: {issue.description[:120]}'
|
||||
f'{"…" if len(issue.description) > 120 else ""}'
|
||||
),
|
||||
@@ -468,20 +469,20 @@ def create():
|
||||
db.session.commit()
|
||||
|
||||
# ── Notify via matrix (issue_created) ────────────────────────
|
||||
area = db.session.get(Area, issue.area_id)
|
||||
if area:
|
||||
facility = issue.resolved_facility
|
||||
if facility:
|
||||
notify_by_matrix(
|
||||
event_type = 'issue_created',
|
||||
title = f'New Issue #{issue.id} at {area.facility.name}',
|
||||
title = f'New Issue #{issue.id} at {facility.name}',
|
||||
body = (
|
||||
f'A new {issue.severity.title()}-severity issue has been logged '
|
||||
f'in {area.name} at {area.facility.name}. '
|
||||
f'at {facility.name}. '
|
||||
f'Description: {issue.description[:120]}'
|
||||
f'{"…" if len(issue.description) > 120 else ""}'
|
||||
),
|
||||
link = url_for('issues.view', issue_id=issue.id),
|
||||
issue_id = issue.id,
|
||||
facility_id = area.facility_id,
|
||||
facility_id = facility.id,
|
||||
exclude_user_ids = {current_user.id},
|
||||
)
|
||||
db.session.commit()
|
||||
@@ -521,7 +522,7 @@ def verify(issue_id):
|
||||
issue_id, current_user.username, note,
|
||||
)
|
||||
log_action(ACTION_UPDATE, 'Issue', issue_id,
|
||||
f'#{issue_id} in {issue.area.name}',
|
||||
f'#{issue_id} in {issue.area.name if issue.area else issue.resolved_facility.name if issue.resolved_facility else '—'}',
|
||||
f'verified_by={current_user.username}')
|
||||
flash(f'Issue #{issue_id} verified and closed.', 'success')
|
||||
return redirect(url_for('issues.view', issue_id=issue_id))
|
||||
@@ -560,7 +561,7 @@ def request_verification(issue_id):
|
||||
issue_id, current_user.username,
|
||||
)
|
||||
log_action(ACTION_UPDATE, 'Issue', issue_id,
|
||||
f'#{issue_id} in {issue.area.name}',
|
||||
f'#{issue_id} in {issue.area.name if issue.area else issue.resolved_facility.name if issue.resolved_facility else '—'}',
|
||||
f'status=pending_verification; requested_by={current_user.username}')
|
||||
|
||||
# Notify via matrix (verification_requested)
|
||||
@@ -569,7 +570,7 @@ def request_verification(issue_id):
|
||||
title = f'Issue #{issue_id} Awaiting Verification',
|
||||
body = (
|
||||
f'{current_user.username} has marked Issue #{issue_id} '
|
||||
f'({issue.severity.title()} severity) in {issue.area.name} '
|
||||
f'({issue.severity.title()} severity) in {issue.area.name if issue.area else issue.resolved_facility.name if issue.resolved_facility else '—'} '
|
||||
f'as pending your verification.'
|
||||
),
|
||||
link = url_for('issues.view', issue_id=issue_id),
|
||||
@@ -602,7 +603,9 @@ def verification_queue():
|
||||
from collections import defaultdict
|
||||
by_facility = defaultdict(list)
|
||||
for issue in pending:
|
||||
by_facility[issue.area.facility].append(issue)
|
||||
f = issue.resolved_facility
|
||||
if f:
|
||||
by_facility[f].append(issue)
|
||||
|
||||
# Sort facilities alphabetically
|
||||
grouped = sorted(by_facility.items(), key=lambda x: x[0].name)
|
||||
@@ -638,8 +641,8 @@ def delete(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 '—'
|
||||
area_name = issue.area.name if issue.area else '—'
|
||||
facility_name = issue.resolved_facility.name if issue.resolved_facility else '—'
|
||||
severity = issue.severity
|
||||
|
||||
# Collect photo paths to clean up from disk after DB delete
|
||||
|
||||
+10
-4
@@ -64,8 +64,11 @@ def index():
|
||||
if customer_facility_ids is not None:
|
||||
if not customer_facility_ids:
|
||||
return q.filter(False)
|
||||
return q.join(Area, Issue.area_id == Area.id).filter(
|
||||
Area.facility_id.in_(customer_facility_ids)
|
||||
return q.outerjoin(Area, Issue.area_id == Area.id).filter(
|
||||
db.or_(
|
||||
Issue.facility_id.in_(customer_facility_ids),
|
||||
Area.facility_id.in_(customer_facility_ids)
|
||||
)
|
||||
)
|
||||
return q
|
||||
|
||||
@@ -450,8 +453,11 @@ def export_issues():
|
||||
Issue.status,
|
||||
Issue.resolved_at,
|
||||
User.username.label('assigned_to'),
|
||||
).join(Area, Issue.area_id == Area.id)\
|
||||
.join(Facility, Area.facility_id == Facility.id)\
|
||||
).outerjoin(Area, Issue.area_id == Area.id)\
|
||||
.outerjoin(Facility, db.or_(
|
||||
Facility.id == Area.facility_id,
|
||||
Facility.id == Issue.facility_id
|
||||
))\
|
||||
.outerjoin(User, Issue.assigned_to == User.id)\
|
||||
.filter(
|
||||
Issue.reported_at >= start,
|
||||
|
||||
@@ -92,7 +92,12 @@ def _build_report_data(report: ScheduledReport, start: datetime, end: datetime)
|
||||
|
||||
def _iq(q):
|
||||
if fid_filter:
|
||||
return q.join(Area, Issue.area_id == Area.id).filter(Area.facility_id.in_(fid_filter))
|
||||
return q.outerjoin(Area, Issue.area_id == Area.id).filter(
|
||||
db.or_(
|
||||
Issue.facility_id.in_(fid_filter),
|
||||
Area.facility_id.in_(fid_filter)
|
||||
)
|
||||
)
|
||||
return q
|
||||
|
||||
if report.report_type in ('summary', 'facility'):
|
||||
@@ -156,13 +161,18 @@ def _build_csv(report: ScheduledReport, start: datetime, end: datetime) -> bytes
|
||||
'Description', 'Status', 'Assigned To'])
|
||||
q = Issue.query
|
||||
if fid_filter:
|
||||
q = q.join(Area, Issue.area_id == Area.id).filter(Area.facility_id.in_(fid_filter))
|
||||
q = q.outerjoin(Area, Issue.area_id == Area.id).filter(
|
||||
db.or_(
|
||||
Issue.facility_id.in_(fid_filter),
|
||||
Area.facility_id.in_(fid_filter)
|
||||
)
|
||||
)
|
||||
for i in q.filter(Issue.status != 'resolved').order_by(Issue.reported_at.desc()).all():
|
||||
writer.writerow([
|
||||
i.id,
|
||||
i.reported_at.strftime('%Y-%m-%d %H:%M'),
|
||||
i.area.facility.name,
|
||||
i.area.name,
|
||||
i.resolved_facility.name if i.resolved_facility else '—',
|
||||
i.area.name if i.area else '—',
|
||||
i.severity,
|
||||
i.description.replace('\n', ' '),
|
||||
i.status,
|
||||
|
||||
Reference in New Issue
Block a user