83 lines
3.1 KiB
Python
83 lines
3.1 KiB
Python
"""Operator issue reports (screenshot + free-text notes from the tray).
|
|
|
|
An operator on a shop-floor PC opens the tray "Report an issue" dialog, writes a
|
|
free-text description, and optionally attaches a screenshot of their screen. The
|
|
agent uploads that to ShopDB, where it becomes one OperatorReport row linked to
|
|
the reporting PC's asset. The screenshot bytes live on disk under the instance
|
|
uploads dir (like employee photos); this row holds only metadata plus the
|
|
relative file path so the image can be served back and purged on retention.
|
|
|
|
Reports are transient by design: a retention window (default 7 days) purges old
|
|
rows and their image files, so this is a rolling operator-signal feed, not a
|
|
permanent record.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
|
|
from shopdb.api import db
|
|
|
|
|
|
def _utciso(value):
|
|
"""Naive-UTC datetime -> ISO string marked UTC, or None."""
|
|
return value.isoformat() + 'Z' if value else None
|
|
|
|
|
|
class OperatorReport(db.Model):
|
|
"""One operator-submitted issue report for a PC asset."""
|
|
|
|
__tablename__ = 'operatorreports'
|
|
|
|
operatorreportid = db.Column(db.Integer, primary_key=True)
|
|
|
|
# The PC this report is about. ON DELETE CASCADE: when the asset goes, its
|
|
# operator reports go with it.
|
|
assetid = db.Column(
|
|
db.Integer,
|
|
db.ForeignKey('assets.assetid', ondelete='CASCADE'),
|
|
nullable=False,
|
|
)
|
|
|
|
# The dropdown reason. Only 'Other (free text)' is functional today, but the
|
|
# column is kept so the richer reason routing can return without a migration.
|
|
reason = db.Column(db.String(128), nullable=False, default='Other (free text)')
|
|
|
|
# The operator's free-text description. Nullable: a screenshot alone is a
|
|
# valid report.
|
|
details = db.Column(db.Text, nullable=True)
|
|
|
|
# Screenshot metadata. imagefilename is the stored file name under the
|
|
# operator-reports uploads dir; null when the operator declined a screenshot.
|
|
imagefilename = db.Column(db.String(255), nullable=True)
|
|
imagemimetype = db.Column(db.String(64), nullable=True)
|
|
imagebytesize = db.Column(db.Integer, nullable=True)
|
|
|
|
# The hostname the agent reported at upload time, kept for traceability even
|
|
# if the asset link is later re-pointed.
|
|
reportedhostname = db.Column(db.String(255), nullable=True)
|
|
|
|
createdat = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
|
|
|
|
__table_args__ = (
|
|
db.Index('idx_operatorreports_asset', 'assetid'),
|
|
db.Index('idx_operatorreports_createdat', 'createdat'),
|
|
)
|
|
|
|
def __repr__(self):
|
|
return f"<OperatorReport {self.operatorreportid} asset={self.assetid}>"
|
|
|
|
def has_image(self):
|
|
return bool(self.imagefilename)
|
|
|
|
def to_dict(self):
|
|
return {
|
|
'operatorreportid': self.operatorreportid,
|
|
'assetid': self.assetid,
|
|
'reason': self.reason,
|
|
'details': self.details,
|
|
'hasimage': self.has_image(),
|
|
'imagemimetype': self.imagemimetype,
|
|
'imagebytesize': self.imagebytesize,
|
|
'reportedhostname': self.reportedhostname,
|
|
'createdat': _utciso(self.createdat),
|
|
}
|