Add basic support for receiving from multiple invoice files

This commit is contained in:
Lance Edgar 2023-01-10 16:46:21 -06:00
parent 2b7ebedb22
commit dfa4178204
10 changed files with 295 additions and 40 deletions

View file

@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2022 Lance Edgar
# Copyright © 2010-2023 Lance Edgar
#
# This file is part of Rattail.
#
@ -51,7 +51,9 @@ from webhelpers2.html import tags, HTML
from tailbone.db import Session
from tailbone.util import raw_datetime, get_form_data, render_markdown
from . import types
from .widgets import ReadonlyWidget, PlainDateWidget, JQueryDateWidget, JQueryTimeWidget
from .widgets import (ReadonlyWidget, PlainDateWidget,
JQueryDateWidget, JQueryTimeWidget,
MultiFileUploadWidget)
from tailbone.exceptions import TailboneJSONFieldError
@ -579,6 +581,10 @@ class Form(object):
node = colander.SchemaNode(nodeinfo, **kwargs)
self.nodes[key] = node
# must explicitly replace node, if we already have a schema
if self.schema:
self.schema[key] = node
def set_type(self, key, type_, **kwargs):
if type_ == 'datetime':
self.set_renderer(key, self.render_datetime)
@ -624,9 +630,18 @@ class Form(object):
if 'required' in kwargs and not kwargs['required']:
kw['missing'] = colander.null
self.set_node(key, colander.SchemaNode(deform.FileData(), **kw))
# must explicitly replace node, if we already have a schema
if self.schema:
self.schema[key] = self.nodes[key]
elif type_ == 'multi_file':
tmpstore = SessionFileUploadTempStore(self.request)
file_node = colander.SchemaNode(deform.FileData(),
name='upload')
kw = {'name': key,
'title': self.get_label(key),
'widget': MultiFileUploadWidget(tmpstore)}
# if 'required' in kwargs and not kwargs['required']:
# kw['missing'] = colander.null
files_node = colander.SequenceSchema(file_node, **kw)
self.set_node(key, files_node)
else:
raise ValueError("unknown type for '{}' field: {}".format(key, type_))
@ -853,25 +868,31 @@ class Form(object):
value = convert(field.cstruct)
return json.dumps(value)
if isinstance(field.schema.typ, deform.FileData):
# TODO: we used to always/only return 'null' here but hopefully
# this also works, to show existing filename when present
if field.cstruct and field.cstruct['filename']:
return json.dumps({'name': field.cstruct['filename']})
return 'null'
if isinstance(field.schema.typ, colander.Set):
if field.cstruct is colander.null:
return '[]'
if field.cstruct is colander.null:
return 'null'
try:
return json.dumps(field.cstruct)
return self.jsonify_value(field.cstruct)
except Exception as error:
raise TailboneJSONFieldError(field.name, error)
def jsonify_value(self, value):
"""
Take a Python value and convert to JSON
"""
if value is colander.null:
return 'null'
if isinstance(value, dfwidget.filedict):
# TODO: we used to always/only return 'null' here but hopefully
# this also works, to show existing filename when present
if value and value['filename']:
return json.dumps({'name': value['filename']})
return 'null'
return json.dumps(value)
def get_error_messages(self, field):
if field.error:
return field.error.messages()

View file

@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2022 Lance Edgar
# Copyright © 2010-2023 Lance Edgar
#
# This file is part of Rattail.
#
@ -289,6 +289,79 @@ class JQueryAutocompleteWidget(dfwidget.AutocompleteInputWidget):
return field.renderer(template, **tmpl_values)
class MultiFileUploadWidget(dfwidget.FileUploadWidget):
"""
Widget to handle multiple (arbitrary number) of file uploads.
"""
template = 'multi_file_upload'
requirements = ()
def deserialize(self, field, pstruct):
if pstruct is colander.null:
return colander.null
# TODO: why is this a thing? pstruct == [b'']
if len(pstruct) == 1 and not pstruct[0]:
return colander.null
files_data = []
for upload in pstruct:
data = self.deserialize_upload(upload)
if data:
files_data.append(data)
if not files_data:
return colander.null
return files_data
def deserialize_upload(self, upload):
# nb. this logic was copied from parent class and adapted
# to allow for multiple files. needs some more love.
uid = None # TODO?
if hasattr(upload, "file"):
# the upload control had a file selected
data = dfwidget.filedict()
data["fp"] = upload.file
filename = upload.filename
# sanitize IE whole-path filenames
filename = filename[filename.rfind("\\") + 1 :].strip()
data["filename"] = filename
data["mimetype"] = upload.type
data["size"] = upload.length
if uid is None:
# no previous file exists
while 1:
uid = self.random_id()
if self.tmpstore.get(uid) is None:
data["uid"] = uid
self.tmpstore[uid] = data
preview_url = self.tmpstore.preview_url(uid)
self.tmpstore[uid]["preview_url"] = preview_url
break
else:
# a previous file exists
data["uid"] = uid
self.tmpstore[uid] = data
preview_url = self.tmpstore.preview_url(uid)
self.tmpstore[uid]["preview_url"] = preview_url
else:
# the upload control had no file selected
if uid is None:
# no previous file exists
return colander.null
else:
# a previous file should exist
data = self.tmpstore.get(uid)
# but if it doesn't, don't blow up
if data is None:
return colander.null
return data
def make_customer_widget(request, **kwargs):
"""
Make a customer widget; will be either autocomplete or dropdown