"""Documents, versions, and per-user assignments. The old contract flow stored one uploaded file per player even when the file was identical for everyone, and Drive received no team/role structure at all. This module replaces that with three two-level concepts: - ``Document`` the canonical document (e.g. ``Contrat joueur``). One row per kind; the file is stored once, never once per person. - ``DocumentVersion`` an immutable published revision. Uploading a new PDF is publishing a new version, which is the twice a year update cycle. - ``DocumentAssignment`` a per-person obligation. ``UNIQUE(document_id, user_id)`` is what guarantees a player signs a document exactly once, whatever the number of teams they sit on. Signing obligations are role-scoped, not team-scoped: the team folder in Drive is a *filing* concern for the signed copy; the obligation itself is created once per eligible person. """ from app.extensions import db from app.time_utils import utc_now_naive #: Every document kind this application can hold. DOCUMENT_KINDS = ( 'player', 'coach', 'manager', 'team', 'code_of_conduct', 'ethics_code', ) #: Kinds whose completion is an uploaded signed PDF. SIGNATURE_KINDS = ('player', 'coach', 'manager', 'team') #: Kinds whose completion is read-and-acknowledge only (no file). ACKNOWLEDGMENT_KINDS = ('code_of_conduct', 'ethics_code') #: Kind -> set of roles eligible to complete it. ``team`` and the two #: codes span roles; the three role contracts are single-role by nature. DOCUMENT_KIND_ROLES = { 'player': ('player',), 'coach': ('coach',), 'manager': ('manager',), 'team': ('player', 'coach', 'manager'), 'code_of_conduct': ('player', 'coach', 'manager'), 'ethics_code': ('player', 'coach', 'manager'), } #: Possible values for DocumentAssignment.status. ASSIGNMENT_STATUSES = ('pending', 'signed', 'acknowledged') class Document(db.Model): """A canonical document, stored once, completed by eligible people.""" __tablename__ = 'documents' id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(255), nullable=False) kind = db.Column(db.String(30), nullable=False, default='player') #: Admins choose when the document is downloadable by team members. The #: old ``document_uploads_enabled`` global toggle is replaced by this #: per-document switch for the new flow. is_published = db.Column(db.Boolean, nullable=False, default=False) current_version_id = db.Column(db.Integer, db.ForeignKey('document_versions.id'), nullable=True) created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) created_at = db.Column(db.DateTime, default=utc_now_naive) versions = db.relationship( 'DocumentVersion', backref='document', lazy='dynamic', order_by='DocumentVersion.version', foreign_keys='DocumentVersion.document_id', ) current_version = db.relationship( 'DocumentVersion', foreign_keys=[current_version_id], uselist=False, ) assignments = db.relationship( 'DocumentAssignment', backref='assigned_document', lazy='dynamic', order_by='DocumentAssignment.created_at', foreign_keys='DocumentAssignment.document_id', ) creator = db.relationship('User', foreign_keys=[created_by]) @property def requires_signature(self): """Whether completion means an uploaded signed PDF.""" return self.kind in SIGNATURE_KINDS def completion_counts(self): """(completed, pending) over every assignment of this document. ``completed`` covers both signed and acknowledged assignments. """ rows = ( db.session.query(DocumentAssignment.status, db.func.count()) .filter(DocumentAssignment.document_id == self.id) .group_by(DocumentAssignment.status) .all() ) totals = dict(rows) completed = totals.get('signed', 0) + totals.get('acknowledged', 0) return completed, totals.get('pending', 0) def next_version_number(self): """The version number a freshly published revision should carry.""" latest = self.versions.order_by(DocumentVersion.version.desc()).first() return (latest.version + 1) if latest else 1 class DocumentVersion(db.Model): """One immutable published revision of a document.""" __tablename__ = 'document_versions' id = db.Column(db.Integer, primary_key=True) document_id = db.Column(db.Integer, db.ForeignKey('documents.id'), nullable=False) version = db.Column(db.Integer, nullable=False) original_filename = db.Column(db.String(255), nullable=False) stored_filename = db.Column(db.String(255), nullable=False) #: ``gdrive://`` for Drive storage, or a path relative to the #: document root for the local backend. file_path = db.Column(db.String(500), nullable=False) notes = db.Column(db.Text, nullable=True) published_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) published_at = db.Column(db.DateTime, default=utc_now_naive) publisher = db.relationship('User', foreign_keys=[published_by]) __table_args__ = ( db.UniqueConstraint('document_id', 'version', name='unique_document_version'), ) class DocumentAssignment(db.Model): """One person's obligation to sign or acknowledge a document.""" __tablename__ = 'document_assignments' id = db.Column(db.Integer, primary_key=True) document_id = db.Column(db.Integer, db.ForeignKey('documents.id'), nullable=False) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) completed_version_id = db.Column( db.Integer, db.ForeignKey('document_versions.id'), nullable=True ) status = db.Column(db.String(20), nullable=False, default='pending') signed_filename = db.Column(db.String(255), nullable=True) signed_file_path = db.Column(db.String(500), nullable=True) completed_at = db.Column(db.DateTime, nullable=True) #: The team whose Drive folder files the signed copy. A member on #: several teams still files under one canonical team only. filed_team_id = db.Column(db.Integer, db.ForeignKey('org_teams.id'), nullable=True) created_at = db.Column(db.DateTime, default=utc_now_naive) document = db.relationship('Document', foreign_keys=[document_id]) user = db.relationship('User', foreign_keys=[user_id], backref='document_assignments') completed_version = db.relationship('DocumentVersion', foreign_keys=[completed_version_id]) filed_team = db.relationship('OrgTeam', foreign_keys=[filed_team_id]) __table_args__ = (db.UniqueConstraint('document_id', 'user_id', name='unique_document_user'),) @property def completed(self): return self.status in ('signed', 'acknowledged')