174 lines
6.9 KiB
Python
174 lines
6.9 KiB
Python
"""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://<file-id>`` 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')
|