Add new BatchMasterView for new-style batches.

This commit is contained in:
Lance Edgar 2016-02-14 16:47:35 -06:00
parent 7338560fc3
commit 62221a1a25
14 changed files with 1112 additions and 93 deletions

View file

@ -63,7 +63,8 @@ class FileFieldRenderer(fsblob.FileFieldRenderer):
def get_url(self, filename): def get_url(self, filename):
batch = self.field.parent.model batch = self.field.parent.model
return self.view.request.route_url('{0}.download'.format(self.view.route_prefix), uuid=batch.uuid) return self.view.request.route_url('{}.download'.format(self.view.get_route_prefix()),
uuid=batch.uuid)
def render(self, **kwargs): def render(self, **kwargs):
return Base.render(self, **kwargs) return Base.render(self, **kwargs)

View file

@ -79,7 +79,7 @@ class Grid(object):
self.width = width self.width = width
self.checkboxes = checkboxes self.checkboxes = checkboxes
self.row_attrs = row_attrs self.row_attrs = row_attrs or {}
self.cell_attrs = cell_attrs self.cell_attrs = cell_attrs
def get_default_filters(self): def get_default_filters(self):
@ -160,10 +160,10 @@ class Grid(object):
instance attributes. instance attributes.
""" """
# Initial settings come from class defaults. # Initial settings come from class defaults.
settings = { settings = {}
'sortkey': self.default_sortkey, if self.sortable:
'sortdir': self.default_sortdir, settings['sortkey'] = self.default_sortkey
} settings['sortdir'] = self.default_sortdir
if self.pageable: if self.pageable:
settings['pagesize'] = self.default_pagesize settings['pagesize'] = self.default_pagesize
settings['page'] = self.default_page settings['page'] = self.default_page
@ -514,6 +514,7 @@ class Grid(object):
Render the complete grid, including filters. Render the complete grid, including filters.
""" """
kwargs['grid'] = self kwargs['grid'] = self
kwargs.setdefault('allow_save_defaults', True)
return render(template, kwargs) return render(template, kwargs)
def render_grid(self, template='/newgrids/grid.mako', **kwargs): def render_grid(self, template='/newgrids/grid.mako', **kwargs):

View file

@ -8,7 +8,7 @@
% if master.viewable and request.has_perm('{}.view'.format(permission_prefix)): % if master.viewable and request.has_perm('{}.view'.format(permission_prefix)):
<li>${h.link_to("View this {}".format(model_title), action_url('view', instance))}</li> <li>${h.link_to("View this {}".format(model_title), action_url('view', instance))}</li>
% endif % endif
% if master.deletable and master.deletable_instance(instance) and request.has_perm('{}.delete'.format(permission_prefix)): % if master.deletable and instance_deletable and request.has_perm('{}.delete'.format(permission_prefix)):
<li>${h.link_to("Delete this {}".format(model_title), action_url('delete', instance))}</li> <li>${h.link_to("Delete this {}".format(model_title), action_url('delete', instance))}</li>
% endif % endif
% if master.creatable and request.has_perm('{}.create'.format(permission_prefix)): % if master.creatable and request.has_perm('{}.create'.format(permission_prefix)):

View file

@ -5,10 +5,10 @@
<%def name="context_menu_items()"> <%def name="context_menu_items()">
<li>${h.link_to("Back to {}".format(model_title_plural), index_url)}</li> <li>${h.link_to("Back to {}".format(model_title_plural), index_url)}</li>
% if master.editable and request.has_perm('{}.edit'.format(permission_prefix)): % if master.editable and instance_editable and request.has_perm('{}.edit'.format(permission_prefix)):
<li>${h.link_to("Edit this {}".format(model_title), action_url('edit', instance))}</li> <li>${h.link_to("Edit this {}".format(model_title), action_url('edit', instance))}</li>
% endif % endif
% if master.deletable and master.deletable_instance(instance) and request.has_perm('{}.delete'.format(permission_prefix)): % if master.deletable and instance_deletable and request.has_perm('{}.delete'.format(permission_prefix)):
<li>${h.link_to("Delete this {}".format(model_title), action_url('delete', instance))}</li> <li>${h.link_to("Delete this {}".format(model_title), action_url('delete', instance))}</li>
% endif % endif
% if master.creatable and request.has_perm('{}.create'.format(permission_prefix)): % if master.creatable and request.has_perm('{}.create'.format(permission_prefix)):

View file

@ -0,0 +1,3 @@
## -*- coding: utf-8 -*-
<%inherit file="/master/create.mako" />
${parent.body()}

View file

@ -0,0 +1,59 @@
## -*- coding: utf-8 -*-
<%inherit file="/master/edit.mako" />
<%def name="head_tags()">
${parent.head_tags()}
${h.javascript_link(request.static_url('tailbone:static/js/jquery.ui.tailbone.js'))}
<script type="text/javascript">
$(function() {
$('.newgrid-wrapper').gridwrapper();
$('#save-refresh').click(function() {
var form = $(this).parents('form');
form.append($('<input type="hidden" name="refresh" value="true" />'));
form.submit();
});
$('#execute-batch').click(function() {
$(this).button('option', 'label', "Executing, please wait...").button('disable');
location.href = '${url('{}.execute'.format(route_prefix), uuid=batch.uuid)}';
});
});
</script>
<style type="text/css">
.newgrid-wrapper {
margin-top: 10px;
}
</style>
</%def>
<%def name="buttons()">
<div class="buttons">
% if master.refreshable:
${h.submit('save-refresh', "Save & Refresh Data")}
% endif
% if not batch.executed and request.has_perm('{}.execute'.format(permission_prefix)):
<button type="button" id="execute-batch"${'' if execute_enabled else ' disabled="disabled"'}>${execute_title}</button>
% endif
</div>
</%def>
<%def name="grid_tools()">
% if not batch.executed:
<p>${h.link_to("Delete all rows matching current search", url('{}.rows.bulk_delete'.format(route_prefix), uuid=batch.uuid))}</p>
% endif
</%def>
<ul id="context-menu">
${self.context_menu_items()}
</ul>
<div class="form-wrapper">
${form.render(buttons=capture(buttons))|n}
</div><!-- form-wrapper -->
${rows_grid.render_complete(allow_save_defaults=False, tools=capture(self.grid_tools))|n}

View file

@ -0,0 +1,3 @@
## -*- coding: utf-8 -*-
<%inherit file="/master/index.mako" />
${parent.body()}

View file

@ -0,0 +1,54 @@
## -*- coding: utf-8 -*-
<%inherit file="/master/view.mako" />
<%def name="head_tags()">
${parent.head_tags()}
${h.javascript_link(request.static_url('tailbone:static/js/jquery.ui.tailbone.js'))}
<script type="text/javascript">
$(function() {
$('.newgrid-wrapper').gridwrapper();
$('#execute-batch').click(function() {
$(this).button('option', 'label', "Executing, please wait...").button('disable');
location.href = '${url('{}.execute'.format(route_prefix), uuid=batch.uuid)}';
});
});
</script>
<style type="text/css">
.newgrid-wrapper {
margin-top: 10px;
}
</style>
</%def>
<%def name="context_menu_items()">
${parent.context_menu_items()}
% if request.has_perm('{}.csv'.format(permission_prefix)):
<li>${h.link_to("Download row data as CSV", url('{}.csv'.format(route_prefix), uuid=batch.uuid))}</li>
% endif
</%def>
<%def name="buttons()">
<div class="buttons">
% if not form.readonly and batch.refreshable:
${h.submit('save-refresh', "Save & Refresh Data")}
% endif
% if not batch.executed and request.has_perm('{}.execute'.format(permission_prefix)):
<button type="button" id="execute-batch"${'' if execute_enabled else ' disabled="disabled"'}>${execute_title}</button>
% endif
</div>
</%def>
<ul id="context-menu">
${self.context_menu_items()}
</ul>
<div class="form-wrapper">
${form.render(form_id='batch-form', buttons=capture(buttons))|n}
</div><!-- form-wrapper -->
${rows_grid|n}

View file

@ -0,0 +1,19 @@
## -*- coding: utf-8 -*-
<%inherit file="/master/view.mako" />
<%def name="title()">${model_title}</%def>
<%def name="context_menu_items()">
<li>${h.link_to("Back to {}".format(batch_model_title), index_url)}</li>
% if master.rows_editable and instance_editable and request.has_perm('{}.edit'.format(permission_prefix)):
<li>${h.link_to("Edit this {}".format(model_title), action_url('edit', instance))}</li>
% endif
% if master.rows_deletable and instance_deletable and request.has_perm('{}.delete'.format(permission_prefix)):
<li>${h.link_to("Delete this {}".format(model_title), action_url('delete', instance))}</li>
% endif
% if master.rows_creatable and request.has_perm('{}.create'.format(permission_prefix)):
<li>${h.link_to("Create a new {}".format(model_title), url('{}.create'.format(route_prefix)))}</li>
% endif
</%def>
${parent.body()}

View file

@ -1,7 +1,7 @@
## -*- coding: utf-8 -*- ## -*- coding: utf-8 -*-
<div class="newgrid-wrapper"> <div class="newgrid-wrapper">
% if grid.filterable: % if grid.filterable:
${grid.render_filters()|n} ${grid.render_filters(allow_save_defaults=allow_save_defaults)|n}
% endif % endif
% if tools: % if tools:
<div class="grid-tools"> <div class="grid-tools">

View file

@ -29,7 +29,7 @@
</select> </select>
${form.tag('button', type='button', id='default-filters', c="Default View")} ${form.tag('button', type='button', id='default-filters', c="Default View")}
${form.tag('button', type='button', id='clear-filters', c="No Filters")} ${form.tag('button', type='button', id='clear-filters', c="No Filters")}
% if request.user: % if allow_save_defaults and request.user:
${form.tag('button', type='button', id='save-defaults', c="Save Defaults")} ${form.tag('button', type='button', id='save-defaults', c="Save Defaults")}
% endif % endif
</div> </div>

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@
################################################################################ ################################################################################
# #
# Rattail -- Retail Software Framework # Rattail -- Retail Software Framework
# Copyright © 2010-2015 Lance Edgar # Copyright © 2010-2016 Lance Edgar
# #
# This file is part of Rattail. # This file is part of Rattail.
# #
@ -24,7 +24,9 @@
Model Master View Model Master View
""" """
from __future__ import unicode_literals from __future__ import unicode_literals, absolute_import
import re
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy import orm from sqlalchemy import orm
@ -100,18 +102,24 @@ class MasterView(View):
View for creating a new model record. View for creating a new model record.
""" """
self.creating = True self.creating = True
form = self.make_form(self.model_class) form = self.make_form(self.get_model_class())
if self.request.method == 'POST': if self.request.method == 'POST':
if form.validate(): if form.validate():
self.before_create(form) self.save_create_form(form)
form.save()
instance = form.fieldset.model instance = form.fieldset.model
self.after_create(instance) self.after_create(instance)
self.request.session.flash("{} {} has been created.".format( self.request.session.flash("{} {} has been created.".format(
self.get_model_title(), instance)) self.get_model_title(), instance))
return self.redirect(self.get_action_url('view', instance)) return self.redirect_after_create(instance)
return self.render_to_response('create', {'form': form}) return self.render_to_response('create', {'form': form})
def save_create_form(self, form):
self.before_create(form)
form.save()
def redirect_after_create(self, instance):
return self.redirect(self.get_action_url('view', instance))
def view(self): def view(self):
""" """
View for viewing details of an existing model record. View for viewing details of an existing model record.
@ -119,8 +127,11 @@ class MasterView(View):
self.viewing = True self.viewing = True
instance = self.get_instance() instance = self.get_instance()
form = self.make_form(instance) form = self.make_form(instance)
return self.render_to_response('view', {'instance': instance, return self.render_to_response('view', {
'instance': instance,
'instance_title': self.get_instance_title(instance), 'instance_title': self.get_instance_title(instance),
'instance_editable': self.editable_instance(instance),
'instance_deletable': self.deletable_instance(instance),
'form': form}) 'form': form})
def edit(self): def edit(self):
@ -130,17 +141,27 @@ class MasterView(View):
self.editing = True self.editing = True
instance = self.get_instance() instance = self.get_instance()
form = self.make_form(instance) form = self.make_form(instance)
if self.request.method == 'POST': if self.request.method == 'POST':
if form.validate(): if form.validate():
self.save_form(form) self.save_edit_form(form)
self.after_edit(instance)
self.request.session.flash("{0} {1} has been updated.".format( self.request.session.flash("{0} {1} has been updated.".format(
self.get_model_title(), self.get_instance_title(instance))) self.get_model_title(), self.get_instance_title(instance)))
return self.redirect(self.get_action_url('view', instance)) return self.redirect_after_edit(instance)
return self.render_to_response('edit', {'instance': instance,
return self.render_to_response('edit', {
'instance': instance,
'instance_title': self.get_instance_title(instance), 'instance_title': self.get_instance_title(instance),
'instance_deletable': self.deletable_instance(instance),
'form': form}) 'form': form})
def save_edit_form(self, form):
self.save_form(form)
self.after_edit(form.fieldset.model)
def redirect_after_edit(self, instance):
return self.redirect(self.get_action_url('view', instance))
def delete(self): def delete(self):
""" """
View for deleting an existing model record. View for deleting an existing model record.
@ -181,7 +202,7 @@ class MasterView(View):
Returns the data model class for which the master view exists. Returns the data model class for which the master view exists.
""" """
if not hasattr(cls, 'model_class') and error: if not hasattr(cls, 'model_class') and error:
raise NotImplementedError("You must define the `model_class` for: {0}".format(cls)) raise NotImplementedError("You must define the `model_class` for: {}".format(cls))
return getattr(cls, 'model_class', None) return getattr(cls, 'model_class', None)
@classmethod @classmethod
@ -213,7 +234,9 @@ class MasterView(View):
""" """
if hasattr(cls, 'model_title'): if hasattr(cls, 'model_title'):
return cls.model_title return cls.model_title
return cls.model_class.__name__ title = cls.get_model_class().__name__
# convert "CamelCase" to "Camel Case"
return re.sub(r'([a-z])([A-Z])', r'\g<1> \g<2>', title)
@classmethod @classmethod
def get_model_title_plural(cls): def get_model_title_plural(cls):
@ -279,7 +302,7 @@ class MasterView(View):
If that doesn't work, another attempt will be made using '/master' as If that doesn't work, another attempt will be made using '/master' as
the template prefix. the template prefix.
""" """
data.update({ context = {
'master': self, 'master': self,
'model_title': self.get_model_title(), 'model_title': self.get_model_title(),
'model_title_plural': self.get_model_title_plural(), 'model_title_plural': self.get_model_title_plural(),
@ -287,16 +310,33 @@ class MasterView(View):
'permission_prefix': self.get_permission_prefix(), 'permission_prefix': self.get_permission_prefix(),
'index_url': self.get_index_url(), 'index_url': self.get_index_url(),
'action_url': self.get_action_url, 'action_url': self.get_action_url,
}) }
data.update(self.template_kwargs(**data)) context.update(data)
if hasattr(self, 'template_kwargs_{0}'.format(template)): context.update(self.template_kwargs(**context))
data.update(getattr(self, 'template_kwargs_{0}'.format(template))(**data)) if hasattr(self, 'template_kwargs_{}'.format(template)):
context.update(getattr(self, 'template_kwargs_{}'.format(template))(**context))
# First try the template path most specific to the view.
try: try:
return render_to_response('{0}/{1}.mako'.format(self.get_template_prefix(), template), return render_to_response('{}/{}.mako'.format(self.get_template_prefix(), template),
data, request=self.request) context, request=self.request)
except IOError: except IOError:
return render_to_response('/master/{0}.mako'.format(template),
data, request=self.request) # Failing that, try one or more fallback templates.
for fallback in self.get_fallback_templates(template):
try:
return render_to_response(fallback, context, request=self.request)
except IOError:
pass
# If we made it all the way here, we found no templates at all, in
# which case re-attempt the first and let that error raise on up.
return render_to_response('{}/{}.mako'.format(self.get_template_prefix(), template),
context, request=self.request)
def get_fallback_templates(self, template):
return ['/master/{}.mako'.format(template)]
def template_kwargs(self, **kwargs): def template_kwargs(self, **kwargs):
""" """
@ -439,10 +479,14 @@ class MasterView(View):
data = self.get_data(session=kwargs.get('session')) data = self.get_data(session=kwargs.get('session'))
kwargs = self.make_grid_kwargs(**kwargs) kwargs = self.make_grid_kwargs(**kwargs)
grid = factory(key, self.request, data=data, model_class=self.get_model_class(error=False), **kwargs) grid = factory(key, self.request, data=data, model_class=self.get_model_class(error=False), **kwargs)
self._preconfigure_grid(grid)
self.configure_grid(grid) self.configure_grid(grid)
grid.load_settings() grid.load_settings()
return grid return grid
def _preconfigure_grid(self, grid):
pass
def configure_grid(self, grid): def configure_grid(self, grid):
""" """
Configure the grid, customizing as necessary. Subclasses are Configure the grid, customizing as necessary. Subclasses are
@ -479,7 +523,17 @@ class MasterView(View):
users. You would modify the base query to hide what you wanted, users. You would modify the base query to hide what you wanted,
regardless of the user's filter selections. regardless of the user's filter selections.
""" """
return session.query(self.model_class) return session.query(self.get_model_class())
def get_effective_query(self, session):
"""
Convenience method which returns the "effective" query for the master
grid, filtered and sorted to match what would show on the UI, but not
paged etc.
"""
grid = self.make_grid(session=session, pageable=False,
main_actions=[], more_actions=[])
return grid._fa_grid.rows
def checkbox(self, instance): def checkbox(self, instance):
""" """
@ -507,7 +561,7 @@ class MasterView(View):
doing a database lookup. If the instance cannot be found, raises 404. doing a database lookup. If the instance cannot be found, raises 404.
""" """
key = self.request.matchdict[self.get_model_key()] key = self.request.matchdict[self.get_model_key()]
instance = self.Session.query(self.model_class).get(key) instance = self.Session.query(self.get_model_class()).get(key)
if not instance: if not instance:
raise httpexceptions.HTTPNotFound() raise httpexceptions.HTTPNotFound()
return instance return instance
@ -529,7 +583,9 @@ class MasterView(View):
kwargs.setdefault('editing', self.editing) kwargs.setdefault('editing', self.editing)
fieldset = self.make_fieldset(instance) fieldset = self.make_fieldset(instance)
self._preconfigure_fieldset(fieldset)
self.configure_fieldset(fieldset) self.configure_fieldset(fieldset)
self._postconfigure_fieldset(fieldset)
kwargs.setdefault('action_url', self.request.current_route_url(_query=None)) kwargs.setdefault('action_url', self.request.current_route_url(_query=None))
if self.creating: if self.creating:
@ -553,12 +609,18 @@ class MasterView(View):
fieldset.prettify = prettify fieldset.prettify = prettify
return fieldset return fieldset
def _preconfigure_fieldset(self, fieldset):
pass
def configure_fieldset(self, fieldset): def configure_fieldset(self, fieldset):
""" """
Configure the given fieldset. Configure the given fieldset.
""" """
fieldset.configure() fieldset.configure()
def _postconfigure_fieldset(self, fieldset):
pass
def before_create(self, form): def before_create(self, form):
""" """
Event hook, called just after the form to create a new instance has Event hook, called just after the form to create a new instance has
@ -570,6 +632,14 @@ class MasterView(View):
Event hook, called just after a new instance is saved. Event hook, called just after a new instance is saved.
""" """
def editable_instance(self, instance):
"""
Returns boolean indicating whether or not the given instance can be
considered "editable". Returns ``True`` by default; override as
necessary.
"""
return True
def after_edit(self, instance): def after_edit(self, instance):
""" """
Event hook, called just after an existing instance is saved. Event hook, called just after an existing instance is saved.
@ -617,7 +687,7 @@ class MasterView(View):
""" """
Provide default configuration for a master view. Provide default configuration for a master view.
""" """
return cls._defaults(config) cls._defaults(config)
@classmethod @classmethod
def _defaults(cls, config): def _defaults(cls, config):

View file

@ -339,11 +339,7 @@ class ProductsView(MasterView):
Threat target for making a batch from current products query. Threat target for making a batch from current products query.
""" """
session = RattailSession() session = RattailSession()
products = self.get_effective_query(session)
grid = self.make_grid(session=session, pageable=False,
main_actions=[], more_actions=[])
products = grid._fa_grid.rows
batch = provider.make_batch(session, products, progress) batch = provider.make_batch(session, products, progress)
if not batch: if not batch:
session.rollback() session.rollback()