Add basic UI support for "importer batch" feature
This commit is contained in:
parent
c3c77ed586
commit
fb140f24c1
69
tailbone/templates/batch/importer/view_row.mako
Normal file
69
tailbone/templates/batch/importer/view_row.mako
Normal file
|
@ -0,0 +1,69 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/master/view_row.mako" />
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
% if not batch.executed and request.has_perm('{}.delete_row'.format(permission_prefix)):
|
||||
<li>${h.link_to("Delete this Row", url('{}.delete_row'.format(route_prefix), uuid=batch.uuid, row_uuid=instance.uuid))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
||||
|
||||
% if instance.status_code == enum.IMPORTER_BATCH_ROW_STATUS_CREATE:
|
||||
<table class="diff monospace new">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>field name</th>
|
||||
<th>old value</th>
|
||||
<th>new value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
% for field in diff_fields:
|
||||
<tr>
|
||||
<td class="field">${field}</td>
|
||||
<td class="value old-value"> </td>
|
||||
<td class="value new-value">${repr(diff_new_values[field])}</td>
|
||||
</tr>
|
||||
% endfor
|
||||
</tbody>
|
||||
</table>
|
||||
% elif instance.status_code in (enum.IMPORTER_BATCH_ROW_STATUS_UPDATE, enum.IMPORTER_BATCH_ROW_STATUS_NOCHANGE):
|
||||
<table class="diff monospace dirty">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>field name</th>
|
||||
<th>old value</th>
|
||||
<th>new value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
% for field in diff_fields:
|
||||
<tr${' class="diff"' if diff_new_values[field] != diff_old_values[field] else ''|n}>
|
||||
<td class="field">${field}</td>
|
||||
<td class="value old-value">${repr(diff_old_values[field])}</td>
|
||||
<td class="value new-value">${repr(diff_new_values[field])}</td>
|
||||
</tr>
|
||||
% endfor
|
||||
</tbody>
|
||||
</table>
|
||||
% elif instance.status_code == enum.IMPORTER_BATCH_ROW_STATUS_DELETE:
|
||||
<table class="diff monospace deleted">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>field name</th>
|
||||
<th>old value</th>
|
||||
<th>new value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
% for field in diff_fields:
|
||||
<tr>
|
||||
<td class="field">${field}</td>
|
||||
<td class="value old-value">${repr(diff_old_values[field])}</td>
|
||||
<td class="value new-value"> </td>
|
||||
</tr>
|
||||
% endfor
|
||||
</tbody>
|
||||
</table>
|
||||
% endif
|
|
@ -439,7 +439,8 @@ class BatchMasterView(MasterView):
|
|||
"""
|
||||
if hasattr(batch, 'delete_data'):
|
||||
batch.delete_data(self.rattail_config)
|
||||
del batch.data_rows[:]
|
||||
if hasattr(batch, 'data_rows'):
|
||||
del batch.data_rows[:]
|
||||
super(BatchMasterView, self).delete_instance(batch)
|
||||
|
||||
def get_fallback_templates(self, template, mobile=False):
|
||||
|
@ -688,14 +689,18 @@ class BatchMasterView(MasterView):
|
|||
"""
|
||||
Batch rows are editable only until batch has been executed.
|
||||
"""
|
||||
batch = row.batch
|
||||
batch = self.get_parent(row)
|
||||
return self.rows_editable and not batch.executed and not batch.complete
|
||||
|
||||
def row_deletable(self, row):
|
||||
"""
|
||||
Batch rows are deletable only until batch has been executed.
|
||||
"""
|
||||
return self.rows_deletable and not row.batch.executed
|
||||
if self.rows_deletable:
|
||||
batch = self.get_parent(row)
|
||||
if not batch.executed:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _preconfigure_row_fieldset(self, fs):
|
||||
fs.sequence.set(readonly=True)
|
||||
|
@ -958,11 +963,12 @@ class BatchMasterView(MasterView):
|
|||
renderer='json', permission='{}.worksheet'.format(permission_prefix))
|
||||
|
||||
# refresh batch data
|
||||
config.add_route('{}.refresh'.format(route_prefix), '{}/{{uuid}}/refresh'.format(url_prefix))
|
||||
config.add_view(cls, attr='refresh', route_name='{}.refresh'.format(route_prefix),
|
||||
permission='{}.refresh'.format(permission_prefix))
|
||||
config.add_tailbone_permission(permission_prefix, '{}.refresh'.format(permission_prefix),
|
||||
"Refresh data for {}".format(model_title))
|
||||
if cls.refreshable:
|
||||
config.add_route('{}.refresh'.format(route_prefix), '{}/{{uuid}}/refresh'.format(url_prefix))
|
||||
config.add_view(cls, attr='refresh', route_name='{}.refresh'.format(route_prefix),
|
||||
permission='{}.refresh'.format(permission_prefix))
|
||||
config.add_tailbone_permission(permission_prefix, '{}.refresh'.format(permission_prefix),
|
||||
"Refresh data for {}".format(model_title))
|
||||
|
||||
# bulk delete rows
|
||||
if cls.rows_bulk_deletable:
|
||||
|
|
|
@ -96,11 +96,13 @@ class BatchMasterView2(MasterView2, BatchMasterView):
|
|||
def configure_row_grid(self, g):
|
||||
super(BatchMasterView2, self).configure_row_grid(g)
|
||||
|
||||
g.filters['status_code'].set_value_renderer(grids.filters.EnumValueRenderer(self.model_row_class.STATUS))
|
||||
if 'status_code' in g.filters:
|
||||
g.filters['status_code'].set_value_renderer(grids.filters.EnumValueRenderer(self.model_row_class.STATUS))
|
||||
|
||||
g.set_sort_defaults('sequence')
|
||||
|
||||
g.set_enum('status_code', self.model_row_class.STATUS)
|
||||
if self.model_row_class:
|
||||
g.set_enum('status_code', self.model_row_class.STATUS)
|
||||
|
||||
g.set_renderer('status_code', self.render_row_status)
|
||||
|
||||
|
@ -108,11 +110,14 @@ class BatchMasterView2(MasterView2, BatchMasterView):
|
|||
g.set_label('status_code', "Status")
|
||||
g.set_label('item_id', "Item ID")
|
||||
|
||||
def get_row_status_enum(self):
|
||||
return self.model_row_class.STATUS
|
||||
|
||||
def render_row_status(self, row, column):
|
||||
code = row.status_code
|
||||
if code is None:
|
||||
return ""
|
||||
text = self.model_row_class.STATUS.get(code, six.text_type(code))
|
||||
text = self.get_row_status_enum().get(code, six.text_type(code))
|
||||
if row.status_text:
|
||||
return HTML.tag('span', title=row.status_text, c=text)
|
||||
return text
|
||||
|
|
277
tailbone/views/batch/importer.py
Normal file
277
tailbone/views/batch/importer.py
Normal file
|
@ -0,0 +1,277 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2018 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 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 General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Views for importer batches
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals, absolute_import
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from rattail.core import Object
|
||||
from rattail.db import model
|
||||
|
||||
from tailbone import forms, forms2
|
||||
from tailbone.views.batch import BatchMasterView2 as BatchMasterView
|
||||
|
||||
|
||||
class ImporterBatchView(BatchMasterView):
|
||||
"""
|
||||
Master view for importer batches.
|
||||
"""
|
||||
model_class = model.ImporterBatch
|
||||
default_handler_spec = 'rattail.batch.importer:ImporterBatchHandler'
|
||||
model_title_plural = "Import / Export Batches"
|
||||
route_prefix = 'batch.importer'
|
||||
url_prefix = '/batches/importer'
|
||||
template_prefix = '/batch/importer'
|
||||
creatable = False
|
||||
refreshable = False
|
||||
bulk_deletable = True
|
||||
rows_downloadable_csv = False
|
||||
rows_bulk_deletable = True
|
||||
|
||||
grid_columns = [
|
||||
'id',
|
||||
'description',
|
||||
'host_title',
|
||||
'local_title',
|
||||
'importer_key',
|
||||
'created',
|
||||
'created_by',
|
||||
'rowcount',
|
||||
'executed',
|
||||
'executed_by',
|
||||
]
|
||||
|
||||
labels = {
|
||||
'host_title': "Source",
|
||||
'local_title': "Target",
|
||||
'importer_key': "Model",
|
||||
}
|
||||
|
||||
row_grid_columns = [
|
||||
'sequence',
|
||||
'object_key',
|
||||
'object_str',
|
||||
'status_code',
|
||||
]
|
||||
|
||||
def configure_fieldset(self, fs):
|
||||
fs.configure(
|
||||
include=[
|
||||
fs.id,
|
||||
fs.description,
|
||||
# fs.batch_handler_spec.readonly(),
|
||||
fs.import_handler_spec.readonly(),
|
||||
fs.host_title.readonly().label("Source"),
|
||||
fs.local_title.readonly().label("Target"),
|
||||
fs.importer_key.readonly().label("Model"),
|
||||
fs.notes,
|
||||
fs.created,
|
||||
fs.created_by,
|
||||
fs.row_table.readonly(),
|
||||
fs.rowcount,
|
||||
fs.executed,
|
||||
fs.executed_by,
|
||||
])
|
||||
|
||||
def delete_instance(self, batch):
|
||||
self.make_row_table(batch.row_table)
|
||||
self.current_row_table.drop()
|
||||
super(ImporterBatchView, self).delete_instance(batch)
|
||||
|
||||
def make_row_table(self, name):
|
||||
if not hasattr(self, 'current_row_table'):
|
||||
metadata = sa.MetaData(schema='batch', bind=self.Session.bind)
|
||||
self.current_row_table = sa.Table(name, metadata, autoload=True)
|
||||
|
||||
def get_row_data(self, batch):
|
||||
self.make_row_table(batch.row_table)
|
||||
return self.Session.query(self.current_row_table)
|
||||
|
||||
def get_row_status_enum(self):
|
||||
return self.enum.IMPORTER_BATCH_ROW_STATUS
|
||||
|
||||
def configure_row_grid(self, g):
|
||||
super(ImporterBatchView, self).configure_row_grid(g)
|
||||
|
||||
def make_filter(field, **kwargs):
|
||||
column = getattr(self.current_row_table.c, field)
|
||||
g.set_filter(field, column, **kwargs)
|
||||
|
||||
make_filter('object_key')
|
||||
make_filter('object_str')
|
||||
make_filter('status_code', label="Status",
|
||||
value_enum=self.enum.IMPORTER_BATCH_ROW_STATUS)
|
||||
|
||||
def make_sorter(field):
|
||||
column = getattr(self.current_row_table.c, field)
|
||||
g.sorters[field] = lambda q, d: q.order_by(getattr(column, d)())
|
||||
|
||||
make_sorter('sequence')
|
||||
make_sorter('object_key')
|
||||
make_sorter('object_str')
|
||||
make_sorter('status_code')
|
||||
|
||||
g.set_sort_defaults('sequence')
|
||||
|
||||
g.set_label('object_str', "Object Description")
|
||||
|
||||
g.set_link('sequence')
|
||||
g.set_link('object_key')
|
||||
g.set_link('object_str')
|
||||
|
||||
def row_grid_extra_class(self, row, i):
|
||||
if row.status_code == self.enum.IMPORTER_BATCH_ROW_STATUS_DELETE:
|
||||
return 'warning'
|
||||
if row.status_code in (self.enum.IMPORTER_BATCH_ROW_STATUS_CREATE,
|
||||
self.enum.IMPORTER_BATCH_ROW_STATUS_UPDATE):
|
||||
return 'notice'
|
||||
|
||||
def get_row_action_route_kwargs(self, row):
|
||||
return {
|
||||
'uuid': self.current_row_table.name,
|
||||
'row_uuid': row.uuid,
|
||||
}
|
||||
|
||||
def get_row_instance(self):
|
||||
batch_uuid = self.request.matchdict['uuid']
|
||||
row_uuid = self.request.matchdict['row_uuid']
|
||||
self.make_row_table(batch_uuid)
|
||||
return self.Session.query(self.current_row_table)\
|
||||
.filter(self.current_row_table.c.uuid == row_uuid)\
|
||||
.one()
|
||||
|
||||
def get_parent(self, row):
|
||||
uuid = self.current_row_table.name
|
||||
return self.Session.query(model.ImporterBatch).get(uuid)
|
||||
|
||||
def get_row_instance_title(self, row):
|
||||
if row.object_str:
|
||||
return row.object_str
|
||||
if row.object_key:
|
||||
return row.object_key
|
||||
return "Row {}".format(row.sequence)
|
||||
|
||||
def template_kwargs_view_row(self, **kwargs):
|
||||
batch = kwargs['parent_instance']
|
||||
row = kwargs['instance']
|
||||
kwargs['batch'] = batch
|
||||
kwargs['instance_title'] = batch.id_str
|
||||
|
||||
fields = set()
|
||||
old_values = {}
|
||||
new_values = {}
|
||||
for col in self.current_row_table.c:
|
||||
if col.name.startswith('key_'):
|
||||
field = col.name[4:]
|
||||
fields.add(field)
|
||||
old_values[field] = new_values[field] = getattr(row, col.name)
|
||||
elif col.name.startswith('pre_'):
|
||||
field = col.name[4:]
|
||||
fields.add(field)
|
||||
old_values[field] = getattr(row, col.name)
|
||||
elif col.name.startswith('post_'):
|
||||
field = col.name[5:]
|
||||
fields.add(field)
|
||||
new_values[field] = getattr(row, col.name)
|
||||
|
||||
kwargs['diff_fields'] = sorted(fields)
|
||||
kwargs['diff_old_values'] = old_values
|
||||
kwargs['diff_new_values'] = new_values
|
||||
return kwargs
|
||||
|
||||
def make_row_form(self, instance=None, factory=None, fields=None, schema=None, **kwargs):
|
||||
"""
|
||||
Creates a new form for the given model class/instance
|
||||
"""
|
||||
if factory is None:
|
||||
factory = forms2.Form
|
||||
if fields is None:
|
||||
fields = ['sequence', 'object_key', 'object_str', 'status_code']
|
||||
for col in self.current_row_table.c:
|
||||
if col.name.startswith('key_'):
|
||||
fields.append(col.name)
|
||||
|
||||
if not self.creating:
|
||||
kwargs['model_instance'] = instance
|
||||
kwargs = self.make_row_form_kwargs(**kwargs)
|
||||
form = factory(fields, schema, **kwargs)
|
||||
self.configure_row_form(form)
|
||||
return form
|
||||
|
||||
def make_row_form_kwargs(self, **kwargs):
|
||||
"""
|
||||
Return a dictionary of kwargs to be passed to the factory when creating
|
||||
new row form instances.
|
||||
"""
|
||||
defaults = {
|
||||
'request': self.request,
|
||||
'readonly': self.viewing,
|
||||
'model_class': getattr(self, 'model_class', None),
|
||||
'action_url': self.request.current_route_url(_query=None),
|
||||
}
|
||||
instance = kwargs['model_instance']
|
||||
defaults.update(kwargs)
|
||||
return defaults
|
||||
|
||||
def configure_row_form(self, f):
|
||||
"""
|
||||
Configure the row form.
|
||||
"""
|
||||
# object_str
|
||||
f.set_label('object_str', "Object Description")
|
||||
|
||||
# status_code
|
||||
f.set_renderer('status_code', self.render_row_status_code)
|
||||
f.set_label('status_code', "Status")
|
||||
|
||||
def render_row_status_code(self, row, field):
|
||||
status = self.enum.IMPORTER_BATCH_ROW_STATUS[row.status_code]
|
||||
if row.status_code == self.enum.IMPORTER_BATCH_ROW_STATUS_UPDATE and row.status_text:
|
||||
return "{} ({})".format(status, row.status_text)
|
||||
return status
|
||||
|
||||
def delete_row(self):
|
||||
row = self.get_row_instance()
|
||||
if not row:
|
||||
raise self.notfound()
|
||||
|
||||
batch = self.get_parent(row)
|
||||
query = self.current_row_table.delete().where(self.current_row_table.c.uuid == row.uuid)
|
||||
query.execute()
|
||||
batch.rowcount -= 1
|
||||
return self.redirect(self.get_action_url('view', batch))
|
||||
|
||||
def bulk_delete_rows(self):
|
||||
batch = self.get_instance()
|
||||
query = self.get_effective_row_data(sort=False)
|
||||
batch.rowcount -= query.count()
|
||||
delete_query = self.current_row_table.delete().where(self.current_row_table.c.uuid.in_([row.uuid for row in query]))
|
||||
delete_query.execute()
|
||||
return self.redirect(self.get_action_url('view', batch))
|
||||
|
||||
|
||||
def includeme(config):
|
||||
ImporterBatchView.defaults(config)
|
Loading…
Reference in a new issue