premiere iteration de la gestion des documents
CI - Security, Lint & Tests / validate (push) Failing after 1m20s
CI - Security, Lint & Tests / validate (push) Failing after 1m20s
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
"""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')
|
||||
Reference in New Issue
Block a user