printedparts stage 15: print-file revision history + role-based alerts
Some checks failed
CI / backend (push) Successful in 1m44s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 8s
CI / migrations-mysql (push) Failing after 7s

printeditemfiles lands as the plugin's first incremental migration
(0002 on the plugin chain - the ADR-008 payoff). Revisions are
append-only per item: upload assigns the next number, records the
uploader from the JWT, enforces an extension allowlist and a 100 MB
cap; download serves the original filename; a permission-gated delete
covers wrong-file mistakes. The detail page gains the revision table
with a current badge. Unique storedfilename is sized 191 so the index
fits MySQL's 767-byte prefix - the per-plugin chain does not apply the
core env's ROW_FORMAT hook.

Alert recipients gain roles: Role joins the 0.13.0 surface, a role
picker on the settings page, and every active member of the selected
roles is folded into the deduped recipient list.
This commit is contained in:
cproudlock
2026-07-17 08:47:41 -04:00
parent 26b6b6b32f
commit aa4bfcd41c
14 changed files with 479 additions and 11 deletions

View File

@@ -1,5 +1,11 @@
"""Printedparts plugin models."""
from .printeditem import PrintedItem, PrintedItemTransaction, TRANSACTION_TYPES
from .printeditem import (
PrintedItem,
PrintedItemTransaction,
PrintedItemFile,
TRANSACTION_TYPES,
)
__all__ = ['PrintedItem', 'PrintedItemTransaction', 'TRANSACTION_TYPES']
__all__ = ['PrintedItem', 'PrintedItemTransaction', 'PrintedItemFile',
'TRANSACTION_TYPES']

View File

@@ -93,3 +93,43 @@ class PrintedItemTransaction(BaseModel):
'reason': self.reason,
'transactiondate': self.transactiondate.isoformat() + 'Z' if self.transactiondate else None,
}
class PrintedItemFile(BaseModel):
"""One uploaded revision of an item's print file (STL/3MF/gcode/...).
Revisions are append-only per item: uploading assigns the next revision
number and never replaces earlier files, so the history of what was
actually printed stays reconstructible. The current file is simply the
highest revision.
"""
__tablename__ = 'printeditemfiles'
fileid = db.Column(db.Integer, primary_key=True)
printeditemid = db.Column(
db.Integer,
db.ForeignKey('printeditems.printeditemid', ondelete='CASCADE'),
nullable=False, index=True)
revision = db.Column(db.Integer, nullable=False)
filename = db.Column(db.String(255), nullable=False,
comment='Original upload name, used for download')
storedfilename = db.Column(db.String(191), nullable=False, unique=True,
comment='191: unique index fits the 767-byte MySQL prefix')
filesize = db.Column(db.Integer, nullable=False)
uploadnote = db.Column(db.String(255),
comment='What changed in this revision')
uploadedby = db.Column(db.String(80), nullable=False,
comment='Username of the uploader')
def to_dict(self):
return {
'fileid': self.fileid,
'printeditemid': self.printeditemid,
'revision': self.revision,
'filename': self.filename,
'filesize': self.filesize,
'uploadnote': self.uploadnote,
'uploadedby': self.uploadedby,
'uploadeddate': self.createddate.isoformat() + 'Z' if self.createddate else None,
}