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} + +${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