Add application support teams with contacts
All checks were successful
CI / backend (push) Successful in 1m7s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 8s

Replaces the legacy supportteams/appowners pair: supportteams
(teamname unique, teamurl ServiceNow link) + supportteamcontacts
(multiple named contacts with SSO per team, the people you reach out
to), applications.supportteamid intact. Migration 7d18 migrates each
legacy team owner into a contact, drops appowners, and has a validated
downgrade. New /api/supportteams CRUD (admin writes, import-mode
timestamps, teamname lookup), Support card on application detail,
contacts column on the list, and a settings management page.
IMPORT-API.md mapping updated to the concrete endpoints.

658 tests pass; live dev migration applied (24 teams / 24 contacts);
fresh-install and downgrade round-trips verified on scratch DBs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-07-11 20:29:12 -04:00
parent 46e50c07ff
commit 7dae281993
18 changed files with 1095 additions and 172 deletions

View File

@@ -2,39 +2,8 @@
from shopdb.extensions import db
from .base import BaseModel
class AppOwner(BaseModel):
"""Application owner/contact."""
__tablename__ = 'appowners'
appownerid = db.Column(db.Integer, primary_key=True)
appowner = db.Column(db.String(100), nullable=False)
sso = db.Column(db.String(50))
email = db.Column(db.String(100))
# Relationships
supportteams = db.relationship('SupportTeam', back_populates='owner', lazy='dynamic')
def __repr__(self):
return f"<AppOwner {self.appowner}>"
class SupportTeam(BaseModel):
"""Application support team."""
__tablename__ = 'supportteams'
supportteamid = db.Column(db.Integer, primary_key=True)
teamname = db.Column(db.String(100), nullable=False)
teamurl = db.Column(db.String(255))
appownerid = db.Column(db.Integer, db.ForeignKey('appowners.appownerid'))
# Relationships
owner = db.relationship('AppOwner', back_populates='supportteams')
applications = db.relationship('Application', back_populates='supportteam', lazy='dynamic')
def __repr__(self):
return f"<SupportTeam {self.teamname}>"
# SupportTeam / SupportTeamContact live in supportteam.py; imported by the
# models package so the Application.supportteam relationship resolves.
class Application(BaseModel):
@@ -63,6 +32,22 @@ class Application(BaseModel):
supportteam = db.relationship('SupportTeam', back_populates='applications')
versions = db.relationship('AppVersion', back_populates='application', lazy='dynamic')
def to_dict(self):
"""Serialize, flattening the support team + its active contacts.
Emits supportteamname, teamurl, and the team's active contacts so the
frontend needs a single call to render the Support card.
"""
result = super().to_dict()
team = self.supportteam
result['supportteamname'] = team.teamname if team else None
result['teamurl'] = team.teamurl if team else None
result['contacts'] = [
{'name': c.name, 'sso': c.sso}
for c in team.active_contacts()
] if team else []
return result
def __repr__(self):
return f"<Application {self.appname}>"