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'):
|
if hasattr(batch, 'delete_data'):
|
||||||
batch.delete_data(self.rattail_config)
|
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)
|
super(BatchMasterView, self).delete_instance(batch)
|
||||||
|
|
||||||
def get_fallback_templates(self, template, mobile=False):
|
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 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
|
return self.rows_editable and not batch.executed and not batch.complete
|
||||||
|
|
||||||
def row_deletable(self, row):
|
def row_deletable(self, row):
|
||||||
"""
|
"""
|
||||||
Batch rows are deletable only until batch has been executed.
|
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):
|
def _preconfigure_row_fieldset(self, fs):
|
||||||
fs.sequence.set(readonly=True)
|
fs.sequence.set(readonly=True)
|
||||||
|
@ -958,11 +963,12 @@ class BatchMasterView(MasterView):
|
||||||
renderer='json', permission='{}.worksheet'.format(permission_prefix))
|
renderer='json', permission='{}.worksheet'.format(permission_prefix))
|
||||||
|
|
||||||
# refresh batch data
|
# refresh batch data
|
||||||
config.add_route('{}.refresh'.format(route_prefix), '{}/{{uuid}}/refresh'.format(url_prefix))
|
if cls.refreshable:
|
||||||
config.add_view(cls, attr='refresh', route_name='{}.refresh'.format(route_prefix),
|
config.add_route('{}.refresh'.format(route_prefix), '{}/{{uuid}}/refresh'.format(url_prefix))
|
||||||
permission='{}.refresh'.format(permission_prefix))
|
config.add_view(cls, attr='refresh', route_name='{}.refresh'.format(route_prefix),
|
||||||
config.add_tailbone_permission(permission_prefix, '{}.refresh'.format(permission_prefix),
|
permission='{}.refresh'.format(permission_prefix))
|
||||||
"Refresh data for {}".format(model_title))
|
config.add_tailbone_permission(permission_prefix, '{}.refresh'.format(permission_prefix),
|
||||||
|
"Refresh data for {}".format(model_title))
|
||||||
|
|
||||||
# bulk delete rows
|
# bulk delete rows
|
||||||
if cls.rows_bulk_deletable:
|
if cls.rows_bulk_deletable:
|
||||||
|
|
|
@ -96,11 +96,13 @@ class BatchMasterView2(MasterView2, BatchMasterView):
|
||||||
def configure_row_grid(self, g):
|
def configure_row_grid(self, g):
|
||||||
super(BatchMasterView2, self).configure_row_grid(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_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)
|
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('status_code', "Status")
|
||||||
g.set_label('item_id', "Item ID")
|
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):
|
def render_row_status(self, row, column):
|
||||||
code = row.status_code
|
code = row.status_code
|
||||||
if code is None:
|
if code is None:
|
||||||
return ""
|
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:
|
if row.status_text:
|
||||||
return HTML.tag('span', title=row.status_text, c=text)
|
return HTML.tag('span', title=row.status_text, c=text)
|
||||||
return 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