"""Document lifecycle logic: eligibility, assignment backfill, primary team. The rules this encodes are the club's: - Only *active* members who belong to at least one team receive an obligation. A player not placed on a team signs nothing. - Obligations are role-scoped. A player on two teams signs the player contract once; ``UNIQUE(document_id, user_id)`` is the guarantee, and the team folder below is only where the signed copy is filed. - Publishing a version (or enabling a document) backfills pending assignments for everyone currently eligible. Joining a team backfills that one person across every published document matching their role. """ from app.extensions import db from app.models import ( DOCUMENT_KIND_ROLES, Coach, Document, DocumentAssignment, Manager, OrgTeam, Player, User, ) def user_org_team_ids(user): """Ids of the organisation teams the user is a member of. Role-appropriate: placements for a player, the coach/managers relationships (many-to-many *and* legacy single column) for staff. An admin or a scout belongs to none, regardless of how many teams exist. """ from app.permissions import coach_org_teams, manager_org_teams if isinstance(user, Player): return [tp.org_team_id for tp in user.team_placements] if isinstance(user, Coach): return [team.id for team in coach_org_teams(user)] if isinstance(user, Manager): return [team.id for team in manager_org_teams(user)] return [] def user_has_any_team(user): """Whether the user belongs to at least one organisation team.""" return bool(user_org_team_ids(user)) def primary_team_for_user(user): """The canonical team used for filing a signed copy, or None. A member can sit on several teams but every signed file is filed under one team only. The choice is deterministic: the team with the lowest id among the user's memberships. """ team_ids = user_org_team_ids(user) if not team_ids: return None return db.session.get(OrgTeam, min(team_ids)) def user_eligible_for_kind(user, kind): """Whether *user* must complete documents of *kind*.""" if kind not in DOCUMENT_KIND_ROLES: return False if not user.is_active_account: return False if user.role not in DOCUMENT_KIND_ROLES[kind]: return False return user_has_any_team(user) def eligible_user_ids(kind): """Ids of every user currently eligible for *kind*, ordered by id.""" roles = DOCUMENT_KIND_ROLES[kind] users = ( User.query.filter(User.role.in_(roles), User.is_active_account.is_(True)) .order_by(User.id) .all() ) return [u.id for u in users if user_has_any_team(u)] def backfill_assignments_for_document(document): """Create a pending assignment for every currently eligible user. Idempotent: anyone who already has an assignment for this document is left alone. Returns how many new assignments were created. """ existing = {a.user_id for a in document.assignments.all()} created = 0 for user_id in eligible_user_ids(document.kind): if user_id in existing: continue db.session.add( DocumentAssignment( document_id=document.id, user_id=user_id, status='pending', ) ) created += 1 if created: db.session.commit() return created def backfill_assignments_for_user(user): """Create pending assignments for *user* across published documents. Called when a member joins (or is reactivated into) a team. Only published documents are backfilled: an essentially invisible draft creates no obligations. Returns how many new assignments were created. """ if not user.is_active_account or not user_has_any_team(user): return 0 documents = Document.query.filter(Document.is_published.is_(True)).order_by(Document.id).all() created = 0 for doc in documents: if user.role not in DOCUMENT_KIND_ROLES.get(doc.kind, ()): continue exists = DocumentAssignment.query.filter_by(document_id=doc.id, user_id=user.id).first() if exists: continue db.session.add( DocumentAssignment( document_id=doc.id, user_id=user.id, status='pending', ) ) created += 1 if created: db.session.commit() return created