July 7 - Implement notification per contract
This commit is contained in:
@@ -5,4 +5,5 @@ from app.models.inspection import (InspectionTemplate, ChecklistItem,
|
||||
from app.models.issue import Issue
|
||||
from app.models.project import Project, CustomerAssignment
|
||||
from app.models.api_token import RefreshToken, DeviceToken
|
||||
from app.models.notification_matrix import NotificationMatrix
|
||||
from app.models.notification_matrix import NotificationMatrix
|
||||
from app.models.notification_recipient import ContractNotificationRecipient
|
||||
@@ -0,0 +1,79 @@
|
||||
"""
|
||||
app/models/notification_recipient.py
|
||||
------------------------------------
|
||||
Per-contract additional notification recipients.
|
||||
|
||||
Each row is ONE extra recipient attached to a Contract (Project) who should be
|
||||
notified when selected notification events fire within that contract's
|
||||
facilities — in ADDITION to whoever the global NotificationMatrix already
|
||||
routes to.
|
||||
|
||||
A recipient is either:
|
||||
- an existing app user (user_id set, email NULL) → in-app notification + email
|
||||
- a free-form email (email set, user_id NULL) → email only
|
||||
|
||||
`event_types` is a JSON list of event_type keys (matching MATRIX_EVENTS) that
|
||||
this recipient subscribes to for this contract. A recipient is notified only
|
||||
when the firing event_type is present in this list. An empty list means the
|
||||
recipient receives nothing (the add form requires at least one event).
|
||||
|
||||
Resolution happens centrally in notify_by_matrix() (app/utils/notifications.py),
|
||||
which derives the contract from the event's facility / issue / inspection.
|
||||
"""
|
||||
|
||||
import json
|
||||
from app import db
|
||||
from app.utils.time_utils import now_eastern
|
||||
|
||||
|
||||
class ContractNotificationRecipient(db.Model):
|
||||
__tablename__ = 'contract_notification_recipients'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
project_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('projects.id', ondelete='CASCADE'),
|
||||
nullable=False, index=True,
|
||||
)
|
||||
# Exactly one of user_id / email is set (enforced in the route, not the DB).
|
||||
user_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('users.id', ondelete='CASCADE'),
|
||||
nullable=True, index=True,
|
||||
)
|
||||
email = db.Column(db.String(200), nullable=True)
|
||||
# JSON list of event_type keys this recipient is subscribed to for this contract.
|
||||
event_types = db.Column(db.Text, nullable=True) # JSON-encoded list[str]
|
||||
created_at = db.Column(db.DateTime, nullable=False, default=now_eastern)
|
||||
|
||||
project = db.relationship('Project', foreign_keys=[project_id],
|
||||
backref=db.backref('notification_recipients',
|
||||
lazy='dynamic',
|
||||
cascade='all, delete-orphan'))
|
||||
user = db.relationship('User', foreign_keys=[user_id])
|
||||
|
||||
def get_event_types(self) -> list:
|
||||
"""Return event_types as a Python list of strings."""
|
||||
if not self.event_types:
|
||||
return []
|
||||
try:
|
||||
result = json.loads(self.event_types)
|
||||
return [e for e in result if isinstance(e, str) and e.strip()]
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return []
|
||||
|
||||
def set_event_types(self, values) -> None:
|
||||
"""Store a list of event_type keys as a JSON string."""
|
||||
clean = [v.strip() for v in (values or []) if isinstance(v, str) and v.strip()]
|
||||
self.event_types = json.dumps(clean)
|
||||
|
||||
@property
|
||||
def display_target(self) -> str:
|
||||
"""Human-readable recipient label for the UI."""
|
||||
if self.user_id and self.user:
|
||||
return f'{self.user.display_name} ({self.user.email})'
|
||||
return self.email or '—'
|
||||
|
||||
def __repr__(self):
|
||||
who = f'user={self.user_id}' if self.user_id else f'email={self.email}'
|
||||
return f'<ContractNotificationRecipient project={self.project_id} {who}>'
|
||||
Reference in New Issue
Block a user