Add basic support for receiving from multiple invoice files
This commit is contained in:
parent
2b7ebedb22
commit
dfa4178204
10 changed files with 295 additions and 40 deletions
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue