99 lines
4.8 KiB
Python
99 lines
4.8 KiB
Python
from app import db
|
|
from app.utils.time_utils import now_eastern
|
|
|
|
|
|
# ── Event type constants ───────────────────────────────────────────────────────
|
|
# These are the canonical keys used across the preference system.
|
|
# Every call to notify() should pass one of these as event_type.
|
|
|
|
EVENT_ISSUE_ASSIGNED = 'issue_assigned'
|
|
EVENT_ISSUE_STATUS = 'issue_status'
|
|
EVENT_ISSUE_COMMENT = 'issue_comment'
|
|
EVENT_ISSUE_FOLLOW = 'issue_follow_update'
|
|
EVENT_INSPECTION_DONE = 'inspection_completed'
|
|
EVENT_SLA_ALERT = 'sla_alert'
|
|
# Fired when an issue is flagged during an inspection (web or mobile).
|
|
# Listed here so users can configure email preferences for this event.
|
|
EVENT_ISSUE_FLAGGED = 'issue_flagged'
|
|
|
|
# ── Customer portal events ─────────────────────────────────────────────────
|
|
# Fired when an inspection completes or an issue is created/updated at a
|
|
# facility the customer is assigned to. Separate constants allow customers
|
|
# to manage these preferences independently from internal staff events.
|
|
EVENT_CUSTOMER_INSPECTION_DONE = 'customer_inspection_completed'
|
|
EVENT_CUSTOMER_ISSUE_UPDATED = 'customer_issue_updated'
|
|
|
|
ALL_EVENT_TYPES = {
|
|
EVENT_ISSUE_ASSIGNED: 'Issue assigned to me',
|
|
EVENT_ISSUE_STATUS: 'Issue status changed',
|
|
EVENT_ISSUE_COMMENT: 'New comment on issue',
|
|
EVENT_ISSUE_FOLLOW: 'Updates on followed issues',
|
|
EVENT_ISSUE_FLAGGED: 'Issue flagged (from inspection)',
|
|
EVENT_INSPECTION_DONE: 'Inspection completed',
|
|
EVENT_SLA_ALERT: 'SLA at-risk / breached alerts',
|
|
# Customer-facing — only relevant for customer role accounts
|
|
EVENT_CUSTOMER_INSPECTION_DONE: 'Inspection completed at my facility (portal)',
|
|
EVENT_CUSTOMER_ISSUE_UPDATED: 'Issue created or updated at my facility (portal)',
|
|
}
|
|
|
|
|
|
class Notification(db.Model):
|
|
"""Stores in-app notifications for users.
|
|
|
|
Each notification is tied to a single recipient and optionally linked to
|
|
either an Issue or an Inspection so the UI can build a direct link.
|
|
"""
|
|
__tablename__ = 'notifications'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False, index=True)
|
|
title = db.Column(db.String(255), nullable=False)
|
|
body = db.Column(db.Text, nullable=False)
|
|
link = db.Column(db.String(512))
|
|
is_read = db.Column(db.Boolean, default=False, nullable=False)
|
|
created_at = db.Column(db.DateTime, default=now_eastern, nullable=False)
|
|
|
|
# Optional FK references — only one will be populated at a time
|
|
issue_id = db.Column(db.Integer, db.ForeignKey('issues.id', ondelete='CASCADE'), nullable=True)
|
|
inspection_id = db.Column(db.Integer, db.ForeignKey('inspections.id', ondelete='CASCADE'), nullable=True)
|
|
|
|
# Event type — stored for mobile API polling so the iPad can categorise alerts.
|
|
# Added phase17; NULL for notifications created before the migration.
|
|
event_type = db.Column(db.String(50), nullable=True)
|
|
|
|
# Digest tracking: set to True when created, cleared after digest email sent
|
|
digest_pending = db.Column(db.Boolean, default=False, nullable=False, index=True)
|
|
|
|
recipient = db.relationship('User', foreign_keys=[user_id], backref='notifications')
|
|
|
|
def __repr__(self):
|
|
return f'<Notification {self.id} user={self.user_id} read={self.is_read}>'
|
|
|
|
|
|
class NotificationPreference(db.Model):
|
|
"""Per-user, per-event notification preferences.
|
|
|
|
One row per (user_id, event_type) combination.
|
|
If no row exists for a user+event, defaults apply (email on, no digest).
|
|
"""
|
|
__tablename__ = 'notification_preferences'
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'),
|
|
nullable=False, index=True)
|
|
event_type = db.Column(db.String(50), nullable=False)
|
|
email_enabled = db.Column(db.Boolean, default=True, nullable=False)
|
|
digest_mode = db.Column(db.Boolean, default=False, nullable=False)
|
|
# digest_frequency: 'hourly' or 'daily' — only relevant when digest_mode is True
|
|
digest_frequency = db.Column(db.String(10), default='daily', nullable=False)
|
|
|
|
__table_args__ = (
|
|
db.UniqueConstraint('user_id', 'event_type', name='uq_notif_pref_user_event'),
|
|
)
|
|
|
|
user = db.relationship('User', foreign_keys=[user_id],
|
|
backref=db.backref('notification_preferences', lazy='dynamic'))
|
|
|
|
def __repr__(self):
|
|
return (f'<NotificationPreference user={self.user_id} '
|
|
f'event={self.event_type} email={self.email_enabled} digest={self.digest_mode}>') |