Add basic paging grid/index support for mobile

still lots to do for this yet..but readonly basics are here..
This commit is contained in:
Lance Edgar 2017-03-30 20:11:17 -05:00
parent e313e1bc8c
commit 0ad29c5283
7 changed files with 132 additions and 57 deletions

View file

@ -26,46 +26,16 @@ Mobile Grids
from __future__ import unicode_literals, absolute_import from __future__ import unicode_literals, absolute_import
from webhelpers.html import tags from webhelpers.html import HTML
from tailbone.newgrids import AlchemyGrid
class Grid(object): class MobileGrid(AlchemyGrid):
"""
Base class for all grids
"""
configured = False
def __init__(self, key, data=None, **kwargs):
"""
Grid constructor
"""
self.key = key
self.data = data
for k, v in kwargs.items():
setattr(self, k, v)
def __iter__(self):
"""
This grid supports iteration, over its data
"""
return iter(self.data)
def configure(self, include=None):
"""
Configure the grid. This must define which columns to display and in
which order, etc.
"""
self.configured = True
def view_url(self, obj, mobile=False):
route = '{}{}.view'.format('mobile.' if mobile else '', self.route_prefix)
return self.request.route_url(route, uuid=obj.uuid)
class MobileGrid(Grid):
""" """
Base class for all mobile grids Base class for all mobile grids
""" """
def render_object(self, obj): def column_header(self, column):
return tags.link_to(obj, self.view_url(obj, mobile=True)) kwargs = {'c': column.label}
return HTML.tag('th', **kwargs)

View file

@ -108,7 +108,12 @@
% endfor % endfor
% endif % endif
% if capture(self.page_title): % if master is not Undefined and master.listing:
<h1>${model_title_plural}</h1>
% elif index_title is not Undefined and index_url is not Undefined:
<h1>${h.link_to(index_title, index_url)}</h1>
<h2>${self.page_title()}</h2>
% elif capture(self.page_title):
<h2>${self.page_title()}</h2> <h2>${self.page_title()}</h2>
% endif % endif

View file

@ -10,7 +10,38 @@
<%def name="title()">${model_title_plural}</%def> <%def name="title()">${model_title_plural}</%def>
<ul data-role="listview"> <ul data-role="listview">
% for obj in grid: % for obj in grid.iter_rows():
<li>${grid.render_object(obj)}</li> <li>${grid.listitem.render_readonly()}</li>
% endfor % endfor
</ul> </ul>
## <table data-role="table" class="ui-responsive table-stroke">
## <thead>
## <tr>
## % for column in grid.iter_visible_columns():
## ${grid.column_header(column)}
## % endfor
## </tr>
## </thead>
## <tbody>
## % for i, row in enumerate(grid.iter_rows(), 1):
## <tr>
## % for column in grid.iter_visible_columns():
## <td>${grid.render_cell(row, column)}</td>
## % endfor
## </tr>
## % endfor
## </tbody>
## </table>
% if grid.pageable and grid.pager:
<br />
<div data-role="controlgroup" data-type="horizontal">
${grid.pager.pager('$link_first $link_previous $link_next $link_last',
symbol_first='<< first', symbol_last='last >>',
symbol_previous='< prev', symbol_next='next >',
link_attr={'class': 'ui-btn ui-corner-all'},
curpage_attr={'class': 'ui-btn ui-corner-all'},
dotdot_attr={'class': 'ui-btn ui-corner-all'})}
</div>
% endif

View file

@ -7,6 +7,6 @@
## ############################################################################## ## ##############################################################################
<%inherit file="/mobile/base.mako" /> <%inherit file="/mobile/base.mako" />
<%def name="title()">${model_title}: ${instance_title}</%def> <%def name="title()">${instance_title}</%def>
<p>TODO: display fieldset for object</p> ${form.render()|n}

View file

@ -133,6 +133,13 @@ class CustomersView(MasterView):
fs.email_preference, fs.email_preference,
]) ])
def configure_mobile_fieldset(self, fs):
fs.configure(
include=[
fs.email,
fs.phone,
])
class CustomerNameAutocomplete(AutocompleteView): class CustomerNameAutocomplete(AutocompleteView):
""" """

View file

@ -26,6 +26,7 @@ Model Master View
from __future__ import unicode_literals, absolute_import from __future__ import unicode_literals, absolute_import
import six
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy import orm from sqlalchemy import orm
@ -102,7 +103,7 @@ class MasterView(View):
View to list/filter/sort the model data. View to list/filter/sort the model data.
If this view receives a non-empty 'partial' parameter in the query If this view receives a non-empty 'partial' parameter in the query
string, then the view will return the renderered grid only. Otherwise string, then the view will return the rendered grid only. Otherwise
returns the full page. returns the full page.
""" """
self.listing = True self.listing = True
@ -135,16 +136,15 @@ class MasterView(View):
def make_mobile_grid(self, **kwargs): def make_mobile_grid(self, **kwargs):
factory = self.get_mobile_grid_factory() factory = self.get_mobile_grid_factory()
key = self.get_mobile_grid_key()
data = self.get_mobile_data(session=kwargs.get('session'))
kwargs = self.make_mobile_grid_kwargs(**kwargs) kwargs = self.make_mobile_grid_kwargs(**kwargs)
kwargs.setdefault('key', key) kwargs.setdefault('key', self.get_mobile_grid_key())
kwargs.setdefault('request', self.request) kwargs.setdefault('request', self.request)
kwargs.setdefault('data', data) kwargs.setdefault('data', self.get_mobile_data(session=kwargs.get('session')))
kwargs.setdefault('model_class', self.get_model_class(error=False)) kwargs.setdefault('model_class', self.get_model_class(error=False))
grid = factory(**kwargs) grid = factory(**kwargs)
self.preconfigure_mobile_grid(grid) self.preconfigure_mobile_grid(grid)
self.configure_mobile_grid(grid) self.configure_mobile_grid(grid)
grid.load_settings()
return grid return grid
@classmethod @classmethod
@ -166,7 +166,7 @@ class MasterView(View):
""" """
if hasattr(cls, 'mobile_grid_key'): if hasattr(cls, 'mobile_grid_key'):
return cls.mobile_grid_key return cls.mobile_grid_key
return cls.get_route_prefix() return 'mobile.{}'.format(cls.get_route_prefix())
def get_mobile_data(self, session=None): def get_mobile_data(self, session=None):
""" """
@ -184,6 +184,8 @@ class MasterView(View):
""" """
defaults = { defaults = {
'route_prefix': self.get_route_prefix(), 'route_prefix': self.get_route_prefix(),
'pageable': self.pageable,
'sortable': True,
} }
defaults.update(kwargs) defaults.update(kwargs)
return defaults return defaults
@ -200,7 +202,39 @@ class MasterView(View):
which columns to show and in which order etc. Along the way you're which columns to show and in which order etc. Along the way you're
free to customize any column(s) you like, as needed. free to customize any column(s) you like, as needed.
""" """
grid.configure() listitem = self.mobile_listitem_field()
if listitem:
grid.append(listitem)
grid.configure(include=[grid.listitem])
else:
grid.configure()
def mobile_listitem_field(self):
"""
Must return a FormAlchemy field to be appended to grid, or ``None`` if
none is desired.
"""
return fa.Field('listitem', value=lambda obj: obj,
renderer=self.mobile_listitem_renderer())
def mobile_listitem_renderer(self):
"""
Must return a FormAlchemy field renderer callable for the mobile grid's
list item field.
"""
master = self
class ListItemRenderer(fa.FieldRenderer):
def render_readonly(self, **kwargs):
obj = self.raw_value
if obj is None:
return ''
title = master.get_instance_title(obj)
url = master.get_action_url('view', obj, mobile=True)
return tags.link_to(title, url)
return ListItemRenderer
def create(self): def create(self):
""" """
@ -271,17 +305,39 @@ 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_mobile_form(instance)
context = { context = {
'instance': instance, 'instance': instance,
'instance_title': self.get_instance_title(instance), 'instance_title': self.get_instance_title(instance),
# 'instance_editable': self.editable_instance(instance), # 'instance_editable': self.editable_instance(instance),
# 'instance_deletable': self.deletable_instance(instance), # 'instance_deletable': self.deletable_instance(instance),
# 'form': form, 'form': form,
} }
return self.render_to_response('view', context, mobile=True) return self.render_to_response('view', context, mobile=True)
def make_mobile_form(self, instance, **kwargs):
"""
Make a FormAlchemy-based form for use with mobile CRUD views
"""
fieldset = self.make_fieldset(instance)
self.preconfigure_mobile_fieldset(fieldset)
self.configure_mobile_fieldset(fieldset)
factory = kwargs.pop('factory', forms.AlchemyForm)
kwargs.setdefault('session', self.Session())
form = factory(self.request, fieldset, **kwargs)
form.readonly = self.viewing
return form
def preconfigure_mobile_fieldset(self, fieldset):
self._preconfigure_fieldset(fieldset)
def configure_mobile_fieldset(self, fieldset):
"""
Configure the given mobile fieldset.
"""
self.configure_fieldset(fieldset)
def make_default_row_grid_tools(self, obj): def make_default_row_grid_tools(self, obj):
if self.rows_creatable: if self.rows_creatable:
link = tags.link_to("Create a new {}".format(self.get_row_model_title()), link = tags.link_to("Create a new {}".format(self.get_row_model_title()),
@ -547,7 +603,7 @@ class MasterView(View):
object_to_keep = self.Session.query(self.get_model_class()).get(uuids[1]) object_to_keep = self.Session.query(self.get_model_class()).get(uuids[1])
if object_to_remove and object_to_keep and self.request.POST.get('commit-merge') == 'yes': if object_to_remove and object_to_keep and self.request.POST.get('commit-merge') == 'yes':
msg = unicode(object_to_remove) msg = six.text_type(object_to_remove)
try: try:
self.validate_merge(object_to_remove, object_to_keep) self.validate_merge(object_to_remove, object_to_keep)
except Exception as error: except Exception as error:
@ -691,11 +747,14 @@ class MasterView(View):
""" """
return getattr(cls, 'permission_prefix', cls.get_route_prefix()) return getattr(cls, 'permission_prefix', cls.get_route_prefix())
def get_index_url(self): def get_index_url(self, mobile=False):
""" """
Returns the master view's index URL. Returns the master view's index URL.
""" """
return self.request.route_url(self.get_route_prefix()) route = self.get_route_prefix()
if mobile:
route = 'mobile.{}'.format(route)
return self.request.route_url(route)
@classmethod @classmethod
def get_index_title(cls): def get_index_title(cls):
@ -704,13 +763,15 @@ class MasterView(View):
""" """
return getattr(cls, 'index_title', cls.get_model_title_plural()) return getattr(cls, 'index_title', cls.get_model_title_plural())
def get_action_url(self, action, instance, **kwargs): def get_action_url(self, action, instance, mobile=False, **kwargs):
""" """
Generate a URL for the given action on the given instance Generate a URL for the given action on the given instance
""" """
kw = self.get_action_route_kwargs(instance) kw = self.get_action_route_kwargs(instance)
kw.update(kwargs) kw.update(kwargs)
route_prefix = self.get_route_prefix() route_prefix = self.get_route_prefix()
if mobile:
route_prefix = 'mobile.{}'.format(route_prefix)
return self.request.route_url('{}.{}'.format(route_prefix, action), **kw) return self.request.route_url('{}.{}'.format(route_prefix, action), **kw)
def render_to_response(self, template, data, mobile=False): def render_to_response(self, template, data, mobile=False):
@ -728,7 +789,7 @@ class MasterView(View):
'route_prefix': self.get_route_prefix(), 'route_prefix': self.get_route_prefix(),
'permission_prefix': self.get_permission_prefix(), 'permission_prefix': self.get_permission_prefix(),
'index_title': self.get_index_title(), 'index_title': self.get_index_title(),
'index_url': self.get_index_url(), 'index_url': self.get_index_url(mobile=mobile),
'action_url': self.get_action_url, 'action_url': self.get_action_url,
'grid_index': self.grid_index, 'grid_index': self.grid_index,
} }
@ -1066,7 +1127,7 @@ class MasterView(View):
""" """
Return a "pretty" title for the instance, to be used in the page title etc. Return a "pretty" title for the instance, to be used in the page title etc.
""" """
return unicode(instance) return six.text_type(instance)
def make_form(self, instance, **kwargs): def make_form(self, instance, **kwargs):
""" """

View file

@ -93,6 +93,7 @@ class ProductsView(MasterView):
Master view for the Product class. Master view for the Product class.
""" """
model_class = model.Product model_class = model.Product
supports_mobile = True
# child_version_classes = [ # child_version_classes = [
# (model.ProductCode, 'product_uuid'), # (model.ProductCode, 'product_uuid'),