Add initial support for mobile "quick row" feature, for ordering

at least for now, ordering only, but hopefully much more soon...
This commit is contained in:
Lance Edgar 2018-07-12 22:53:29 -05:00
parent aa6e540abd
commit 68bd3047c4
10 changed files with 143 additions and 23 deletions

View file

@ -98,6 +98,7 @@ $(document).on('click', '#datasync-restart', function() {
}); });
// TODO: this should go away in favor of quick_row approach
// handle global keypress on product batch "row" page, for sake of scanner wedge // handle global keypress on product batch "row" page, for sake of scanner wedge
var product_batch_routes = [ var product_batch_routes = [
'mobile.batch.inventory.view', 'mobile.batch.inventory.view',
@ -134,6 +135,23 @@ $(document).on('keypress', function(event) {
}); });
// handle ENTER press for quick_row forms
$(document).on('keypress', function(event) {
var quick_row = $('.ui-page-active #quick_row_entry');
if (quick_row.length) {
if (quick_row.is(':focus')) {
if (event.which == 13) { // ENTER
if (quick_row.val()) {
var form = quick_row.parents('form:first');
form.submit();
return false;
}
}
}
}
});
// when numeric keypad button is clicked, update quantity accordingly // when numeric keypad button is clicked, update quantity accordingly
$(document).on('click', '.quantity-keypad-thingy .keypad-button', function() { $(document).on('click', '.quantity-keypad-thingy .keypad-button', function() {
var keypad = $(this).parents('.quantity-keypad-thingy'); var keypad = $(this).parents('.quantity-keypad-thingy');

View file

@ -8,9 +8,9 @@
${h.javascript_link('https://code.jquery.com/jquery-1.12.4.min.js')} ${h.javascript_link('https://code.jquery.com/jquery-1.12.4.min.js')}
${h.javascript_link('https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.js')} ${h.javascript_link('https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.js')}
${h.javascript_link(request.static_url('tailbone:static/js/jquery.ui.tailbone.mobile.js'))} ${h.javascript_link(request.static_url('tailbone:static/js/jquery.ui.tailbone.mobile.js') + '?ver={}'.format(tailbone.__version__))}
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.mobile.js'))} ${h.javascript_link(request.static_url('tailbone:static/js/tailbone.mobile.js') + '?ver={}'.format(tailbone.__version__))}
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.mobile.receiving.js'))} ${h.javascript_link(request.static_url('tailbone:static/js/tailbone.mobile.receiving.js') + '?ver={}'.format(tailbone.__version__))}
${self.extra_javascript()} ${self.extra_javascript()}
## since jquery mobile will "utterly cache" the first page which is loaded ## since jquery mobile will "utterly cache" the first page which is loaded
@ -33,7 +33,7 @@
% endif % endif
${h.stylesheet_link('https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.css')} ${h.stylesheet_link('https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.css')}
${h.stylesheet_link(request.static_url('tailbone:static/css/mobile.css'))} ${h.stylesheet_link(request.static_url('tailbone:static/css/mobile.css') + '?ver={}'.format(tailbone.__version__))}
% if not request.rattail_config.production(): % if not request.rattail_config.production():
<style type="text/css"> <style type="text/css">
.ui-page-theme-a { background-image: url(${request.static_url('tailbone:static/img/testing.png')}); } .ui-page-theme-a { background-image: url(${request.static_url('tailbone:static/img/testing.png')}); }

View file

@ -3,14 +3,6 @@
${parent.body()} ${parent.body()}
% if master.has_rows:
% if master.mobile_rows_creatable and not batch.executed and not batch.complete:
${h.link_to("Add Item", url('mobile.{}.create_row'.format(route_prefix), uuid=instance.uuid), class_='ui-btn ui-corner-all')}
% endif
<br />
${grid.render_complete()|n}
% endif
% if not batch.executed: % if not batch.executed:
% if request.has_perm('{}.edit'.format(permission_prefix)): % if request.has_perm('{}.edit'.format(permission_prefix)):
% if batch.complete: % if batch.complete:

View file

@ -12,3 +12,22 @@
<%def name="page_title()">${h.link_to(index_title, index_url)} &raquo; ${instance_title}</%def> <%def name="page_title()">${h.link_to(index_title, index_url)} &raquo; ${instance_title}</%def>
${form.render()|n} ${form.render()|n}
% if master.has_rows:
% if master.mobile_rows_creatable and master.rows_creatable_for(instance):
## TODO: this seems like a poor choice of names? what are we really testing for here?
% if master.mobile_rows_creatable_via_browse:
${h.link_to(add_item_title, url('mobile.{}.create_row'.format(route_prefix), uuid=instance.uuid), class_='ui-btn ui-corner-all')}
% endif
% if master.mobile_rows_quickable:
${h.form(url('mobile.{}.quick_row'.format(route_prefix), uuid=instance.uuid))}
${h.csrf_token(request)}
${h.text('quick_row_entry', placeholder=quick_row_entry_placeholder, autocomplete='off', **{'data-type': 'search', 'data-url': url('mobile.{}.quick_row'.format(route_prefix), uuid=instance.uuid)})}
${h.end_form()}
% endif
% endif
<br />
${grid.render_complete()|n}
% endif

View file

@ -5,7 +5,7 @@
<%def name="page_title()">${h.link_to(index_title, index_url)} &raquo; ${h.link_to(parent_title, parent_url)} &raquo; ${instance_title}</%def> <%def name="page_title()">${h.link_to(index_title, index_url)} &raquo; ${h.link_to(parent_title, parent_url)} &raquo; ${instance_title}</%def>
${parent.body()} ${form.render()|n}
% if master.mobile_rows_editable and instance_editable and request.has_perm('{}.edit_row'.format(permission_prefix)): % if master.mobile_rows_editable and instance_editable and request.has_perm('{}.edit_row'.format(permission_prefix)):
${h.link_to("Edit", url('mobile.{}.edit_row'.format(route_prefix), uuid=instance.batch_uuid, row_uuid=instance.uuid), class_='ui-btn')} ${h.link_to("Edit", url('mobile.{}.edit_row'.format(route_prefix), uuid=instance.batch_uuid, row_uuid=instance.uuid), class_='ui-btn')}

View file

@ -149,6 +149,12 @@ class BatchMasterView(MasterView):
kwargs['handler'] = self.handler kwargs['handler'] = self.handler
kwargs['execute_title'] = self.get_execute_title(batch) kwargs['execute_title'] = self.get_execute_title(batch)
kwargs['execute_enabled'] = self.instance_executable(batch) kwargs['execute_enabled'] = self.instance_executable(batch)
if kwargs['mobile']:
if self.mobile_rows_creatable:
kwargs.setdefault('add_item_title', "Add Item")
if self.mobile_rows_quickable:
kwargs.setdefault('quick_row_entry_placeholder', "Enter {}".format(
self.rattail_config.product_key_title()))
if kwargs['execute_enabled']: if kwargs['execute_enabled']:
url = self.get_action_url('execute', batch) url = self.get_action_url('execute', batch)
kwargs['execute_form'] = self.make_execute_form(batch, action_url=url) kwargs['execute_form'] = self.make_execute_form(batch, action_url=url)
@ -475,9 +481,14 @@ class BatchMasterView(MasterView):
def rows_creatable_for(self, batch): def rows_creatable_for(self, batch):
""" """
Only allow creating new rows on a batch if it hasn't yet been executed. Only allow creating new rows on a batch if it hasn't yet been executed
or marked complete.
""" """
return not batch.executed if batch.executed:
return False
if batch.complete:
return False
return True
def configure_row_grid(self, g): def configure_row_grid(self, g):
super(BatchMasterView, self).configure_row_grid(g) super(BatchMasterView, self).configure_row_grid(g)

View file

@ -45,6 +45,7 @@ from rattail.csvutil import UnicodeDictWriter
from rattail.files import temp_path from rattail.files import temp_path
from rattail.excel import ExcelWriter from rattail.excel import ExcelWriter
import colander
import deform import deform
from pyramid import httpexceptions from pyramid import httpexceptions
from pyramid.renderers import get_renderer, render_to_response, render from pyramid.renderers import get_renderer, render_to_response, render
@ -132,6 +133,8 @@ class MasterView(View):
rows_downloadable_csv = False rows_downloadable_csv = False
mobile_rows_creatable = False mobile_rows_creatable = False
mobile_rows_creatable_via_browse = False
mobile_rows_quickable = False
mobile_rows_filterable = False mobile_rows_filterable = False
mobile_rows_viewable = False mobile_rows_viewable = False
mobile_rows_editable = False mobile_rows_editable = False
@ -1060,6 +1063,46 @@ class MasterView(View):
self.configure_mobile_row_form(form) self.configure_mobile_row_form(form)
return form return form
def make_quick_row_form(self, instance=None, factory=None, fields=None, schema=None, mobile=False, **kwargs):
"""
Creates a "quick" form for adding a new row to the given instance.
"""
if factory is None:
factory = self.get_quick_row_form_factory(mobile=mobile)
if fields is None:
fields = self.get_quick_row_form_fields(mobile=mobile)
if schema is None:
schema = self.make_quick_row_form_schema(mobile=mobile)
kwargs['mobile'] = mobile
kwargs = self.make_quick_row_form_kwargs(**kwargs)
form = factory(fields, schema, **kwargs)
self.configure_quick_row_form(form, mobile=mobile)
return form
def get_quick_row_form_factory(self, mobile=False):
return forms.Form
def get_quick_row_form_fields(self, mobile=False):
pass
def make_quick_row_form_schema(self, mobile=False):
schema = colander.MappingSchema()
schema.add(colander.SchemaNode(colander.String(), name='quick_row_entry'))
return schema
def make_quick_row_form_kwargs(self, **kwargs):
defaults = {
'request': self.request,
'model_class': getattr(self, 'model_row_class', None),
'cancel_url': self.request.get_referrer(),
}
defaults.update(kwargs)
return defaults
def configure_quick_row_form(self, form, mobile=False):
pass
def get_mobile_row_form_fields(self): def get_mobile_row_form_fields(self):
if hasattr(self, 'mobile_row_form_fields'): if hasattr(self, 'mobile_row_form_fields'):
return self.mobile_row_form_fields return self.mobile_row_form_fields
@ -1117,6 +1160,9 @@ class MasterView(View):
return False return False
return True return True
def validate_quick_row_form(self, form):
return form.validate(newstyle=True)
def get_mobile_row_data(self, parent): def get_mobile_row_data(self, parent):
return self.get_row_data(parent) return self.get_row_data(parent)
@ -1723,6 +1769,7 @@ class MasterView(View):
""" """
context = { context = {
'master': self, 'master': self,
'mobile': mobile,
'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(),
'route_prefix': self.get_route_prefix(), 'route_prefix': self.get_route_prefix(),
@ -1747,7 +1794,7 @@ class MasterView(View):
context.update(self.template_kwargs(**context)) context.update(self.template_kwargs(**context))
if hasattr(self, 'template_kwargs_{}'.format(template)): if hasattr(self, 'template_kwargs_{}'.format(template)):
context.update(getattr(self, 'template_kwargs_{}'.format(template))(**context)) context.update(getattr(self, 'template_kwargs_{}'.format(template))(**context))
if hasattr(self, 'mobile_template_kwargs_{}'.format(template)): if mobile and hasattr(self, 'mobile_template_kwargs_{}'.format(template)):
context.update(getattr(self, 'mobile_template_kwargs_{}'.format(template))(**context)) context.update(getattr(self, 'mobile_template_kwargs_{}'.format(template))(**context))
# First try the template path most specific to the view. # First try the template path most specific to the view.
@ -2448,6 +2495,33 @@ class MasterView(View):
'form': form, 'form': form,
}, mobile=True) }, mobile=True)
def mobile_quick_row(self):
"""
Mobile view for "quick" location or creation of a row object
"""
parent = self.get_instance()
parent_url = self.get_action_url('view', parent, mobile=True)
form = self.make_quick_row_form(self.model_row_class, mobile=True, cancel_url=parent_url)
if self.request.method == 'POST':
if self.validate_quick_row_form(form):
row = self.save_quick_row_form(form)
if not row:
self.request.session.flash("Could not locate/create row for entry: "
"{}".format(form.validated['quick_row_entry']),
'error')
return self.redirect(parent_url)
return self.redirect_after_quick_row(row, mobile=True)
return self.redirect(parent_url)
def save_quick_row_form(self, form):
raise NotImplementedError("You must define `{}:{}.save_quick_row_form()` "
"in order to process quick row forms".format(
self.__class__.__module__,
self.__class__.__name__))
def redirect_after_quick_row(self, row, mobile=False):
return self.redirect(self.get_row_action_url('edit', row, mobile=mobile))
def view_row(self): def view_row(self):
""" """
View for viewing details of a single data row. View for viewing details of a single data row.
@ -2877,6 +2951,10 @@ class MasterView(View):
config.add_route('mobile.{}.create_row'.format(route_prefix), '/mobile{}/{{{}}}/new-row'.format(url_prefix, model_key)) config.add_route('mobile.{}.create_row'.format(route_prefix), '/mobile{}/{{{}}}/new-row'.format(url_prefix, model_key))
config.add_view(cls, attr='mobile_create_row', route_name='mobile.{}.create_row'.format(route_prefix), config.add_view(cls, attr='mobile_create_row', route_name='mobile.{}.create_row'.format(route_prefix),
permission='{}.create_row'.format(permission_prefix)) permission='{}.create_row'.format(permission_prefix))
if cls.mobile_rows_quickable:
config.add_route('mobile.{}.quick_row'.format(route_prefix), '/mobile{}/{{{}}}/quick-row'.format(url_prefix, model_key))
config.add_view(cls, attr='mobile_quick_row', route_name='mobile.{}.quick_row'.format(route_prefix),
permission='{}.create_row'.format(permission_prefix))
# view row # view row
if cls.has_rows: if cls.has_rows:

View file

@ -586,6 +586,13 @@ class PurchasingBatchView(BatchMasterView):
# query = super(PurchasingBatchView, self).get_row_data(batch) # query = super(PurchasingBatchView, self).get_row_data(batch)
# return query.options(orm.joinedload(model.PurchaseBatchRow.credits)) # return query.options(orm.joinedload(model.PurchaseBatchRow.credits))
def get_mobile_row_data(self, parent):
query = self.get_row_data(parent)
return self.sort_mobile_row_data(query)
def sort_mobile_row_data(self, query):
return query.order_by(model.PurchaseBatchRow.modified.desc())
def configure_row_grid(self, g): def configure_row_grid(self, g):
super(PurchasingBatchView, self).configure_row_grid(g) super(PurchasingBatchView, self).configure_row_grid(g)

View file

@ -52,6 +52,8 @@ class OrderingBatchView(PurchasingBatchView):
index_title = "Ordering" index_title = "Ordering"
mobile_creatable = True mobile_creatable = True
rows_editable = True rows_editable = True
mobile_rows_creatable = True
mobile_rows_quickable = True
mobile_rows_editable = True mobile_rows_editable = True
has_worksheet = True has_worksheet = True

View file

@ -579,13 +579,6 @@ class ReceivingBatchView(PurchasingBatchView):
f.set_readonly('po_total') f.set_readonly('po_total')
f.set_readonly('invoice_total') f.set_readonly('invoice_total')
def get_mobile_row_data(self, parent):
query = self.get_row_data(parent)
return self.sort_mobile_row_data(query)
def sort_mobile_row_data(self, query):
return query.order_by(model.PurchaseBatchRow.modified.desc())
def render_mobile_row_listitem(self, row, i): def render_mobile_row_listitem(self, row, i):
description = row.product.full_description if row.product else row.description description = row.product.full_description if row.product else row.description
return "({}) {}".format(row.upc.pretty(), description) return "({}) {}".format(row.upc.pretty(), description)