diff --git a/tailbone/forms/__init__.py b/tailbone/forms/__init__.py
index 7b412dca..a368f2d1 100644
--- a/tailbone/forms/__init__.py
+++ b/tailbone/forms/__init__.py
@@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
-# Copyright © 2010-2017 Lance Edgar
+# Copyright © 2010-2018 Lance Edgar
#
# This file is part of Rattail.
#
@@ -28,4 +28,4 @@ from __future__ import unicode_literals, absolute_import
from . import types
from . import widgets
-from .core import Form
+from .core import Form, SimpleFileImport
diff --git a/tailbone/forms/core.py b/tailbone/forms/core.py
index 04633bbb..dd242041 100644
--- a/tailbone/forms/core.py
+++ b/tailbone/forms/core.py
@@ -881,3 +881,21 @@ class FieldList(list):
def insert_after(self, field, newfield):
i = self.index(field)
self.insert(i + 1, newfield)
+
+
+@colander.deferred
+def upload_widget(node, kw):
+ request = kw['request']
+ tmpstore = SessionFileUploadTempStore(request)
+ return dfwidget.FileUploadWidget(tmpstore)
+
+
+class SimpleFileImport(colander.Schema):
+ """
+ Schema for simple file import. Note that you must bind your ``request``
+ object to this schema, i.e.::
+
+ schema = SimpleFileImport().bind(request=request)
+ """
+ filename = colander.SchemaNode(deform.FileData(),
+ widget=upload_widget)
diff --git a/tailbone/templates/batch/view.mako b/tailbone/templates/batch/view.mako
index 4874d7ef..34c6b406 100644
--- a/tailbone/templates/batch/view.mako
+++ b/tailbone/templates/batch/view.mako
@@ -79,7 +79,7 @@
${self.context_menu_items()}
-% if status_breakdown is not Undefined:
+% if status_breakdown is not Undefined and status_breakdown is not None:
Row Status Breakdown
diff --git a/tailbone/templates/master/import_file.mako b/tailbone/templates/master/import_file.mako
new file mode 100644
index 00000000..0dd03754
--- /dev/null
+++ b/tailbone/templates/master/import_file.mako
@@ -0,0 +1,6 @@
+## -*- coding: utf-8; -*-
+<%inherit file="/master/create.mako" />
+
+<%def name="title()">Import ${model_title_plural} from ${importer_host_title}%def>
+
+${parent.body()}
diff --git a/tailbone/views/batch/importer.py b/tailbone/views/batch/importer.py
index 0aba98c1..b2803183 100644
--- a/tailbone/views/batch/importer.py
+++ b/tailbone/views/batch/importer.py
@@ -103,15 +103,23 @@ class ImporterBatchView(BatchMasterView):
f.set_readonly('importer_key')
f.set_readonly('row_table')
+ def make_status_breakdown(self, batch):
+ # TODO: should implement this, just can't use batch.data_rows apparently
+ pass
+
def delete_instance(self, batch):
self.make_row_table(batch.row_table)
- self.current_row_table.drop()
+ if self.current_row_table is not None:
+ self.current_row_table.drop()
super(ImporterBatchView, self).delete_instance(batch)
def make_row_table(self, name):
if not hasattr(self, 'current_row_table'):
metadata = sa.MetaData(schema='batch', bind=self.Session.bind)
- self.current_row_table = sa.Table(name, metadata, autoload=True)
+ try:
+ self.current_row_table = sa.Table(name, metadata, autoload=True)
+ except sa.exc.NoSuchTableError:
+ self.current_row_table = None
def get_row_data(self, batch):
self.make_row_table(batch.row_table)
diff --git a/tailbone/views/master.py b/tailbone/views/master.py
index 2283faee..8d3af454 100644
--- a/tailbone/views/master.py
+++ b/tailbone/views/master.py
@@ -89,6 +89,7 @@ class MasterView(View):
execute_progress_template = None
execute_progress_initial_msg = None
supports_prev_next = False
+ supports_import_batch_from_file = False
supports_mobile = False
mobile_creatable = False
@@ -705,7 +706,11 @@ class MasterView(View):
if isinstance(node.typ, deform.FileData):
if skip and node.name in skip:
continue
- filedict = self.form_deserialized.get(node.name)
+ # TODO: does form ever *not* have 'validated' attr here?
+ if hasattr(form, 'validated'):
+ filedict = form.validated.get(node.name)
+ else:
+ filedict = self.form_deserialized.get(node.name)
if filedict:
tempdir = tempfile.mkdtemp()
filepath = os.path.join(tempdir, filedict['filename'])
@@ -722,6 +727,38 @@ class MasterView(View):
def process_uploads(self, obj, form, uploads):
pass
+ def import_batch_from_file(self, handler_factory, model_name,
+ delete=False, schema=None, importer_host_title=None):
+
+ handler = handler_factory(self.rattail_config)
+
+ if not schema:
+ schema = forms.SimpleFileImport().bind(request=self.request)
+ form = forms.Form(schema=schema, request=self.request)
+ form.save_label = "Upload"
+ form.cancel_url = self.get_index_url()
+ if form.validate(newstyle=True):
+
+ uploads = self.normalize_uploads(form)
+ filepath = uploads['filename']['temp_path']
+ batches = handler.make_batches(model_name,
+ delete=delete,
+ # tdc_input_path=filepath,
+ # source_csv_path=filepath,
+ source_data_path=filepath,
+ runas_user=self.request.user)
+ batch = batches[0]
+ return self.redirect(self.request.route_url('batch.importer.view', uuid=batch.uuid))
+
+ if not importer_host_title:
+ importer_host_title = handler.host_title
+
+ return self.render_to_response('import_file', {
+ 'form': form,
+ 'dform': form.make_deform_form(),
+ 'importer_host_title': importer_host_title,
+ })
+
def render_product_key_value(self, obj):
"""
Render the "canonical" product key value for the given object.
@@ -3087,6 +3124,11 @@ class MasterView(View):
config.add_tailbone_permission(permission_prefix, '{0}.delete'.format(permission_prefix),
"Delete {0}".format(model_title))
+ # import batch from file
+ if cls.supports_import_batch_from_file:
+ config.add_tailbone_permission(permission_prefix, '{}.import_file'.format(permission_prefix),
+ "Create a new import batch from data file")
+
### sub-rows stuff follows
# download row results as CSV