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:
parent
e313e1bc8c
commit
0ad29c5283
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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,8 +202,40 @@ 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.
|
||||||
"""
|
"""
|
||||||
|
listitem = self.mobile_listitem_field()
|
||||||
|
if listitem:
|
||||||
|
grid.append(listitem)
|
||||||
|
grid.configure(include=[grid.listitem])
|
||||||
|
else:
|
||||||
grid.configure()
|
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):
|
||||||
"""
|
"""
|
||||||
View for creating a new model record.
|
View for creating a new model record.
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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'),
|
||||||
|
|
Loading…
Reference in a new issue