Improve data file handling for file batches.
Leverages a FormAlchemy "extension" of sorts.
This commit is contained in:
		
							parent
							
								
									2e8db05717
								
							
						
					
					
						commit
						3614254804
					
				
					 4 changed files with 103 additions and 41 deletions
				
			
		
							
								
								
									
										64
									
								
								tailbone/forms/renderers/batch.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								tailbone/forms/renderers/batch.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,64 @@
 | 
				
			||||||
 | 
					# -*- coding: utf-8 -*-
 | 
				
			||||||
 | 
					################################################################################
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Rattail -- Retail Software Framework
 | 
				
			||||||
 | 
					#  Copyright © 2010-2015 Lance Edgar
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  This file is part of Rattail.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Rattail is free software: you can redistribute it and/or modify it under the
 | 
				
			||||||
 | 
					#  terms of the GNU Affero General Public License as published by the Free
 | 
				
			||||||
 | 
					#  Software Foundation, either version 3 of the License, or (at your option)
 | 
				
			||||||
 | 
					#  any later version.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
 | 
				
			||||||
 | 
					#  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 | 
				
			||||||
 | 
					#  FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for
 | 
				
			||||||
 | 
					#  more details.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  You should have received a copy of the GNU Affero General Public License
 | 
				
			||||||
 | 
					#  along with Rattail.  If not, see <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					################################################################################
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					Batch Field Renderers
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from __future__ import unicode_literals
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					import stat
 | 
				
			||||||
 | 
					import random
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from formalchemy.ext import fsblob
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class FileFieldRenderer(fsblob.FileFieldRenderer):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Custom file field renderer for batches based on a single source data file.
 | 
				
			||||||
 | 
					    In edit mode, shows a file upload field.  In readonly mode, shows the
 | 
				
			||||||
 | 
					    filename and its size.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def new(cls, view):
 | 
				
			||||||
 | 
					        name = 'Configured%s_%s' % (cls.__name__, str(random.random())[2:])
 | 
				
			||||||
 | 
					        return type(str(name), (cls,), dict(view=view))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def storage_path(self):
 | 
				
			||||||
 | 
					        return self.view.upload_dir
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_size(self):
 | 
				
			||||||
 | 
					        size = super(FileFieldRenderer, self).get_size()
 | 
				
			||||||
 | 
					        if size:
 | 
				
			||||||
 | 
					            return size
 | 
				
			||||||
 | 
					        batch = self.field.parent.model
 | 
				
			||||||
 | 
					        path = os.path.join(self.view.handler.datadir(batch), self.field.value)
 | 
				
			||||||
 | 
					        if os.path.isfile(path):
 | 
				
			||||||
 | 
					            return os.stat(path)[stat.ST_SIZE]
 | 
				
			||||||
 | 
					        return 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_url(self, filename):
 | 
				
			||||||
 | 
					        batch = self.field.parent.model
 | 
				
			||||||
 | 
					        return self.view.request.route_url('{0}.download'.format(self.view.route_prefix), uuid=batch.uuid)
 | 
				
			||||||
| 
						 | 
					@ -46,6 +46,7 @@ from rattail.threads import Thread
 | 
				
			||||||
from tailbone.db import Session
 | 
					from tailbone.db import Session
 | 
				
			||||||
from tailbone.views import SearchableAlchemyGridView, CrudView
 | 
					from tailbone.views import SearchableAlchemyGridView, CrudView
 | 
				
			||||||
from tailbone.forms import DateTimeFieldRenderer, UserFieldRenderer, EnumFieldRenderer
 | 
					from tailbone.forms import DateTimeFieldRenderer, UserFieldRenderer, EnumFieldRenderer
 | 
				
			||||||
 | 
					from tailbone.forms.renderers.batch import FileFieldRenderer
 | 
				
			||||||
from tailbone.grids.search import BooleanSearchFilter, EnumSearchFilter
 | 
					from tailbone.grids.search import BooleanSearchFilter, EnumSearchFilter
 | 
				
			||||||
from tailbone.progress import SessionProgress
 | 
					from tailbone.progress import SessionProgress
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -436,6 +437,7 @@ class BatchCrud(BaseCrud):
 | 
				
			||||||
        Redirect to view batch after creating a batch.
 | 
					        Redirect to view batch after creating a batch.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        batch = form.fieldset.model
 | 
					        batch = form.fieldset.model
 | 
				
			||||||
 | 
					        Session.flush()
 | 
				
			||||||
        return self.view_url(batch.uuid)
 | 
					        return self.view_url(batch.uuid)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def post_update_url(self, form):
 | 
					    def post_update_url(self, form):
 | 
				
			||||||
| 
						 | 
					@ -561,28 +563,6 @@ class BatchCrud(BaseCrud):
 | 
				
			||||||
        return HTTPFound(location=self.view_url(batch.uuid))
 | 
					        return HTTPFound(location=self.view_url(batch.uuid))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class DownloadLinkRenderer(formalchemy.FieldRenderer):
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    Field renderer for batch filenames, shows a link to download the file.
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __init__(self, route_prefix):
 | 
					 | 
				
			||||||
        self.route_prefix = route_prefix
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __call__(self, field):
 | 
					 | 
				
			||||||
        super(DownloadLinkRenderer, self).__init__(field)
 | 
					 | 
				
			||||||
        return self
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def render_readonly(self, **kwargs):
 | 
					 | 
				
			||||||
        filename = self.value
 | 
					 | 
				
			||||||
        if not filename:
 | 
					 | 
				
			||||||
            return ''
 | 
					 | 
				
			||||||
        batch = self.field.parent.model
 | 
					 | 
				
			||||||
        return link_to(filename, self.request.route_url(
 | 
					 | 
				
			||||||
                '{0}.download'.format(self.route_prefix),
 | 
					 | 
				
			||||||
                uuid=batch.uuid))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class FileBatchCrud(BatchCrud):
 | 
					class FileBatchCrud(BatchCrud):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Base CRUD view for batches which involve a file upload as the first step.
 | 
					    Base CRUD view for batches which involve a file upload as the first step.
 | 
				
			||||||
| 
						 | 
					@ -597,6 +577,21 @@ class FileBatchCrud(BatchCrud):
 | 
				
			||||||
            return HTTPFound(location=self.request.route_url(
 | 
					            return HTTPFound(location=self.request.route_url(
 | 
				
			||||||
                    '{0}.refresh'.format(self.route_prefix), uuid=batch.uuid))
 | 
					                    '{0}.refresh'.format(self.route_prefix), uuid=batch.uuid))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def upload_dir(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        The path to the root upload folder, to be used as the ``storage_path``
 | 
				
			||||||
 | 
					        argument for the file field renderer.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        uploads = os.path.join(
 | 
				
			||||||
 | 
					            self.request.rattail_config.require('rattail', 'batch.files'),
 | 
				
			||||||
 | 
					            'uploads')
 | 
				
			||||||
 | 
					        uploads = self.request.rattail_config.get(
 | 
				
			||||||
 | 
					            'tailbone', 'batch.uploads', default=uploads)
 | 
				
			||||||
 | 
					        if not os.path.exists(uploads):
 | 
				
			||||||
 | 
					            os.makedirs(uploads)
 | 
				
			||||||
 | 
					        return uploads
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def fieldset(self, model):
 | 
					    def fieldset(self, model):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Creates the fieldset for the view.  Derived classes should *not*
 | 
					        Creates the fieldset for the view.  Derived classes should *not*
 | 
				
			||||||
| 
						 | 
					@ -609,14 +604,11 @@ class FileBatchCrud(BatchCrud):
 | 
				
			||||||
        fs.cognized_by.set(label="Cognized by", renderer=UserFieldRenderer)
 | 
					        fs.cognized_by.set(label="Cognized by", renderer=UserFieldRenderer)
 | 
				
			||||||
        fs.executed.set(renderer=DateTimeFieldRenderer(self.request.rattail_config))
 | 
					        fs.executed.set(renderer=DateTimeFieldRenderer(self.request.rattail_config))
 | 
				
			||||||
        fs.executed_by.set(label="Executed by", renderer=UserFieldRenderer)
 | 
					        fs.executed_by.set(label="Executed by", renderer=UserFieldRenderer)
 | 
				
			||||||
        fs.append(formalchemy.Field('data_file'))
 | 
					        fs.filename.set(renderer=FileFieldRenderer.new(self), label="Data File")
 | 
				
			||||||
        fs.data_file.set(renderer=formalchemy.fields.FileFieldRenderer)
 | 
					 | 
				
			||||||
        fs.filename.set(renderer=DownloadLinkRenderer(self.route_prefix), readonly=True)
 | 
					 | 
				
			||||||
        self.configure_fieldset(fs)
 | 
					        self.configure_fieldset(fs)
 | 
				
			||||||
        if self.creating:
 | 
					        if self.creating:
 | 
				
			||||||
            del fs.created
 | 
					            del fs.created
 | 
				
			||||||
            del fs.created_by
 | 
					            del fs.created_by
 | 
				
			||||||
            del fs.filename
 | 
					 | 
				
			||||||
            if 'cognized' in fs.render_fields:
 | 
					            if 'cognized' in fs.render_fields:
 | 
				
			||||||
                del fs.cognized
 | 
					                del fs.cognized
 | 
				
			||||||
            if 'cognized_by' in fs.render_fields:
 | 
					            if 'cognized_by' in fs.render_fields:
 | 
				
			||||||
| 
						 | 
					@ -628,8 +620,8 @@ class FileBatchCrud(BatchCrud):
 | 
				
			||||||
            if 'data_rows' in fs.render_fields:
 | 
					            if 'data_rows' in fs.render_fields:
 | 
				
			||||||
                del fs.data_rows
 | 
					                del fs.data_rows
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            if 'data_file' in fs.render_fields:
 | 
					            if self.updating and 'filename' in fs.render_fields:
 | 
				
			||||||
                del fs.data_file
 | 
					                fs.filename.set(readonly=True)
 | 
				
			||||||
            batch = fs.model
 | 
					            batch = fs.model
 | 
				
			||||||
            if not batch.executed:
 | 
					            if not batch.executed:
 | 
				
			||||||
                if 'executed' in fs.render_fields:
 | 
					                if 'executed' in fs.render_fields:
 | 
				
			||||||
| 
						 | 
					@ -648,7 +640,6 @@ class FileBatchCrud(BatchCrud):
 | 
				
			||||||
            include=[
 | 
					            include=[
 | 
				
			||||||
                fs.created,
 | 
					                fs.created,
 | 
				
			||||||
                fs.created_by,
 | 
					                fs.created_by,
 | 
				
			||||||
                fs.data_file,
 | 
					 | 
				
			||||||
                fs.filename,
 | 
					                fs.filename,
 | 
				
			||||||
                # fs.cognized,
 | 
					                # fs.cognized,
 | 
				
			||||||
                # fs.cognized_by,
 | 
					                # fs.cognized_by,
 | 
				
			||||||
| 
						 | 
					@ -669,13 +660,23 @@ class FileBatchCrud(BatchCrud):
 | 
				
			||||||
        # For new batches, assign current user as creator, save file etc.
 | 
					        # For new batches, assign current user as creator, save file etc.
 | 
				
			||||||
        if self.creating:
 | 
					        if self.creating:
 | 
				
			||||||
            batch.created_by = self.request.user
 | 
					            batch.created_by = self.request.user
 | 
				
			||||||
            batch.filename = form.fieldset.data_file.renderer._filename
 | 
					
 | 
				
			||||||
            # Expunge batch from session to prevent it from being flushed.
 | 
					            # Expunge batch from session to prevent it from being flushed
 | 
				
			||||||
 | 
					            # during init.  This is done as a convenience to views which
 | 
				
			||||||
 | 
					            # provide an init method.  Some batches may have required fields
 | 
				
			||||||
 | 
					            # which aren't filled in yet, but the view may need to query the
 | 
				
			||||||
 | 
					            # database to obtain the values.  This will cause a session flush,
 | 
				
			||||||
 | 
					            # and the missing fields will trigger data integrity errors.
 | 
				
			||||||
            Session.expunge(batch)
 | 
					            Session.expunge(batch)
 | 
				
			||||||
            self.batch_inited = self.init_batch(batch)
 | 
					            self.batch_inited = self.init_batch(batch)
 | 
				
			||||||
            if self.batch_inited:
 | 
					            if self.batch_inited:
 | 
				
			||||||
                Session.add(batch)
 | 
					                Session.add(batch)
 | 
				
			||||||
                batch.write_file(self.request.rattail_config, form.fieldset.data_file.value)
 | 
					                Session.flush()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # Handler saves a copy of the file and updates the batch filename.
 | 
				
			||||||
 | 
					                path = os.path.join(self.upload_dir, batch.filename)
 | 
				
			||||||
 | 
					                self.handler.set_data_file(batch, path)
 | 
				
			||||||
 | 
					                os.remove(path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def init_batch(self, batch):
 | 
					    def init_batch(self, batch):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
| 
						 | 
					@ -716,12 +717,11 @@ class FileBatchCrud(BatchCrud):
 | 
				
			||||||
        batch = self.current_batch()
 | 
					        batch = self.current_batch()
 | 
				
			||||||
        if not batch:
 | 
					        if not batch:
 | 
				
			||||||
            return HTTPNotFound()
 | 
					            return HTTPNotFound()
 | 
				
			||||||
        config = self.request.rattail_config
 | 
					        path = self.handler.data_path(batch)
 | 
				
			||||||
        path = batch.filepath(config)
 | 
					 | 
				
			||||||
        response = FileResponse(path, request=self.request)
 | 
					        response = FileResponse(path, request=self.request)
 | 
				
			||||||
        response.headers[b'Content-Length'] = str(batch.filesize(config))
 | 
					        response.headers[b'Content-Length'] = str(os.path.getsize(path))
 | 
				
			||||||
        response.headers[b'Content-Disposition'] = b'attachment; filename="{0}"'.format(
 | 
					        filename = os.path.basename(batch.filename).encode('ascii', 'replace')
 | 
				
			||||||
            batch.filename.encode('ascii', 'replace'))
 | 
					        response.headers[b'Content-Disposition'] = b'attachment; filename="{0}"'.format(filename)
 | 
				
			||||||
        return response
 | 
					        return response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										3
									
								
								tailbone/views/vendors/catalogs.py
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								tailbone/views/vendors/catalogs.py
									
										
									
									
										vendored
									
									
								
							| 
						 | 
					@ -114,10 +114,9 @@ class VendorCatalogCrud(FileBatchCrud):
 | 
				
			||||||
        fs.configure(
 | 
					        fs.configure(
 | 
				
			||||||
            include=[
 | 
					            include=[
 | 
				
			||||||
                fs.vendor,
 | 
					                fs.vendor,
 | 
				
			||||||
                fs.data_file.label("Catalog File"),
 | 
					                fs.filename.label("Catalog File"),
 | 
				
			||||||
                fs.parser_key.label("File Type"),
 | 
					                fs.parser_key.label("File Type"),
 | 
				
			||||||
                fs.effective,
 | 
					                fs.effective,
 | 
				
			||||||
                fs.filename,
 | 
					 | 
				
			||||||
                fs.created,
 | 
					                fs.created,
 | 
				
			||||||
                fs.created_by,
 | 
					                fs.created_by,
 | 
				
			||||||
                fs.executed,
 | 
					                fs.executed,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										3
									
								
								tailbone/views/vendors/invoices.py
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								tailbone/views/vendors/invoices.py
									
										
									
									
										vendored
									
									
								
							| 
						 | 
					@ -114,9 +114,8 @@ class VendorInvoiceCrud(FileBatchCrud):
 | 
				
			||||||
        fs.configure(
 | 
					        fs.configure(
 | 
				
			||||||
            include=[
 | 
					            include=[
 | 
				
			||||||
                fs.vendor.readonly(),
 | 
					                fs.vendor.readonly(),
 | 
				
			||||||
                fs.data_file.label("Invoice File"),
 | 
					                fs.filename.label("Invoice File"),
 | 
				
			||||||
                fs.parser_key.label("File Type"),
 | 
					                fs.parser_key.label("File Type"),
 | 
				
			||||||
                fs.filename,
 | 
					 | 
				
			||||||
                fs.purchase_order_number.label(self.handler.po_number_title),
 | 
					                fs.purchase_order_number.label(self.handler.po_number_title),
 | 
				
			||||||
                fs.invoice_date.readonly(),
 | 
					                fs.invoice_date.readonly(),
 | 
				
			||||||
                fs.created,
 | 
					                fs.created,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue