feat: the server checks the grade against the files before approving
All checks were successful
Build and deploy / Validate source (push) Successful in 1m23s
Build and deploy / Integration suite on a real stack (push) Successful in 3m13s
Build and deploy / Secret scan and release gate (push) Successful in 10s
Build and deploy / Publish images (push) Successful in 1m25s
All checks were successful
Build and deploy / Validate source (push) Successful in 1m23s
Build and deploy / Integration suite on a real stack (push) Successful in 3m13s
Build and deploy / Secret scan and release gate (push) Successful in 10s
Build and deploy / Publish images (push) Successful in 1m25s
The price depends on the grade, which the browser worked out and the API took on trust. Before a cart is approved at checkout the API now recomputes it from the uploaded files by the Site's own rules: the pixel size in a PNG, JPG or WebP header across the printed width (rotation included), and the area-weighted DPI of the images placed in a PDF of up to 150 MB, 300 for vectors. Sheets take the worst grade, artworks the average. A claim more than 2 points above the file's grade, or a discount on a file the server cannot grade, waits for an operator, with the reason on the Kanban. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -175,7 +175,11 @@ def run():
|
||||
assert not client.call('/quotes/'+qid)['auto_approved']
|
||||
# A cart the Site priced is approved at once and can be paid straight away,
|
||||
# at the server's own prices; no operator can then change it.
|
||||
small={**draft,'request_key':str(uuid4()),'items':[item_spec('avulsa','2.75',90,uid)]}
|
||||
# The server works the grade out from the file: this PNG is 6059 px across
|
||||
# 57 cm, 270 DPI, which is grade 90.
|
||||
png=b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR'+(6059).to_bytes(4,'big')+(2000).to_bytes(4,'big')+b'\x08\x06\x00\x00\x00'+b'\x00'*4
|
||||
graded=upload_bytes(client,png,name='arte-270dpi.png')
|
||||
small={**draft,'request_key':str(uuid4()),'items':[item_spec('avulsa','2.75',90,graded)]}
|
||||
auto=client.call('/quotes',small)
|
||||
assert auto['status']=='approved'
|
||||
seen=client.call('/quotes/'+auto['id'])
|
||||
@@ -188,6 +192,12 @@ def run():
|
||||
held=client.call('/quotes',{**small,'request_key':str(uuid4()),'items':[claimed]})
|
||||
assert held['status']=='pending_review'
|
||||
assert client.call('/quotes/'+held['id'])['review_reason']=='Nota informada sem análise da arte'
|
||||
# A better grade than the file supports, or one the server cannot check, waits too.
|
||||
inflated=client.call('/quotes',{**small,'request_key':str(uuid4()),'items':[item_spec('avulsa','2.75',100,graded)]})
|
||||
assert inflated['status']=='pending_review'
|
||||
assert client.call('/quotes/'+inflated['id'])['review_reason']=='Nota 100 maior que a do arquivo (90)'
|
||||
unchecked=client.call('/quotes',{**small,'request_key':str(uuid4()),'items':[item_spec('avulsa','2.75',90,uid)]})
|
||||
assert client.call('/quotes/'+unchecked['id'])['review_reason']=='Nota não conferida no servidor'
|
||||
print('PASS: priced carts are approved at checkout; large or inconsistent ones wait for review')
|
||||
client.call('/orders/dev-paid',{'quote_id':qid,'total_cents':1},expected=422)
|
||||
other.call('/orders/dev-paid',{'quote_id':qid},expected=404)
|
||||
|
||||
86
tests/test_grade_check.py
Normal file
86
tests/test_grade_check.py
Normal file
@@ -0,0 +1,86 @@
|
||||
"""The server's grade against real image and PDF files.
|
||||
|
||||
Runs where Pillow and pikepdf are installed (the API image).
|
||||
"""
|
||||
import io
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import pikepdf
|
||||
from PIL import Image
|
||||
|
||||
from app import grade_check
|
||||
|
||||
|
||||
class FakeFiles:
|
||||
def __init__(self, blobs):
|
||||
self.blobs = blobs
|
||||
|
||||
def head(self, row):
|
||||
return self.blobs[row['name']][:grade_check.HEAD_BYTES]
|
||||
|
||||
def pdf_dpi(self, row):
|
||||
with tempfile.NamedTemporaryFile(suffix='.pdf') as handle:
|
||||
handle.write(self.blobs[row['name']])
|
||||
handle.flush()
|
||||
return grade_check.pdf_dpi(handle.name)
|
||||
|
||||
|
||||
def encoded(fmt, size, **options):
|
||||
out = io.BytesIO()
|
||||
Image.new('RGBA' if fmt in ('PNG', 'WEBP') else 'RGB', size, (200, 30, 30)).save(out, fmt, **options)
|
||||
return out.getvalue()
|
||||
|
||||
|
||||
def item(kind, grade, sources):
|
||||
return {'grade': grade, 'production': {'sources': [
|
||||
{'upload_id': name, 'kind': kind, 'width_cm': width, 'rotation_degrees': rotation}
|
||||
for name, width, rotation in sources]}}
|
||||
|
||||
|
||||
class GradeCheckTests(unittest.TestCase):
|
||||
def test_image_sizes_from_the_first_bytes(self):
|
||||
for fmt, options in (('PNG', {}), ('JPEG', {'quality': 80}), ('JPEG', {'progressive': True}),
|
||||
('WEBP', {'lossless': True}), ('WEBP', {'quality': 80})):
|
||||
self.assertEqual(grade_check.image_size(encoded(fmt, (1234, 567), **options)), (1234, 567), (fmt, options))
|
||||
# A JPEG with a large metadata block before the frame header.
|
||||
exif = Image.Exif()
|
||||
exif[0x010E] = 'x' * 60000
|
||||
self.assertEqual(grade_check.image_size(encoded('JPEG', (800, 600), exif=exif)), (800, 600))
|
||||
self.assertIsNone(grade_check.image_size(b'not an image at all'))
|
||||
|
||||
def test_pdf_dpi_is_the_images_area_weighted(self):
|
||||
out = io.BytesIO()
|
||||
Image.new('RGB', (1500, 750), 'white').save(out, 'PDF', resolution=150)
|
||||
with tempfile.NamedTemporaryFile(suffix='.pdf') as handle:
|
||||
handle.write(out.getvalue()); handle.flush()
|
||||
self.assertEqual(grade_check.pdf_dpi(handle.name), 150)
|
||||
vector = pikepdf.new()
|
||||
vector.add_blank_page(page_size=(1615, 850))
|
||||
with tempfile.NamedTemporaryFile(suffix='.pdf') as handle:
|
||||
vector.save(handle.name)
|
||||
self.assertEqual(grade_check.pdf_dpi(handle.name), 300)
|
||||
|
||||
def test_grades_follow_the_site(self):
|
||||
# 57 cm is 22.44 in: 6059 px is 270 DPI, grade 90; 3366 px is 150 DPI, grade 50.
|
||||
files = FakeFiles({'a.png': encoded('PNG', (6059, 100)), 'b.jpg': encoded('JPEG', (3366, 100)),
|
||||
'tall.png': encoded('PNG', (100, 2362)), 'x.cdr': b'CDR'})
|
||||
rows = {n: {'name': n, 'size': 1} for n in files.blobs}
|
||||
# A sheet takes the worst sheet's grade.
|
||||
self.assertEqual(grade_check.item_grade(files, item('sheet', 0, [('a.png', 57, 0)]), rows), 90)
|
||||
self.assertEqual(grade_check.item_grade(files, item('sheet', 0, [('a.png', 57, 0), ('b.jpg', 57, 0)]), rows), 50)
|
||||
# Artworks average; a rotated one is measured along its height.
|
||||
self.assertEqual(grade_check.item_grade(files, item('artwork', 0, [('a.png', 57, 0), ('b.jpg', 57, 0)]), rows), 70)
|
||||
self.assertEqual(grade_check.item_grade(files, item('artwork', 0, [('tall.png', 20, 90)]), rows), 100)
|
||||
self.assertIsNone(grade_check.item_grade(files, item('sheet', 0, [('x.cdr', 57, 0)]), rows))
|
||||
# What the cart claims is checked, with a point or two of rounding.
|
||||
claim = lambda g, src: grade_check.mismatch(files, [item('sheet', g, src)], rows)
|
||||
self.assertIsNone(claim(90, [('a.png', 57, 0)]))
|
||||
self.assertIsNone(claim(92, [('a.png', 57, 0)]))
|
||||
self.assertEqual(claim(100, [('a.png', 57, 0)]), 'Nota 100 maior que a do arquivo (90)')
|
||||
self.assertEqual(claim(40, [('x.cdr', 57, 0)]), 'Nota não conferida no servidor')
|
||||
self.assertIsNone(claim(0, [('x.cdr', 57, 0)]))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user