Jul 7 - Implement additional recipient per contract

This commit is contained in:
2026-07-07 20:28:17 -04:00
parent 8f971a403b
commit a3c6fd73bd
9 changed files with 613 additions and 8 deletions
+1
View File
@@ -4,6 +4,7 @@ from app.models.inspection import (InspectionTemplate, ChecklistItem,
Inspection, InspectionResult)
from app.models.issue import Issue
from app.models.project import Project, CustomerAssignment
from app.models.project_recipient import ProjectNotificationRecipient
from app.models.api_token import RefreshToken, DeviceToken
from app.models.notification_matrix import NotificationMatrix
from app.models.inspection_schedule import InspectionSchedule
+72
View File
@@ -0,0 +1,72 @@
"""
app/models/project_recipient.py
-------------------------------
Per-contract (Project) additional notification recipients.
The global notification matrix controls WHICH ROLES receive each event
org-wide. This model adds contract-scoped recipients on top of that:
each row subscribes one recipient to a chosen set of matrix event types,
but ONLY for events that occur in facilities belonging to that contract.
Recipient kinds
---------------
staff — user_id set, email NULL. Gets an in-app Notification + email
via notify() (matrix-authority mode, preferences not consulted).
external — email set, user_id NULL. Gets a plain email only (no account,
no in-app record) via _send_custom_email().
`events` stores a JSON list of MATRIX_EVENTS keys (see
app/models/notification_matrix.py). Dispatch happens inside
notify_by_matrix() → _notify_project_recipients() after the matrix roles
and global custom emails are processed.
"""
import json
from app import db
from app.utils.time_utils import now_eastern
class ProjectNotificationRecipient(db.Model):
"""A contract-scoped additional notification recipient."""
__tablename__ = 'project_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 layer.
user_id = db.Column(db.Integer,
db.ForeignKey('users.id', ondelete='CASCADE'),
nullable=True, index=True)
email = db.Column(db.String(255), nullable=True)
events = db.Column(db.Text, nullable=False, default='[]') # JSON list of event keys
created_at = db.Column(db.DateTime, default=now_eastern, nullable=False)
# Relationships
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],
backref=db.backref('project_notification_subscriptions',
lazy='dynamic'))
@property
def is_staff(self):
"""True when the recipient is an application user (in-app + email)."""
return self.user_id is not None
def get_events(self):
"""Return the subscribed event keys as a Python list."""
if not self.events:
return []
try:
result = json.loads(self.events)
return [e for e in result if isinstance(e, str) and e.strip()]
except (json.JSONDecodeError, TypeError):
return []
def __repr__(self):
who = f'user={self.user_id}' if self.user_id else f'email={self.email}'
return f'<ProjectNotificationRecipient project={self.project_id} {who}>'