| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  | # -*- 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/>. | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | ################################################################################ | 
					
						
							|  |  |  | """
 | 
					
						
							|  |  |  | Base views for maintaining new-style batches. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | .. note:: | 
					
						
							|  |  |  |    This is all still very experimental. | 
					
						
							|  |  |  | """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | from __future__ import unicode_literals | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import os | 
					
						
							|  |  |  | import datetime | 
					
						
							|  |  |  | import logging | 
					
						
							| 
									
										
										
										
											2015-08-19 20:06:13 -05:00
										 |  |  | from cStringIO import StringIO | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | from sqlalchemy import orm | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  | import formalchemy | 
					
						
							|  |  |  | from pyramid.renderers import render_to_response | 
					
						
							| 
									
										
										
										
											2015-02-12 23:19:01 -06:00
										 |  |  | from pyramid.response import FileResponse | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  | from pyramid.httpexceptions import HTTPFound, HTTPNotFound | 
					
						
							| 
									
										
										
										
											2015-02-16 18:00:45 -06:00
										 |  |  | from webhelpers.html.tags import link_to, HTML | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  | from rattail.db import model | 
					
						
							|  |  |  | from rattail.db import Session as RatSession | 
					
						
							|  |  |  | from rattail.threads import Thread | 
					
						
							| 
									
										
										
										
											2015-08-19 20:06:13 -05:00
										 |  |  | from rattail.csvutil import UnicodeDictWriter | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  | from tailbone.db import Session | 
					
						
							|  |  |  | from tailbone.views import SearchableAlchemyGridView, CrudView | 
					
						
							|  |  |  | from tailbone.forms import DateTimeFieldRenderer, UserFieldRenderer, EnumFieldRenderer | 
					
						
							| 
									
										
										
										
											2015-02-22 00:00:00 -06:00
										 |  |  | from tailbone.forms.renderers.batch import FileFieldRenderer | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  | from tailbone.grids.search import BooleanSearchFilter, EnumSearchFilter | 
					
						
							|  |  |  | from tailbone.progress import SessionProgress | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | log = logging.getLogger(__name__) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class BaseGrid(SearchableAlchemyGridView): | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Base view for batch and batch row grid views.  You should not derive from | 
					
						
							|  |  |  |     this class, but :class:`BatchGrid` or :class:`BatchRowGrid` instead. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def config_prefix(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Config prefix for the grid view.  This is used to keep track of current | 
					
						
							|  |  |  |         filtering and sorting, within the user's session.  Derived classes may | 
					
						
							|  |  |  |         override this. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         return self.mapped_class.__name__.lower() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def permission_prefix(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Permission prefix for the grid view.  This is used to automatically | 
					
						
							|  |  |  |         protect certain views common to all batches.  Derived classes can | 
					
						
							|  |  |  |         override this. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         return self.route_prefix | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def join_map_extras(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Derived classes can override this.  The value returned will be used to | 
					
						
							|  |  |  |         supplement the default join map. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         return {} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def filter_map_extras(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Derived classes can override this.  The value returned will be used to | 
					
						
							|  |  |  |         supplement the default filter map. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         return {} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def make_filter_map(self, **kwargs): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Make a filter map by combining kwargs from the base class, with extras | 
					
						
							|  |  |  |         supplied by a derived class. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         extras = self.filter_map_extras() | 
					
						
							|  |  |  |         exact = extras.pop('exact', None) | 
					
						
							|  |  |  |         if exact: | 
					
						
							|  |  |  |             kwargs.setdefault('exact', []).extend(exact) | 
					
						
							|  |  |  |         ilike = extras.pop('ilike', None) | 
					
						
							|  |  |  |         if ilike: | 
					
						
							|  |  |  |             kwargs.setdefault('ilike', []).extend(ilike) | 
					
						
							|  |  |  |         kwargs.update(extras) | 
					
						
							|  |  |  |         return super(BaseGrid, self).make_filter_map(**kwargs) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def filter_config_extras(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Derived classes can override this.  The value returned will be used to | 
					
						
							|  |  |  |         supplement the default filter config. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         return {} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def sort_map_extras(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Derived classes can override this.  The value returned will be used to | 
					
						
							|  |  |  |         supplement the default sort map. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         return {} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def _configure_grid(self, grid): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Internal method for configuring the grid.  This is meant only for base | 
					
						
							|  |  |  |         classes; derived classes should not need to override it. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def configure_grid(self, grid): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Derived classes can override this.  Customizes a grid which has already | 
					
						
							|  |  |  |         been created with defaults by the base class. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class BatchGrid(BaseGrid): | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Base grid view for batches, which can be filtered and sorted. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def batch_class(self): | 
					
						
							|  |  |  |         raise NotImplementedError | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def mapped_class(self): | 
					
						
							|  |  |  |         return self.batch_class | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-03-19 01:25:38 -05:00
										 |  |  |     @property | 
					
						
							|  |  |  |     def batch_display(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Singular display text for the batch type, e.g. "Vendor Invoice". | 
					
						
							|  |  |  |         Override this as necessary. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         return self.batch_class.__name__ | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |     @property | 
					
						
							|  |  |  |     def batch_display_plural(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2015-03-19 01:25:38 -05:00
										 |  |  |         Plural display text for the batch type, e.g. "Vendor Invoices". | 
					
						
							|  |  |  |         Override this as necessary. | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |         """
 | 
					
						
							|  |  |  |         return "{0}s".format(self.batch_display) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def join_map(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Provides the default join map for batch grid views.  Derived classes | 
					
						
							|  |  |  |         should *not* override this, but :meth:`join_map_extras()` instead. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         map_ = { | 
					
						
							|  |  |  |             'created_by': | 
					
						
							|  |  |  |                 lambda q: q.join(model.User, model.User.uuid == self.batch_class.created_by_uuid), | 
					
						
							| 
									
										
										
										
											2015-02-23 18:47:49 -06:00
										 |  |  |             'executed_by': | 
					
						
							|  |  |  |                 lambda q: q.outerjoin(model.User, model.User.uuid == self.batch_class.executed_by_uuid), | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |             } | 
					
						
							|  |  |  |         map_.update(self.join_map_extras()) | 
					
						
							|  |  |  |         return map_ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def filter_map(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Provides the default filter map for batch grid views.  Derived classes | 
					
						
							|  |  |  |         should *not* override this, but :meth:`filter_map_extras()` instead. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         def executed_is(q, v): | 
					
						
							|  |  |  |             if v == 'True': | 
					
						
							|  |  |  |                 return q.filter(self.batch_class.executed != None) | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 return q.filter(self.batch_class.executed == None) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         def executed_nt(q, v): | 
					
						
							|  |  |  |             if v == 'True': | 
					
						
							|  |  |  |                 return q.filter(self.batch_class.executed == None) | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 return q.filter(self.batch_class.executed != None) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return self.make_filter_map( | 
					
						
							|  |  |  |             executed={'is': executed_is, 'nt': executed_nt}) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def filter_config(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Provides the default filter config for batch grid views.  Derived | 
					
						
							|  |  |  |         classes should *not* override this, but :meth:`filter_config_extras()` | 
					
						
							|  |  |  |         instead. | 
					
						
							|  |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2015-07-15 23:19:29 -05:00
										 |  |  |         defaults = self.filter_config_extras() | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |         config = self.make_filter_config( | 
					
						
							|  |  |  |             filter_factory_executed=BooleanSearchFilter, | 
					
						
							|  |  |  |             filter_type_executed='is', | 
					
						
							|  |  |  |             executed=False, | 
					
						
							|  |  |  |             include_filter_executed=True) | 
					
						
							| 
									
										
										
										
											2015-07-15 23:19:29 -05:00
										 |  |  |         defaults.update(config) | 
					
						
							|  |  |  |         return defaults | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def sort_map(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Provides the default sort map for batch grid views.  Derived classes | 
					
						
							|  |  |  |         should *not* override this, but :meth:`sort_map_extras()` instead. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         map_ = self.make_sort_map( | 
					
						
							| 
									
										
										
										
											2015-02-23 18:47:49 -06:00
										 |  |  |             created_by=self.sorter(model.User.username), | 
					
						
							|  |  |  |             executed_by=self.sorter(model.User.username)) | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |         map_.update(self.sort_map_extras()) | 
					
						
							|  |  |  |         return map_ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def sort_config(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Provides the default sort config for batch grid views.  Derived classes | 
					
						
							|  |  |  |         may override this. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         return self.make_sort_config(sort='created', dir='desc') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def grid(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Creates the grid for the view.  Derived classes should *not* override | 
					
						
							|  |  |  |         this, but :meth:`configure_grid()` instead. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         g = self.make_grid() | 
					
						
							|  |  |  |         g.created.set(renderer=DateTimeFieldRenderer(self.request.rattail_config)) | 
					
						
							|  |  |  |         g.created_by.set(renderer=UserFieldRenderer) | 
					
						
							|  |  |  |         g.cognized.set(renderer=DateTimeFieldRenderer(self.request.rattail_config)) | 
					
						
							|  |  |  |         g.cognized_by.set(renderer=UserFieldRenderer) | 
					
						
							|  |  |  |         g.executed.set(renderer=DateTimeFieldRenderer(self.request.rattail_config)) | 
					
						
							|  |  |  |         g.executed_by.set(renderer=UserFieldRenderer) | 
					
						
							|  |  |  |         self._configure_grid(g) | 
					
						
							|  |  |  |         self.configure_grid(g) | 
					
						
							|  |  |  |         if self.request.has_perm('{0}.view'.format(self.permission_prefix)): | 
					
						
							|  |  |  |             g.viewable = True | 
					
						
							|  |  |  |             g.view_route_name = '{0}.view'.format(self.route_prefix) | 
					
						
							| 
									
										
										
										
											2015-02-16 18:00:45 -06:00
										 |  |  |         if self.request.has_perm('{0}.edit'.format(self.permission_prefix)): | 
					
						
							|  |  |  |             g.editable = True | 
					
						
							|  |  |  |             g.edit_route_name = '{0}.edit'.format(self.route_prefix) | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |         if self.request.has_perm('{0}.delete'.format(self.permission_prefix)): | 
					
						
							|  |  |  |             g.deletable = True | 
					
						
							|  |  |  |             g.delete_route_name = '{0}.delete'.format(self.route_prefix) | 
					
						
							|  |  |  |         return g | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def _configure_grid(self, grid): | 
					
						
							|  |  |  |         grid.created_by.set(label="Created by") | 
					
						
							|  |  |  |         grid.executed_by.set(label="Executed by") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def configure_grid(self, grid): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Derived classes can override this.  Customizes a grid which has already | 
					
						
							|  |  |  |         been created with defaults by the base class. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         g = grid | 
					
						
							|  |  |  |         g.configure( | 
					
						
							|  |  |  |             include=[ | 
					
						
							|  |  |  |                 g.created, | 
					
						
							|  |  |  |                 g.created_by, | 
					
						
							|  |  |  |                 g.executed, | 
					
						
							|  |  |  |                 g.executed_by, | 
					
						
							|  |  |  |                 ], | 
					
						
							|  |  |  |             readonly=True) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def render_kwargs(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Add some things to the template context: batch type display name, route | 
					
						
							|  |  |  |         and permission prefixes. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         return { | 
					
						
							|  |  |  |             'batch_display': self.batch_display, | 
					
						
							|  |  |  |             'batch_display_plural': self.batch_display_plural, | 
					
						
							|  |  |  |             'route_prefix': self.route_prefix, | 
					
						
							|  |  |  |             'permission_prefix': self.permission_prefix, | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class FileBatchGrid(BatchGrid): | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Base grid view for batches, which involve primarily a file upload. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def _configure_grid(self, g): | 
					
						
							|  |  |  |         super(FileBatchGrid, self)._configure_grid(g) | 
					
						
							|  |  |  |         g.created.set(label="Uploaded") | 
					
						
							|  |  |  |         g.created_by.set(label="Uploaded by") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def configure_grid(self, grid): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Derived classes can override this.  Customizes a grid which has already | 
					
						
							|  |  |  |         been created with defaults by the base class. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         g = grid | 
					
						
							|  |  |  |         g.configure( | 
					
						
							|  |  |  |             include=[ | 
					
						
							|  |  |  |                 g.created, | 
					
						
							|  |  |  |                 g.created_by, | 
					
						
							|  |  |  |                 g.filename, | 
					
						
							|  |  |  |                 g.executed, | 
					
						
							|  |  |  |                 g.executed_by, | 
					
						
							|  |  |  |                 ], | 
					
						
							|  |  |  |             readonly=True) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class BaseCrud(CrudView): | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Base CRUD view for batches and batch rows. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     flash = {} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-03-19 01:25:38 -05:00
										 |  |  |     @property | 
					
						
							|  |  |  |     def home_route(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         The "home" route for the batch type, i.e. its grid view. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         return self.route_prefix | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |     @property | 
					
						
							|  |  |  |     def permission_prefix(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Permission prefix used to generically protect certain views common to | 
					
						
							|  |  |  |         all batches.  Derived classes can override this. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         return self.route_prefix | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def flash_create(self, model): | 
					
						
							|  |  |  |         if 'create' in self.flash: | 
					
						
							|  |  |  |             self.request.session.flash(self.flash['create']) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             super(BaseCrud, self).flash_create(model) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-16 18:00:45 -06:00
										 |  |  |     def flash_update(self, model): | 
					
						
							|  |  |  |         if 'update' in self.flash: | 
					
						
							|  |  |  |             self.request.session.flash(self.flash['update']) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             super(BaseCrud, self).flash_update(model) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |     def flash_delete(self, model): | 
					
						
							|  |  |  |         if 'delete' in self.flash: | 
					
						
							|  |  |  |             self.request.session.flash(self.flash['delete']) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             super(BaseCrud, self).flash_delete(model) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class BatchCrud(BaseCrud): | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Base CRUD view for batches. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     refreshable = False | 
					
						
							|  |  |  |     flash = {} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def batch_class(self): | 
					
						
							|  |  |  |         raise NotImplementedError | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-08-19 20:06:13 -05:00
										 |  |  |     @property | 
					
						
							|  |  |  |     def batch_row_class(self): | 
					
						
							|  |  |  |         raise NotImplementedError | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |     @property | 
					
						
							|  |  |  |     def mapped_class(self): | 
					
						
							|  |  |  |         return self.batch_class | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def permission_prefix(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Permission prefix for the grid view.  This is used to automatically | 
					
						
							|  |  |  |         protect certain views common to all batches.  Derived classes can - and | 
					
						
							|  |  |  |         typically should - override this. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         return self.route_prefix | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def batch_display_plural(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Plural display text for the batch type. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         return "{0}s".format(self.batch_display) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __init__(self, request): | 
					
						
							|  |  |  |         self.request = request | 
					
						
							| 
									
										
										
										
											2015-01-20 13:22:20 -06:00
										 |  |  |         self.handler = self.get_handler() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def get_handler(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Returns a `BatchHandler` instance for the view.  Derived classes may | 
					
						
							|  |  |  |         override this as needed.  The default is to create an instance of | 
					
						
							|  |  |  |         :attr:`batch_handler_class`. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         return self.batch_handler_class(self.request.rattail_config) | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def fieldset(self, model): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Creates the fieldset for the view.  Derived classes should *not* | 
					
						
							|  |  |  |         override this, but :meth:`configure_fieldset()` instead. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         fs = self.make_fieldset(model) | 
					
						
							| 
									
										
										
										
											2015-08-18 23:50:23 -05:00
										 |  |  |         fs.created.set(renderer=DateTimeFieldRenderer(self.request.rattail_config), | 
					
						
							|  |  |  |                        readonly=True) | 
					
						
							|  |  |  |         fs.created_by.set(label="Created by", renderer=UserFieldRenderer, | 
					
						
							|  |  |  |                           readonly=True) | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |         fs.cognized.set(renderer=DateTimeFieldRenderer(self.request.rattail_config)) | 
					
						
							|  |  |  |         fs.cognized_by.set(label="Cognized by", renderer=UserFieldRenderer) | 
					
						
							|  |  |  |         fs.executed.set(renderer=DateTimeFieldRenderer(self.request.rattail_config)) | 
					
						
							|  |  |  |         fs.executed_by.set(label="Executed by", renderer=UserFieldRenderer) | 
					
						
							|  |  |  |         self.configure_fieldset(fs) | 
					
						
							|  |  |  |         if self.creating: | 
					
						
							|  |  |  |             del fs.created | 
					
						
							|  |  |  |             del fs.created_by | 
					
						
							| 
									
										
										
										
											2015-08-18 21:17:15 -05:00
										 |  |  |             if 'cognized' in fs.render_fields: | 
					
						
							|  |  |  |                 del fs.cognized | 
					
						
							|  |  |  |             if 'cognized_by' in fs.render_fields: | 
					
						
							|  |  |  |                 del fs.cognized_by | 
					
						
							|  |  |  |             if 'executed' in fs.render_fields: | 
					
						
							|  |  |  |                 del fs.executed | 
					
						
							|  |  |  |             if 'executed_by' in fs.render_fields: | 
					
						
							|  |  |  |                 del fs.executed_by | 
					
						
							| 
									
										
										
										
											2015-08-18 23:50:23 -05:00
										 |  |  |         else: | 
					
						
							|  |  |  |             batch = fs.model | 
					
						
							|  |  |  |             if not batch.executed: | 
					
						
							|  |  |  |                 if 'executed' in fs.render_fields: | 
					
						
							|  |  |  |                     del fs.executed | 
					
						
							|  |  |  |                 if 'executed_by' in fs.render_fields: | 
					
						
							|  |  |  |                     del fs.executed_by | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |         return fs | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def configure_fieldset(self, fieldset): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Derived classes can override this.  Customizes a fieldset which has | 
					
						
							|  |  |  |         already been created with defaults by the base class. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         fs = fieldset | 
					
						
							|  |  |  |         fs.configure( | 
					
						
							|  |  |  |             include=[ | 
					
						
							|  |  |  |                 fs.created, | 
					
						
							|  |  |  |                 fs.created_by, | 
					
						
							|  |  |  |                 # fs.cognized, | 
					
						
							|  |  |  |                 # fs.cognized_by, | 
					
						
							|  |  |  |                 fs.executed, | 
					
						
							|  |  |  |                 fs.executed_by, | 
					
						
							|  |  |  |                 ]) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-08-18 21:17:15 -05:00
										 |  |  |     def init_batch(self, batch): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Initialize a new batch.  Derived classes can override this to | 
					
						
							|  |  |  |         effectively provide default values for a batch, etc.  This method is | 
					
						
							|  |  |  |         invoked after a batch has been fully prepared for insertion to the | 
					
						
							|  |  |  |         database, but before the push to the database occurs. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         Note that the return value of this function matters; if it is boolean | 
					
						
							|  |  |  |         false then the batch will not be persisted at all, and the user will be | 
					
						
							|  |  |  |         redirected to the "create batch" page. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         return True | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def save_form(self, form): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Save the uploaded data file if necessary, etc.  If batch initialization | 
					
						
							|  |  |  |         fails, don't persist the batch at all; the user will be sent back to | 
					
						
							|  |  |  |         the "create batch" page in that case. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         # Transfer form data to batch instance. | 
					
						
							|  |  |  |         form.fieldset.sync() | 
					
						
							|  |  |  |         batch = form.fieldset.model | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # For new batches, assign current user as creator, etc. | 
					
						
							|  |  |  |         if self.creating: | 
					
						
							|  |  |  |             with Session.no_autoflush: | 
					
						
							|  |  |  |                 batch.created_by = self.request.user or self.late_login_user() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             # 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) | 
					
						
							|  |  |  |             self.batch_inited = self.init_batch(batch) | 
					
						
							|  |  |  |             if self.batch_inited: | 
					
						
							|  |  |  |                 Session.add(batch) | 
					
						
							|  |  |  |                 Session.flush() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-16 18:00:45 -06:00
										 |  |  |     def update(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Don't allow editing a batch which has already been executed. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         batch = self.get_model_from_request() | 
					
						
							|  |  |  |         if not batch: | 
					
						
							|  |  |  |             return HTTPNotFound() | 
					
						
							|  |  |  |         if batch.executed: | 
					
						
							|  |  |  |             return HTTPFound(location=self.view_url(batch.uuid)) | 
					
						
							|  |  |  |         return self.crud(batch) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def post_create_url(self, form): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Redirect to view batch after creating a batch. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         batch = form.fieldset.model | 
					
						
							| 
									
										
										
										
											2015-02-22 00:00:00 -06:00
										 |  |  |         Session.flush() | 
					
						
							| 
									
										
										
										
											2015-02-16 18:00:45 -06:00
										 |  |  |         return self.view_url(batch.uuid) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def post_update_url(self, form): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Redirect back to edit batch page after editing a batch, unless the | 
					
						
							|  |  |  |         refresh flag is set, in which case do that. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         if self.request.params.get('refresh') == 'true': | 
					
						
							|  |  |  |             return self.refresh_url() | 
					
						
							|  |  |  |         return self.request.current_route_url() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |     def template_kwargs(self, form): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Add some things to the template context: current batch model, batch | 
					
						
							|  |  |  |         type display name, route and permission prefixes, batch row grid. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         batch = form.fieldset.model | 
					
						
							|  |  |  |         batch.refreshable = self.refreshable | 
					
						
							| 
									
										
										
										
											2015-07-09 15:45:54 -05:00
										 |  |  |         kwargs = { | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |             'batch': batch, | 
					
						
							|  |  |  |             'batch_display': self.batch_display, | 
					
						
							|  |  |  |             'batch_display_plural': self.batch_display_plural, | 
					
						
							| 
									
										
										
										
											2015-02-16 18:00:45 -06:00
										 |  |  |             'execute_title': self.handler.get_execute_title(batch), | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |             'route_prefix': self.route_prefix, | 
					
						
							|  |  |  |             'permission_prefix': self.permission_prefix, | 
					
						
							|  |  |  |             } | 
					
						
							| 
									
										
										
										
											2015-07-09 15:45:54 -05:00
										 |  |  |         if not self.creating: | 
					
						
							|  |  |  |             kwargs['execute_enabled'] = self.executable(batch) | 
					
						
							|  |  |  |         return kwargs | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-07-09 13:38:54 -05:00
										 |  |  |     def executable(self, batch): | 
					
						
							|  |  |  |         return self.handler.executable(batch) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |     def flash_create(self, batch): | 
					
						
							|  |  |  |         if 'create' in self.flash: | 
					
						
							|  |  |  |             self.request.session.flash(self.flash['create']) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             super(BatchCrud, self).flash_create(batch) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def flash_delete(self, batch): | 
					
						
							|  |  |  |         if 'delete' in self.flash: | 
					
						
							|  |  |  |             self.request.session.flash(self.flash['delete']) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             super(BatchCrud, self).flash_delete(batch) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def current_batch(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Return the current batch, based on the UUID within the URL. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         return Session.query(self.mapped_class).get(self.request.matchdict['uuid']) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def refresh(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         View which will attempt to refresh all data for the batch.  What | 
					
						
							|  |  |  |         exactly this means will depend on the type of batch etc. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         batch = self.current_batch() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # If handler doesn't declare the need for progress indicator, things | 
					
						
							|  |  |  |         # are nice and simple. | 
					
						
							|  |  |  |         if not self.handler.show_progress: | 
					
						
							|  |  |  |             self.refresh_data(Session, batch) | 
					
						
							|  |  |  |             self.request.session.flash("Batch data has been refreshed.") | 
					
						
							|  |  |  |             return HTTPFound(location=self.view_url(batch.uuid)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Showing progress requires a separate thread; start that first. | 
					
						
							|  |  |  |         key = '{0}.refresh'.format(self.batch_class.__tablename__) | 
					
						
							|  |  |  |         progress = SessionProgress(self.request, key) | 
					
						
							|  |  |  |         thread = Thread(target=self.refresh_thread, args=(batch.uuid, progress)) | 
					
						
							|  |  |  |         thread.start() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Send user to progress page. | 
					
						
							|  |  |  |         kwargs = { | 
					
						
							|  |  |  |             'key': key, | 
					
						
							|  |  |  |             'cancel_url': self.view_url(batch.uuid), | 
					
						
							|  |  |  |             'cancel_msg': "Batch refresh was canceled.", | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         return render_to_response('/progress.mako', kwargs, request=self.request) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-08-15 17:00:01 -05:00
										 |  |  |     def refresh_data(self, session, batch, cognizer=None, progress=None): | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |         """
 | 
					
						
							|  |  |  |         Instruct the batch handler to refresh all data for the batch. | 
					
						
							|  |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2015-02-12 21:35:28 -06:00
										 |  |  |         self.handler.refresh_data(session, batch, progress=progress) | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |         batch.cognized = datetime.datetime.utcnow() | 
					
						
							| 
									
										
										
										
											2015-08-15 17:00:01 -05:00
										 |  |  |         if cognizer is not None: | 
					
						
							|  |  |  |             batch.cognized_by = cognizer | 
					
						
							|  |  |  |         else: | 
					
						
							| 
									
										
										
										
											2016-02-09 20:47:12 -06:00
										 |  |  |             batch.cognized_by = session.merge(self.request.user) | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-08-18 21:17:15 -05:00
										 |  |  |     def refresh_thread(self, batch_uuid, progress=None, cognizer_uuid=None, success_url=None): | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |         """
 | 
					
						
							|  |  |  |         Thread target for refreshing batch data with progress indicator. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         # Refresh data for the batch, with progress.  Note that we must use the | 
					
						
							|  |  |  |         # rattail session here; can't use tailbone because it has web request | 
					
						
							|  |  |  |         # transaction binding etc. | 
					
						
							|  |  |  |         session = RatSession() | 
					
						
							|  |  |  |         batch = session.query(self.batch_class).get(batch_uuid) | 
					
						
							| 
									
										
										
										
											2015-08-15 17:00:01 -05:00
										 |  |  |         cognizer = session.query(model.User).get(cognizer_uuid) if cognizer_uuid else None | 
					
						
							| 
									
										
										
										
											2015-02-23 20:10:21 -06:00
										 |  |  |         try: | 
					
						
							| 
									
										
										
										
											2015-08-15 17:00:01 -05:00
										 |  |  |             self.refresh_data(session, batch, cognizer=cognizer, progress=progress) | 
					
						
							| 
									
										
										
										
											2015-02-23 20:10:21 -06:00
										 |  |  |         except Exception as error: | 
					
						
							|  |  |  |             session.rollback() | 
					
						
							| 
									
										
										
										
											2015-07-29 12:27:28 -05:00
										 |  |  |             log.warning("refreshing data for batch failed: {0}".format(batch), exc_info=True) | 
					
						
							| 
									
										
										
										
											2015-02-23 20:10:21 -06:00
										 |  |  |             session.close() | 
					
						
							| 
									
										
										
										
											2015-08-15 17:00:01 -05:00
										 |  |  |             if progress: | 
					
						
							|  |  |  |                 progress.session.load() | 
					
						
							|  |  |  |                 progress.session['error'] = True | 
					
						
							|  |  |  |                 progress.session['error_msg'] = "Data refresh failed: {0}".format(error) | 
					
						
							|  |  |  |                 progress.session.save() | 
					
						
							| 
									
										
										
										
											2015-02-23 20:10:21 -06:00
										 |  |  |             return | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |         session.commit() | 
					
						
							|  |  |  |         session.refresh(batch) | 
					
						
							|  |  |  |         session.close() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Finalize progress indicator. | 
					
						
							| 
									
										
										
										
											2015-08-15 17:00:01 -05:00
										 |  |  |         if progress: | 
					
						
							|  |  |  |             progress.session.load() | 
					
						
							|  |  |  |             progress.session['complete'] = True | 
					
						
							|  |  |  |             progress.session['success_url'] = success_url or self.view_url(batch.uuid) | 
					
						
							|  |  |  |             progress.session.save() | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |          | 
					
						
							|  |  |  |     def view_url(self, uuid=None): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Returns the URL for viewing a batch; defaults to current batch. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         if uuid is None: | 
					
						
							|  |  |  |             uuid = self.request.matchdict['uuid'] | 
					
						
							|  |  |  |         return self.request.route_url('{0}.view'.format(self.route_prefix), uuid=uuid) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-16 18:00:45 -06:00
										 |  |  |     def refresh_url(self, uuid=None): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Returns the URL for refreshing a batch; defaults to current batch. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         if uuid is None: | 
					
						
							|  |  |  |             uuid = self.request.matchdict['uuid'] | 
					
						
							|  |  |  |         return self.request.route_url('{0}.refresh'.format(self.route_prefix), uuid=uuid) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |     def execute(self): | 
					
						
							| 
									
										
										
										
											2015-08-19 18:49:09 -05:00
										 |  |  |         """
 | 
					
						
							|  |  |  |         Execute a batch.  Starts a separate thread for the execution, and | 
					
						
							|  |  |  |         displays a progress indicator page. | 
					
						
							|  |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |         batch = self.current_batch() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-08-19 18:49:09 -05:00
										 |  |  |         key = '{0}.execute'.format(self.batch_class.__tablename__) | 
					
						
							|  |  |  |         progress = SessionProgress(self.request, key) | 
					
						
							|  |  |  |         thread = Thread(target=self.execute_thread, args=(batch.uuid, progress)) | 
					
						
							|  |  |  |         thread.start() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         kwargs = { | 
					
						
							|  |  |  |             'key': key, | 
					
						
							|  |  |  |             'cancel_url': self.view_url(batch.uuid), | 
					
						
							|  |  |  |             'cancel_msg': "Batch execution was canceled.", | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         return render_to_response('/progress.mako', kwargs, request=self.request) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def execute_thread(self, batch_uuid, progress=None): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Thread target for executing a batch with progress indicator. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         # Execute the batch, with progress.  Note that we must use the rattail | 
					
						
							|  |  |  |         # session here; can't use tailbone because it has web request | 
					
						
							|  |  |  |         # transaction binding etc. | 
					
						
							|  |  |  |         session = RatSession() | 
					
						
							|  |  |  |         batch = session.query(self.batch_class).get(batch_uuid) | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             result = self.handler.execute(batch, progress=progress) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # If anything goes wrong, rollback and log the error etc. | 
					
						
							|  |  |  |         except Exception as error: | 
					
						
							|  |  |  |             session.rollback() | 
					
						
							|  |  |  |             log.exception("execution failed for batch: {0}".format(batch)) | 
					
						
							|  |  |  |             session.close() | 
					
						
							|  |  |  |             if progress: | 
					
						
							|  |  |  |                 progress.session.load() | 
					
						
							|  |  |  |                 progress.session['error'] = True | 
					
						
							|  |  |  |                 progress.session['error_msg'] = "Batch execution failed: {0}".format(error) | 
					
						
							|  |  |  |                 progress.session.save() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # If no error, check result flag (false means user canceled). | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             if result: | 
					
						
							|  |  |  |                 batch.executed = datetime.datetime.utcnow() | 
					
						
							| 
									
										
										
										
											2016-02-01 12:23:29 -06:00
										 |  |  |                 batch.executed_by = session.merge(self.request.user) | 
					
						
							| 
									
										
										
										
											2015-08-19 18:49:09 -05:00
										 |  |  |                 session.commit() | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 session.rollback() | 
					
						
							|  |  |  |             session.refresh(batch) | 
					
						
							|  |  |  |             session.close() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if progress: | 
					
						
							|  |  |  |                 progress.session.load() | 
					
						
							|  |  |  |                 progress.session['complete'] = True | 
					
						
							|  |  |  |                 progress.session['success_url'] = self.view_url(batch.uuid) | 
					
						
							|  |  |  |                 progress.session.save() | 
					
						
							|  |  |  |          | 
					
						
							| 
									
										
										
										
											2015-08-19 20:06:13 -05:00
										 |  |  |     def csv(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Download batch data as CSV. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         batch = self.current_batch() | 
					
						
							|  |  |  |         fields = self.get_csv_fields() | 
					
						
							|  |  |  |         data = StringIO() | 
					
						
							|  |  |  |         writer = UnicodeDictWriter(data, fields) | 
					
						
							|  |  |  |         writer.writeheader() | 
					
						
							|  |  |  |         for row in batch.data_rows: | 
					
						
							|  |  |  |             if not row.removed: | 
					
						
							|  |  |  |                 writer.writerow(self.get_csv_row(row, fields)) | 
					
						
							|  |  |  |         response = self.request.response | 
					
						
							|  |  |  |         response.text = data.getvalue().decode('utf_8') | 
					
						
							|  |  |  |         data.close() | 
					
						
							|  |  |  |         response.content_length = len(response.text) | 
					
						
							|  |  |  |         response.content_type = b'text/csv' | 
					
						
							|  |  |  |         response.content_disposition = b'attachment; filename=batch.csv' | 
					
						
							|  |  |  |         return response | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def get_csv_fields(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Return the list of fields to be written to CSV download. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         fields = [] | 
					
						
							|  |  |  |         mapper = orm.class_mapper(self.batch_row_class) | 
					
						
							|  |  |  |         for prop in mapper.iterate_properties: | 
					
						
							|  |  |  |             if isinstance(prop, orm.ColumnProperty): | 
					
						
							|  |  |  |                 if prop.key != 'removed' and not prop.key.endswith('uuid'): | 
					
						
							|  |  |  |                     fields.append(prop.key) | 
					
						
							|  |  |  |         return fields | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def get_csv_row(self, row, fields): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Return a dict for use when writing the row's data to CSV download. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         csvrow = {} | 
					
						
							|  |  |  |         for field in fields: | 
					
						
							|  |  |  |             value = getattr(row, field) | 
					
						
							|  |  |  |             csvrow[field] = '' if value is None else unicode(value) | 
					
						
							|  |  |  |         return csvrow | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  | class FileBatchCrud(BatchCrud): | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Base CRUD view for batches which involve a file upload as the first step. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     refreshable = True | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-23 20:10:21 -06:00
										 |  |  |     def post_create_url(self, form): | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2015-02-23 20:10:21 -06:00
										 |  |  |         Redirect to refresh batch after creating a batch. | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2015-02-23 20:10:21 -06:00
										 |  |  |         batch = form.fieldset.model | 
					
						
							|  |  |  |         Session.flush() | 
					
						
							|  |  |  |         return self.refresh_url(batch.uuid) | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-22 00:00:00 -06:00
										 |  |  |     @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 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |     def fieldset(self, model): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Creates the fieldset for the view.  Derived classes should *not* | 
					
						
							|  |  |  |         override this, but :meth:`configure_fieldset()` instead. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         fs = self.make_fieldset(model) | 
					
						
							| 
									
										
										
										
											2015-02-16 18:00:45 -06:00
										 |  |  |         fs.created.set(label="Uploaded", renderer=DateTimeFieldRenderer(self.request.rattail_config), readonly=True) | 
					
						
							|  |  |  |         fs.created_by.set(label="Uploaded by", renderer=UserFieldRenderer, readonly=True) | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |         fs.cognized.set(renderer=DateTimeFieldRenderer(self.request.rattail_config)) | 
					
						
							|  |  |  |         fs.cognized_by.set(label="Cognized by", renderer=UserFieldRenderer) | 
					
						
							|  |  |  |         fs.executed.set(renderer=DateTimeFieldRenderer(self.request.rattail_config)) | 
					
						
							|  |  |  |         fs.executed_by.set(label="Executed by", renderer=UserFieldRenderer) | 
					
						
							| 
									
										
										
										
											2015-02-22 00:00:00 -06:00
										 |  |  |         fs.filename.set(renderer=FileFieldRenderer.new(self), label="Data File") | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |         self.configure_fieldset(fs) | 
					
						
							|  |  |  |         if self.creating: | 
					
						
							| 
									
										
										
										
											2015-07-03 17:48:53 -05:00
										 |  |  |             if 'created' in fs.render_fields: | 
					
						
							|  |  |  |                 del fs.created | 
					
						
							|  |  |  |             if 'created_by' in fs.render_fields: | 
					
						
							|  |  |  |                 del fs.created_by | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |             if 'cognized' in fs.render_fields: | 
					
						
							|  |  |  |                 del fs.cognized | 
					
						
							|  |  |  |             if 'cognized_by' in fs.render_fields: | 
					
						
							|  |  |  |                 del fs.cognized_by | 
					
						
							|  |  |  |             if 'executed' in fs.render_fields: | 
					
						
							|  |  |  |                 del fs.executed | 
					
						
							|  |  |  |             if 'executed_by' in fs.render_fields: | 
					
						
							|  |  |  |                 del fs.executed_by | 
					
						
							|  |  |  |             if 'data_rows' in fs.render_fields: | 
					
						
							|  |  |  |                 del fs.data_rows | 
					
						
							|  |  |  |         else: | 
					
						
							| 
									
										
										
										
											2015-02-22 00:00:00 -06:00
										 |  |  |             if self.updating and 'filename' in fs.render_fields: | 
					
						
							|  |  |  |                 fs.filename.set(readonly=True) | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |             batch = fs.model | 
					
						
							|  |  |  |             if not batch.executed: | 
					
						
							|  |  |  |                 if 'executed' in fs.render_fields: | 
					
						
							|  |  |  |                     del fs.executed | 
					
						
							|  |  |  |                 if 'executed_by' in fs.render_fields: | 
					
						
							|  |  |  |                     del fs.executed_by | 
					
						
							|  |  |  |         return fs | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def configure_fieldset(self, fieldset): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Derived classes can override this.  Customizes a fieldset which has | 
					
						
							|  |  |  |         already been created with defaults by the base class. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         fs = fieldset | 
					
						
							|  |  |  |         fs.configure( | 
					
						
							|  |  |  |             include=[ | 
					
						
							|  |  |  |                 fs.created, | 
					
						
							|  |  |  |                 fs.created_by, | 
					
						
							|  |  |  |                 fs.filename, | 
					
						
							|  |  |  |                 # fs.cognized, | 
					
						
							|  |  |  |                 # fs.cognized_by, | 
					
						
							|  |  |  |                 fs.executed, | 
					
						
							|  |  |  |                 fs.executed_by, | 
					
						
							|  |  |  |                 ]) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def save_form(self, form): | 
					
						
							|  |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2015-02-12 21:35:28 -06:00
										 |  |  |         Save the uploaded data file if necessary, etc.  If batch initialization | 
					
						
							|  |  |  |         fails, don't persist the batch at all; the user will be sent back to | 
					
						
							|  |  |  |         the "create batch" page in that case. | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |         """
 | 
					
						
							|  |  |  |         # Transfer form data to batch instance. | 
					
						
							|  |  |  |         form.fieldset.sync() | 
					
						
							|  |  |  |         batch = form.fieldset.model | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # For new batches, assign current user as creator, save file etc. | 
					
						
							|  |  |  |         if self.creating: | 
					
						
							| 
									
										
										
										
											2015-08-15 17:00:01 -05:00
										 |  |  |             with Session.no_autoflush: | 
					
						
							| 
									
										
										
										
											2015-08-17 16:55:10 -05:00
										 |  |  |                 batch.created_by = self.request.user or self.late_login_user() | 
					
						
							| 
									
										
										
										
											2015-02-22 00:00:00 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  |             # 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. | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |             Session.expunge(batch) | 
					
						
							| 
									
										
										
										
											2015-02-12 21:35:28 -06:00
										 |  |  |             self.batch_inited = self.init_batch(batch) | 
					
						
							|  |  |  |             if self.batch_inited: | 
					
						
							|  |  |  |                 Session.add(batch) | 
					
						
							| 
									
										
										
										
											2015-02-22 00:00:00 -06:00
										 |  |  |                 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) | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-12 21:35:28 -06:00
										 |  |  |     def post_save(self, form): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         This checks for failed batch initialization when creating a new batch. | 
					
						
							|  |  |  |         If a failure is detected, the user is redirected to the page for | 
					
						
							|  |  |  |         creating new batches.  The assumption here is that the | 
					
						
							|  |  |  |         :meth:`init_batch()` method responsible for indicating the failure will | 
					
						
							|  |  |  |         have set a flash message for the user with more info. | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2015-02-12 21:35:28 -06:00
										 |  |  |         if self.creating and not self.batch_inited: | 
					
						
							|  |  |  |             return HTTPFound(location=self.request.route_url( | 
					
						
							|  |  |  |                     '{0}.create'.format(self.route_prefix))) | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def pre_delete(self, batch): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Delete all data (files etc.) for the batch. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         batch.delete_data(self.request.rattail_config) | 
					
						
							|  |  |  |         del batch.data_rows[:] | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-12 23:19:01 -06:00
										 |  |  |     def download(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         View for downloading the data file associated with a batch. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         batch = self.current_batch() | 
					
						
							|  |  |  |         if not batch: | 
					
						
							|  |  |  |             return HTTPNotFound() | 
					
						
							| 
									
										
										
										
											2015-02-22 00:00:00 -06:00
										 |  |  |         path = self.handler.data_path(batch) | 
					
						
							| 
									
										
										
										
											2015-02-12 23:19:01 -06:00
										 |  |  |         response = FileResponse(path, request=self.request) | 
					
						
							| 
									
										
										
										
											2015-02-22 00:00:00 -06:00
										 |  |  |         response.headers[b'Content-Length'] = str(os.path.getsize(path)) | 
					
						
							|  |  |  |         filename = os.path.basename(batch.filename).encode('ascii', 'replace') | 
					
						
							|  |  |  |         response.headers[b'Content-Disposition'] = b'attachment; filename="{0}"'.format(filename) | 
					
						
							| 
									
										
										
										
											2015-02-12 23:19:01 -06:00
										 |  |  |         return response | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-16 18:00:45 -06:00
										 |  |  | class StatusRenderer(EnumFieldRenderer): | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Custom renderer for ``status_code`` fields.  Adds ``status_text`` value as | 
					
						
							|  |  |  |     title attribute if it exists. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def render_readonly(self, **kwargs): | 
					
						
							|  |  |  |         value = self.raw_value | 
					
						
							|  |  |  |         if value is None: | 
					
						
							|  |  |  |             return '' | 
					
						
							|  |  |  |         status_code_text = self.enumeration.get(value, unicode(value)) | 
					
						
							|  |  |  |         row = self.field.parent.model | 
					
						
							|  |  |  |         if row.status_text: | 
					
						
							|  |  |  |             return HTML.tag('span', title=row.status_text, c=status_code_text) | 
					
						
							|  |  |  |         return status_code_text | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  | class BatchRowGrid(BaseGrid): | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2015-02-12 21:35:28 -06:00
										 |  |  |     Base grid view for batch rows, which can be filtered and sorted.  Also it | 
					
						
							|  |  |  |     can delete all rows matching the current list view query. | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |     """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def row_class(self): | 
					
						
							|  |  |  |         raise NotImplementedError | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def mapped_class(self): | 
					
						
							|  |  |  |         return self.row_class | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def config_prefix(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Config prefix for the grid view.  This is used to keep track of current | 
					
						
							|  |  |  |         filtering and sorting, within the user's session.  Derived classes may | 
					
						
							|  |  |  |         override this. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         return '{0}.{1}'.format(self.mapped_class.__name__.lower(), | 
					
						
							|  |  |  |                                 self.request.matchdict['uuid']) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-03-19 01:25:38 -05:00
										 |  |  |     @property | 
					
						
							|  |  |  |     def batch_class(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Model class of the batch to which the rows belong. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         return self.row_class.__batch_class__ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def batch_display(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Singular display text for the batch type, e.g. "Vendor Invoice". | 
					
						
							|  |  |  |         Override this as necessary. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         return self.batch_class.__name__ | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |     def current_batch(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Return the current batch, based on the UUID within the URL. | 
					
						
							|  |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2015-03-19 01:25:38 -05:00
										 |  |  |         return Session.query(self.batch_class).get(self.request.matchdict['uuid']) | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def modify_query(self, q): | 
					
						
							|  |  |  |         q = super(BatchRowGrid, self).modify_query(q) | 
					
						
							| 
									
										
										
										
											2015-04-06 20:43:55 -05:00
										 |  |  |         q = q.filter(self.row_class.batch == self.current_batch()) | 
					
						
							|  |  |  |         q = q.filter(self.row_class.removed == False) | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |         return q | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def join_map(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Provides the default join map for batch row grid views.  Derived | 
					
						
							|  |  |  |         classes should *not* override this, but :meth:`join_map_extras()` | 
					
						
							|  |  |  |         instead. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         return self.join_map_extras() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def filter_map(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Provides the default filter map for batch row grid views.  Derived | 
					
						
							|  |  |  |         classes should *not* override this, but :meth:`filter_map_extras()` | 
					
						
							|  |  |  |         instead. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         return self.make_filter_map(exact=['status_code']) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def filter_config(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Provides the default filter config for batch grid views.  Derived | 
					
						
							|  |  |  |         classes should *not* override this, but :meth:`filter_config_extras()` | 
					
						
							|  |  |  |         instead. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         kwargs = {'filter_label_status_code': "Status", | 
					
						
							|  |  |  |                   'filter_factory_status_code': EnumSearchFilter(self.row_class.STATUS)} | 
					
						
							|  |  |  |         kwargs.update(self.filter_config_extras()) | 
					
						
							|  |  |  |         return self.make_filter_config(**kwargs) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def sort_map(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Provides the default sort map for batch grid views.  Derived classes | 
					
						
							|  |  |  |         should *not* override this, but :meth:`sort_map_extras()` instead. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         map_ = self.make_sort_map() | 
					
						
							|  |  |  |         map_.update(self.sort_map_extras()) | 
					
						
							|  |  |  |         return map_ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def sort_config(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Provides the default sort config for batch grid views.  Derived classes | 
					
						
							|  |  |  |         may override this. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         return self.make_sort_config(sort='sequence', dir='asc') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def grid(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Creates the grid for the view.  Derived classes should *not* override | 
					
						
							|  |  |  |         this, but :meth:`configure_grid()` instead. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         g = self.make_grid() | 
					
						
							|  |  |  |         g.extra_row_class = self.tr_class | 
					
						
							|  |  |  |         g.sequence.set(label="Seq.") | 
					
						
							| 
									
										
										
										
											2015-02-16 18:00:45 -06:00
										 |  |  |         g.status_code.set(label="Status", renderer=StatusRenderer(self.row_class.STATUS)) | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |         self._configure_grid(g) | 
					
						
							|  |  |  |         self.configure_grid(g) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         batch = self.current_batch() | 
					
						
							| 
									
										
										
										
											2015-03-19 01:25:38 -05:00
										 |  |  |         g.viewable = True | 
					
						
							|  |  |  |         g.view_route_name = '{0}.row.view'.format(self.route_prefix) | 
					
						
							| 
									
										
										
										
											2015-02-16 18:00:45 -06:00
										 |  |  |         # TODO: Fix this check for edit mode. | 
					
						
							|  |  |  |         edit_mode = self.request.referrer.endswith('/edit') | 
					
						
							|  |  |  |         if edit_mode and not batch.executed and self.request.has_perm('{0}.edit'.format(self.permission_prefix)): | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |             # g.editable = True | 
					
						
							|  |  |  |             # g.edit_route_name = '{0}.rows.edit'.format(self.route_prefix) | 
					
						
							|  |  |  |             g.deletable = True | 
					
						
							|  |  |  |             g.delete_route_name = '{0}.rows.delete'.format(self.route_prefix) | 
					
						
							|  |  |  |         return g | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def tr_class(self, row, i): | 
					
						
							|  |  |  |         pass | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-12 21:35:28 -06:00
										 |  |  |     def render_kwargs(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Add the current batch and route prefix to the template context. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         return {'batch': self.current_batch(), | 
					
						
							|  |  |  |                 'route_prefix': self.route_prefix} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def bulk_delete(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2015-03-19 02:02:07 -05:00
										 |  |  |         "Delete" all rows matching the current row grid view query.  This sets | 
					
						
							|  |  |  |         the ``removed`` flag on the rows but does not truly delete them. | 
					
						
							| 
									
										
										
										
											2015-02-12 21:35:28 -06:00
										 |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2015-03-19 02:02:07 -05:00
										 |  |  |         self.query().update({'removed': True}, synchronize_session=False) | 
					
						
							| 
									
										
										
										
											2015-02-12 21:35:28 -06:00
										 |  |  |         return HTTPFound(location=self.request.route_url( | 
					
						
							|  |  |  |                 '{0}.view'.format(self.route_prefix), | 
					
						
							|  |  |  |                 uuid=self.request.matchdict['uuid'])) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  | class ProductBatchRowGrid(BatchRowGrid): | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Base grid view for batch rows which deal directly with products. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def filter_map(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Provides the default filter map for batch row grid views.  Derived | 
					
						
							|  |  |  |         classes should *not* override this, but :meth:`filter_map_extras()` | 
					
						
							|  |  |  |         instead. | 
					
						
							|  |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2015-03-07 14:19:19 -06:00
										 |  |  |         return self.make_filter_map(exact=['status_code'], | 
					
						
							|  |  |  |                                     ilike=['brand_name', 'description', 'size'], | 
					
						
							|  |  |  |                                     upc=self.filter_gpc(self.row_class.upc)) | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def filter_config(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Provides the default filter config for batch grid views.  Derived | 
					
						
							|  |  |  |         classes should *not* override this, but :meth:`filter_config_extras()` | 
					
						
							|  |  |  |         instead. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         kwargs = {'filter_label_status_code': "Status", | 
					
						
							|  |  |  |                   'filter_factory_status_code': EnumSearchFilter(self.row_class.STATUS), | 
					
						
							|  |  |  |                   'filter_label_upc': "UPC", | 
					
						
							|  |  |  |                   'filter_label_brand_name': "Brand"} | 
					
						
							|  |  |  |         kwargs.update(self.filter_config_extras()) | 
					
						
							|  |  |  |         return self.make_filter_config(**kwargs) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class BatchRowCrud(BaseCrud): | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Base CRUD view for batch rows. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def row_class(self): | 
					
						
							|  |  |  |         raise NotImplementedError | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def mapped_class(self): | 
					
						
							|  |  |  |         return self.row_class | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-03-19 01:25:38 -05:00
										 |  |  |     @property | 
					
						
							|  |  |  |     def batch_class(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Model class of the batch to which the rows belong. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         return self.row_class.__batch_class__ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def batch_display(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Singular display text for the batch type, e.g. "Vendor Invoice". | 
					
						
							|  |  |  |         Override this as necessary. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         return self.batch_class.__name__ | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-08-19 17:15:09 -05:00
										 |  |  |     def fieldset(self, row): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Creates the fieldset for the view.  Derived classes should *not* | 
					
						
							|  |  |  |         override this, but :meth:`configure_fieldset()` instead. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         fs = self.make_fieldset(row) | 
					
						
							|  |  |  |         self.configure_fieldset(fs) | 
					
						
							|  |  |  |         return fs | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def configure_fieldset(self, fieldset): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Derived classes can override this.  Customizes a fieldset which has | 
					
						
							|  |  |  |         already been created with defaults by the base class. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         fieldset.configure() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-03-19 01:25:38 -05:00
										 |  |  |     def template_kwargs(self, form): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Add batch row instance etc. to template context. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         row = form.fieldset.model | 
					
						
							|  |  |  |         return { | 
					
						
							|  |  |  |             'row': row, | 
					
						
							|  |  |  |             'batch_display': self.batch_display, | 
					
						
							|  |  |  |             'route_prefix': self.route_prefix, | 
					
						
							|  |  |  |             'permission_prefix': self.permission_prefix, | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |     def delete(self): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         "Delete" a row from the batch.  This sets the ``removed`` flag on the | 
					
						
							|  |  |  |         row but does not truly delete it. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         row = self.get_model_from_request() | 
					
						
							|  |  |  |         if not row: | 
					
						
							|  |  |  |             return HTTPNotFound() | 
					
						
							|  |  |  |         row.removed = True | 
					
						
							|  |  |  |         return HTTPFound(location=self.request.route_url( | 
					
						
							|  |  |  |                 '{0}.view'.format(self.route_prefix), uuid=row.batch_uuid)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def defaults(config, batch_grid, batch_crud, row_grid, row_crud, url_prefix, | 
					
						
							|  |  |  |              route_prefix=None, permission_prefix=None, template_prefix=None): | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Apply default configuration to the Pyramid configurator object, for the | 
					
						
							|  |  |  |     given batch grid and CRUD views. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     assert batch_grid | 
					
						
							|  |  |  |     assert batch_crud | 
					
						
							|  |  |  |     assert url_prefix | 
					
						
							|  |  |  |     if route_prefix is None: | 
					
						
							|  |  |  |         route_prefix = batch_grid.route_prefix | 
					
						
							|  |  |  |     if permission_prefix is None: | 
					
						
							|  |  |  |         permission_prefix = route_prefix | 
					
						
							|  |  |  |     if template_prefix is None: | 
					
						
							|  |  |  |         template_prefix = url_prefix | 
					
						
							|  |  |  |     template_prefix.rstrip('/') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Batches grid | 
					
						
							|  |  |  |     config.add_route(route_prefix, url_prefix) | 
					
						
							|  |  |  |     config.add_view(batch_grid, route_name=route_prefix, | 
					
						
							|  |  |  |                     renderer='{0}/index.mako'.format(template_prefix), | 
					
						
							|  |  |  |                     permission='{0}.view'.format(permission_prefix)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Create batch | 
					
						
							|  |  |  |     config.add_route('{0}.create'.format(route_prefix), '{0}new'.format(url_prefix)) | 
					
						
							|  |  |  |     config.add_view(batch_crud, attr='create', route_name='{0}.create'.format(route_prefix), | 
					
						
							|  |  |  |                     renderer='{0}/create.mako'.format(template_prefix), | 
					
						
							|  |  |  |                     permission='{0}.create'.format(permission_prefix)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # View batch | 
					
						
							|  |  |  |     config.add_route('{0}.view'.format(route_prefix), '{0}{{uuid}}'.format(url_prefix)) | 
					
						
							|  |  |  |     config.add_view(batch_crud, attr='read', route_name='{0}.view'.format(route_prefix), | 
					
						
							|  |  |  |                     renderer='{0}/view.mako'.format(template_prefix), | 
					
						
							|  |  |  |                     permission='{0}.view'.format(permission_prefix)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Edit batch | 
					
						
							|  |  |  |     config.add_route('{0}.edit'.format(route_prefix), '{0}{{uuid}}/edit'.format(url_prefix)) | 
					
						
							|  |  |  |     config.add_view(batch_crud, attr='update', route_name='{0}.edit'.format(route_prefix), | 
					
						
							|  |  |  |                     renderer='{0}/edit.mako'.format(template_prefix), | 
					
						
							|  |  |  |                     permission='{0}.edit'.format(permission_prefix)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Refresh batch row data | 
					
						
							|  |  |  |     config.add_route('{0}.refresh'.format(route_prefix), '{0}{{uuid}}/refresh'.format(url_prefix)) | 
					
						
							|  |  |  |     config.add_view(batch_crud, attr='refresh', route_name='{0}.refresh'.format(route_prefix), | 
					
						
							| 
									
										
										
										
											2015-04-10 22:04:37 -05:00
										 |  |  |                     permission='{0}.create'.format(permission_prefix)) | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  |     # Execute batch | 
					
						
							|  |  |  |     config.add_route('{0}.execute'.format(route_prefix), '{0}{{uuid}}/execute'.format(url_prefix)) | 
					
						
							|  |  |  |     config.add_view(batch_crud, attr='execute', route_name='{0}.execute'.format(route_prefix), | 
					
						
							|  |  |  |                     permission='{0}.execute'.format(permission_prefix)) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-08-19 20:06:13 -05:00
										 |  |  |     # Download batch row data as CSV | 
					
						
							|  |  |  |     config.add_route('{0}.csv'.format(route_prefix), '{0}{{uuid}}/csv'.format(url_prefix)) | 
					
						
							|  |  |  |     config.add_view(batch_crud, attr='csv', route_name='{0}.csv'.format(route_prefix), | 
					
						
							|  |  |  |                     permission='{0}.csv'.format(permission_prefix)) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-12 23:19:01 -06:00
										 |  |  |     # Download batch data file | 
					
						
							|  |  |  |     if hasattr(batch_crud, 'download'): | 
					
						
							|  |  |  |         config.add_route('{0}.download'.format(route_prefix), '{0}{{uuid}}/download'.format(url_prefix)) | 
					
						
							|  |  |  |         config.add_view(batch_crud, attr='download', route_name='{0}.download'.format(route_prefix), | 
					
						
							|  |  |  |                         permission='{0}.download'.format(permission_prefix)) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |     # Delete batch | 
					
						
							|  |  |  |     config.add_route('{0}.delete'.format(route_prefix), '{0}{{uuid}}/delete'.format(url_prefix)) | 
					
						
							|  |  |  |     config.add_view(batch_crud, attr='delete', route_name='{0}.delete'.format(route_prefix), | 
					
						
							|  |  |  |                     permission='{0}.delete'.format(permission_prefix)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Batch rows grid | 
					
						
							|  |  |  |     config.add_route('{0}.rows'.format(route_prefix), '{0}{{uuid}}/rows/'.format(url_prefix)) | 
					
						
							|  |  |  |     config.add_view(row_grid, route_name='{0}.rows'.format(route_prefix), | 
					
						
							|  |  |  |                     renderer='/batch/rows.mako', | 
					
						
							|  |  |  |                     permission='{0}.view'.format(permission_prefix)) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-03-19 01:25:38 -05:00
										 |  |  |     # view batch row | 
					
						
							|  |  |  |     config.add_route('{0}.row.view'.format(route_prefix), '{0}row/{{uuid}}'.format(url_prefix)) | 
					
						
							|  |  |  |     config.add_view(row_crud, attr='read', route_name='{0}.row.view'.format(route_prefix), | 
					
						
							|  |  |  |                     renderer='{0}/row.view.mako'.format(template_prefix), | 
					
						
							|  |  |  |                     permission='{0}.view'.format(permission_prefix)) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-12 21:35:28 -06:00
										 |  |  |     # Bulk delete batch rows | 
					
						
							|  |  |  |     config.add_route('{0}.rows.bulk_delete'.format(route_prefix), '{0}{{uuid}}/rows/delete'.format(url_prefix)) | 
					
						
							|  |  |  |     config.add_view(row_grid, attr='bulk_delete', route_name='{0}.rows.bulk_delete'.format(route_prefix), | 
					
						
							| 
									
										
										
										
											2015-02-12 23:19:01 -06:00
										 |  |  |                     permission='{0}.edit'.format(permission_prefix)) | 
					
						
							| 
									
										
										
										
											2015-02-12 21:35:28 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-01-19 00:52:40 -06:00
										 |  |  |     # Delete batch row | 
					
						
							|  |  |  |     config.add_route('{0}.rows.delete'.format(route_prefix), '{0}delete-row/{{uuid}}'.format(url_prefix)) | 
					
						
							|  |  |  |     config.add_view(row_crud, attr='delete', route_name='{0}.rows.delete'.format(route_prefix), | 
					
						
							|  |  |  |                     permission='{0}.edit'.format(permission_prefix)) |