printedparts stage 15: print-file revision history + role-based alerts
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:
@@ -55,7 +55,7 @@ EXPECTED_HEAD_REVISION['employees'] = 'employees0002photo'
|
||||
# usb drops the dead usbcheckouts.machineid column on top of its anchor.
|
||||
EXPECTED_HEAD_REVISION['usb'] = 'usb0002dropmachineid'
|
||||
# printedparts is post-cutover: its 0001 really creates its tables.
|
||||
EXPECTED_HEAD_REVISION['printedparts'] = 'printedparts0001baseline'
|
||||
EXPECTED_HEAD_REVISION['printedparts'] = 'printedparts0002files'
|
||||
# notifications indexes businessunitid on top of its anchor.
|
||||
EXPECTED_HEAD_REVISION['notifications'] = 'notifications0002buidx'
|
||||
|
||||
|
||||
@@ -260,3 +260,69 @@ def test_retire_hides_and_restore_returns(client, auth_headers, item):
|
||||
assert client.post(f'/api/printedparts/items/{item}/restore',
|
||||
headers=auth_headers).status_code == 200
|
||||
assert client.get('/api/printedparts/kiosk/item/3DP-9001').status_code == 200
|
||||
|
||||
|
||||
def test_file_revisions_append_and_download(client, auth_headers, item, tmp_path):
|
||||
"""Uploads mint sequential revisions; download returns the original name."""
|
||||
import io
|
||||
|
||||
first = client.post(f'/api/printedparts/items/{item}/files',
|
||||
data={'file': (io.BytesIO(b'solid part'), 'clip_v1.stl'),
|
||||
'note': 'initial'},
|
||||
headers=auth_headers,
|
||||
content_type='multipart/form-data')
|
||||
assert first.status_code == 201, first.get_json()
|
||||
assert first.get_json()['data']['revision'] == 1
|
||||
|
||||
second = client.post(f'/api/printedparts/items/{item}/files',
|
||||
data={'file': (io.BytesIO(b'G1 X0 Y0'), 'clip_v2.gcode')},
|
||||
headers=auth_headers,
|
||||
content_type='multipart/form-data')
|
||||
assert second.get_json()['data']['revision'] == 2
|
||||
|
||||
bad = client.post(f'/api/printedparts/items/{item}/files',
|
||||
data={'file': (io.BytesIO(b'x'), 'malware.exe')},
|
||||
headers=auth_headers,
|
||||
content_type='multipart/form-data')
|
||||
assert bad.status_code == 400
|
||||
|
||||
listing = client.get(f'/api/printedparts/items/{item}/files').get_json()['data']
|
||||
assert [f['revision'] for f in listing] == [2, 1]
|
||||
|
||||
fileid = listing[1]['fileid']
|
||||
download = client.get(f'/api/printedparts/files/{fileid}/download')
|
||||
assert download.status_code == 200
|
||||
assert download.data == b'solid part'
|
||||
assert 'clip_v1.stl' in download.headers['Content-Disposition']
|
||||
|
||||
|
||||
def test_alert_role_members_receive(client, auth_headers, app, item,
|
||||
directory_employee, monkeypatch):
|
||||
"""Every active member of a selected role gets the alert."""
|
||||
import shopdb.api as contract_surface
|
||||
captured = {}
|
||||
monkeypatch.setattr(contract_surface, 'send_email',
|
||||
lambda to, subject, html, text=None:
|
||||
captured.setdefault('to', to) or True)
|
||||
|
||||
with app.app_context():
|
||||
from shopdb.api import User, Role
|
||||
from werkzeug.security import generate_password_hash
|
||||
role = Role(rolename='partscrew', description='3D parts crew')
|
||||
member = User(username='crewone', email='crewone@site.test',
|
||||
passwordhash=generate_password_hash('x'), isactive=True)
|
||||
member.roles.append(role)
|
||||
db.session.add_all([role, member])
|
||||
db.session.commit()
|
||||
Setting.set('printedparts_alert_roleids', str(role.roleid),
|
||||
valuetype='string', category='printedparts')
|
||||
db.session.commit()
|
||||
|
||||
client.post(f'/api/printedparts/items/{item}/restock',
|
||||
json={'quantity': 10, 'badge': directory_employee},
|
||||
headers=auth_headers)
|
||||
take = client.post('/api/printedparts/kiosk/take',
|
||||
json={'itemcode': '3DP-9001',
|
||||
'badge': directory_employee, 'quantity': 6})
|
||||
assert take.status_code == 200
|
||||
assert captured['to'] == ['crewone@site.test']
|
||||
|
||||
Reference in New Issue
Block a user