diff --git a/tailbone/forms/core.py b/tailbone/forms/core.py index 06bf96e4..2c23b126 100644 --- a/tailbone/forms/core.py +++ b/tailbone/forms/core.py @@ -24,6 +24,7 @@ Forms Core """ +import hashlib import json import logging import warnings @@ -659,11 +660,25 @@ class Form(object): 'widget': MultiFileUploadWidget(tmpstore)} # if 'required' in kwargs and not kwargs['required']: # kw['missing'] = colander.null + if kwargs.get('validate_unique'): + kw['validator'] = self.validate_multiple_files_unique files_node = colander.SequenceSchema(file_node, **kw) self.set_node(key, files_node) else: raise ValueError("unknown type for '{}' field: {}".format(key, type_)) + def validate_multiple_files_unique(self, node, value): + + # get SHA256 hash for each file; error if duplicates encountered + hashes = {} + for fileinfo in value: + fp = fileinfo['fp'] + fp.seek(0) + filehash = hashlib.sha256(fp.read()).hexdigest() + if filehash in hashes: + node.raise_invalid(f"Duplicate file detected: {fileinfo['filename']}") + hashes[filehash] = fileinfo + def set_enum(self, key, enum, empty=None): if enum: self.enums[key] = enum @@ -906,6 +921,11 @@ class Form(object): return json.dumps({'name': value['filename']}) return 'null' + elif isinstance(value, list) and all([isinstance(f, dfwidget.filedict) + for f in value]): + return json.dumps([{'name': f['filename']} + for f in value]) + app = self.request.rattail_config.get_app() value = app.json_friendly(value) return json.dumps(value) diff --git a/tailbone/forms/widgets.py b/tailbone/forms/widgets.py index 23bbac00..0b8d3dc9 100644 --- a/tailbone/forms/widgets.py +++ b/tailbone/forms/widgets.py @@ -323,6 +323,21 @@ class MultiFileUploadWidget(dfwidget.FileUploadWidget): template = 'multi_file_upload' requirements = () + def serialize(self, field, cstruct, **kw): + if cstruct in (colander.null, None): + cstruct = [] + + if cstruct: + for fileinfo in cstruct: + uid = fileinfo['uid'] + if uid not in self.tmpstore: + self.tmpstore[uid] = fileinfo + + readonly = kw.get("readonly", self.readonly) + template = readonly and self.readonly_template or self.template + values = self.get_template_values(field, cstruct, kw) + return field.renderer(template, **values) + def deserialize(self, field, pstruct): if pstruct is colander.null: return colander.null diff --git a/tailbone/views/purchasing/receiving.py b/tailbone/views/purchasing/receiving.py index 5ccf6081..9de4baa3 100644 --- a/tailbone/views/purchasing/receiving.py +++ b/tailbone/views/purchasing/receiving.py @@ -570,7 +570,7 @@ class ReceivingBatchView(PurchasingBatchView): elif workflow == 'from_multi_invoice': if 'invoice_files' not in f: f.insert_before('invoice_file', 'invoice_files') - f.set_type('invoice_files', 'multi_file') + f.set_type('invoice_files', 'multi_file', validate_unique=True) f.set_required('invoice_parser_key') f.remove('truck_dump_batch_uuid', 'po_number',