Add new "v3" grids, refactor all views to use them

or at least that's the idea..hopefully we caught them all
This commit is contained in:
Lance Edgar 2017-07-07 09:13:53 -05:00
parent f244c2934b
commit 5b1ae27a10
71 changed files with 2679 additions and 2030 deletions

View file

@ -96,6 +96,7 @@ requires = [
'transaction', # 1.2.0
'waitress', # 0.8.1
'WebHelpers2', # 2.0
'webhelpers2_grid', # 0.1
'WTForms', # 2.1
'zope.sqlalchemy', # 0.7
]

View file

@ -0,0 +1,33 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2017 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/>.
#
################################################################################
"""
Grids and Friends
"""
from __future__ import unicode_literals, absolute_import
from .core import Grid, GridAction
from .mobile import MobileGrid
# TODO
from tailbone.newgrids import filters

919
tailbone/grids3/core.py Normal file
View file

@ -0,0 +1,919 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2017 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/>.
#
################################################################################
"""
Core Grid Classes
"""
from __future__ import unicode_literals, absolute_import
import six
import sqlalchemy as sa
from sqlalchemy import orm
from rattail.db import api
from rattail.db.types import GPCType
from rattail.util import pretty_boolean, pretty_quantity
import webhelpers2_grid
from pyramid.renderers import render
from webhelpers2.html import HTML, tags
from paginate_sqlalchemy import SqlalchemyOrmPage
from tailbone.db import Session
from tailbone import newgrids
from tailbone.newgrids import GridAction
from tailbone.newgrids.alchemy import URLMaker
from tailbone.util import raw_datetime
class Grid(object):
"""
Core grid class. In sore need of documentation.
"""
def __init__(self, key, data, columns, request=None, mobile=False, model_class=None, enums={},
labels={}, renderers={}, extra_row_class=None, linked_columns=[], url='#',
joiners={}, filterable=False, filters={},
sortable=False, sorters={}, default_sortkey=None, default_sortdir='asc',
pageable=False, default_pagesize=20, default_page=1,
checkboxes=False, main_actions=[], more_actions=[],
**kwargs):
self.key = key
self.data = data
self.columns = columns
self.request = request
self.mobile = mobile
self.model_class = model_class
self.enums = enums or {}
self.labels = labels or {}
self.renderers = renderers or {}
self.extra_row_class = extra_row_class
self.linked_columns = linked_columns or []
self.url = url
self.joiners = joiners or {}
self.filterable = filterable
self.filters = self.make_filters(filters)
self.sortable = sortable
self.sorters = self.make_sorters(sorters)
self.default_sortkey = default_sortkey
self.default_sortdir = default_sortdir
self.pageable = pageable
self.default_pagesize = default_pagesize
self.default_page = default_page
self.checkboxes = checkboxes
self.main_actions = main_actions
self.more_actions = more_actions
self._whgrid_kwargs = kwargs
def hide_column(self, key):
if key in self.columns:
self.columns.remove(key)
def set_label(self, key, label):
self.labels[key] = label
if key in self.filters:
self.filters[key].label = label
def set_link(self, key, link=True):
if link:
if key not in self.linked_columns:
self.linked_columns.append(key)
else: # unlink
if self.linked_columns and key in self.linked_columns:
self.linked_columns.remove(key)
def set_renderer(self, key, renderer):
# TODO: deprecate / remove "type" detection here
if renderer == 'boolean':
renderer = self.render_boolean
elif renderer == 'currency':
renderer = self.render_currency
elif renderer == 'datetime':
renderer = self.render_datetime
elif renderer == 'gpc':
renderer = self.render_gpc
elif renderer == 'quantity':
renderer = self.render_quantity
self.renderers[key] = renderer
def set_type(self, key, type_):
if type_ == 'boolean':
self.set_renderer(key, self.render_boolean)
elif type_ == 'currency':
self.set_renderer(key, self.render_currency)
elif type_ == 'datetime':
self.set_renderer(key, self.render_datetime)
elif type_ == 'enum':
self.set_renderer(key, self.render_enum)
elif type_ == 'gpc':
self.set_renderer(key, self.render_gpc)
elif type_ == 'quantity':
self.set_renderer(key, self.render_quantity)
else:
raise ValueError("Unsupported type for column '{}': {}".format(key, type_))
def set_enum(self, key, enum):
if enum:
self.enums[key] = enum
self.set_type(key, 'enum')
else:
self.enums.pop(key, None)
def render_boolean(self, obj, column_name):
value = self.obtain_value(obj, column_name)
return pretty_boolean(value)
def obtain_value(self, obj, column_name):
try:
return obj[column_name]
except TypeError:
return getattr(obj, column_name)
def render_currency(self, obj, column_name):
value = self.obtain_value(obj, column_name)
if value is None:
return ""
if value < 0:
return "(${:0,.2f})".format(0 - value)
return "${:0,.2f}".format(value)
def render_datetime(self, obj, column_name):
value = self.obtain_value(obj, column_name)
if value is None:
return ""
return raw_datetime(self.request.rattail_config, value)
def render_enum(self, obj, column_name):
value = self.obtain_value(obj, column_name)
if value is None:
return ""
enum = self.enums.get(column_name)
if enum and value in enum:
return six.text_type(enum[value])
return six.text_type(value)
def render_gpc(self, obj, column_name):
value = self.obtain_value(obj, column_name)
if value is None:
return ""
return value.pretty()
def render_quantity(self, obj, column_name):
value = self.obtain_value(obj, column_name)
return pretty_quantity(value)
def set_url(self, url):
self.url = url
def make_url(self, obj, i=None):
if callable(self.url):
return self.url(obj)
return self.url
def make_webhelpers_grid(self):
kwargs = dict(self._whgrid_kwargs)
kwargs['request'] = self.request
kwargs['mobile'] = self.mobile
kwargs['url'] = self.make_url
columns = list(self.columns)
column_labels = kwargs.setdefault('column_labels', {})
column_formats = kwargs.setdefault('column_formats', {})
for key, value in self.labels.items():
column_labels.setdefault(key, value)
if self.checkboxes:
columns.insert(0, 'checkbox')
column_labels['checkbox'] = tags.checkbox('check-all')
column_formats['checkbox'] = self.checkbox_column_format
if self.renderers:
kwargs['renderers'] = dict(self.renderers)
if self.extra_row_class:
kwargs['extra_record_class'] = self.extra_row_class
if self.linked_columns:
kwargs['linked_columns'] = list(self.linked_columns)
if self.main_actions or self.more_actions:
columns.append('actions')
column_formats['actions'] = self.actions_column_format
# TODO: pretty sure this factory doesn't serve all use cases yet?
factory = CustomWebhelpersGrid
# factory = webhelpers2_grid.Grid
if self.sortable:
# factory = CustomWebhelpersGrid
kwargs['order_column'] = self.sortkey
kwargs['order_direction'] = 'dsc' if self.sortdir == 'desc' else 'asc'
grid = factory(self.make_visible_data(), columns, **kwargs)
if self.sortable:
grid.exclude_ordering = list([key for key in grid.exclude_ordering
if key not in self.sorters])
return grid
def checkbox_column_format(self, column_number, row_number, item):
return HTML.td(self.render_checkbox(item), class_='checkbox')
def actions_column_format(self, column_number, row_number, item):
return HTML.td(self.render_actions(item, row_number), class_='actions')
def render_grid(self, template='/grids3/grid.mako', **kwargs):
context = kwargs
context['grid'] = self
return render(template, context)
def get_default_filters(self):
"""
Returns the default set of filters provided by the grid.
"""
if hasattr(self, 'default_filters'):
if callable(self.default_filters):
return self.default_filters()
return self.default_filters
filters = newgrids.filters.GridFilterSet()
if self.model_class:
mapper = orm.class_mapper(self.model_class)
for prop in mapper.iterate_properties:
if isinstance(prop, orm.ColumnProperty) and not prop.key.endswith('uuid'):
filters[prop.key] = self.make_filter(prop.key, prop.columns[0])
return filters
def make_filters(self, filters=None):
"""
Returns an initial set of filters which will be available to the grid.
The grid itself may or may not provide some default filters, and the
``filters`` kwarg may contain additions and/or overrides.
"""
if filters:
return filters
return self.get_default_filters()
def make_filter(self, key, column, **kwargs):
"""
Make a filter suitable for use with the given column.
"""
factory = kwargs.pop('factory', None)
if not factory:
factory = newgrids.filters.AlchemyGridFilter
if isinstance(column.type, sa.String):
factory = newgrids.filters.AlchemyStringFilter
elif isinstance(column.type, sa.Numeric):
factory = newgrids.filters.AlchemyNumericFilter
elif isinstance(column.type, sa.Integer):
factory = newgrids.filters.AlchemyNumericFilter
elif isinstance(column.type, sa.Boolean):
# TODO: check column for nullable here?
factory = newgrids.filters.AlchemyNullableBooleanFilter
elif isinstance(column.type, sa.Date):
factory = newgrids.filters.AlchemyDateFilter
elif isinstance(column.type, sa.DateTime):
factory = newgrids.filters.AlchemyDateTimeFilter
elif isinstance(column.type, GPCType):
factory = newgrids.filters.AlchemyGPCFilter
return factory(key, column=column, config=self.request.rattail_config, **kwargs)
def iter_filters(self):
"""
Iterate over all filters available to the grid.
"""
return six.itervalues(self.filters)
def iter_active_filters(self):
"""
Iterate over all *active* filters for the grid. Whether a filter is
active is determined by current grid settings.
"""
for filtr in self.iter_filters():
if filtr.active:
yield filtr
def make_sorters(self, sorters=None):
"""
Returns an initial set of sorters which will be available to the grid.
The grid itself may or may not provide some default sorters, and the
``sorters`` kwarg may contain additions and/or overrides.
"""
sorters, updates = {}, sorters
if self.model_class:
mapper = orm.class_mapper(self.model_class)
for prop in mapper.iterate_properties:
if isinstance(prop, orm.ColumnProperty) and not prop.key.endswith('uuid'):
sorters[prop.key] = self.make_sorter(prop)
if updates:
sorters.update(updates)
return sorters
def make_sorter(self, model_property):
"""
Returns a function suitable for a sort map callable, with typical logic
built in for sorting applied to ``field``.
"""
class_ = getattr(model_property, 'class_', self.model_class)
column = getattr(class_, model_property.key)
return lambda q, d: q.order_by(getattr(column, d)())
def make_simple_sorter(self, key, foldcase=False):
"""
Returns a function suitable for a sort map callable, with typical logic
built in for sorting a data set comprised of dicts, on the given key.
"""
if foldcase:
keyfunc = lambda v: v[key].lower()
else:
keyfunc = lambda v: v[key]
return lambda q, d: sorted(q, key=keyfunc, reverse=d == 'desc')
def load_settings(self, store=True):
"""
Load current/effective settings for the grid, from the request query
string and/or session storage. If ``store`` is true, then once
settings have been fully read, they are stored in current session for
next time. Finally, various instance attributes of the grid and its
filters are updated in-place to reflect the settings; this is so code
needn't access the settings dict directly, but the more Pythonic
instance attributes.
"""
# initial default settings
settings = {}
if self.sortable:
settings['sortkey'] = self.default_sortkey
settings['sortdir'] = self.default_sortdir
if self.pageable:
settings['pagesize'] = self.default_pagesize
settings['page'] = self.default_page
if self.filterable:
for filtr in self.iter_filters():
settings['filter.{}.active'.format(filtr.key)] = filtr.default_active
settings['filter.{}.verb'.format(filtr.key)] = filtr.default_verb
settings['filter.{}.value'.format(filtr.key)] = filtr.default_value
# If user has default settings on file, apply those first.
if self.user_has_defaults():
self.apply_user_defaults(settings)
# If request contains instruction to reset to default filters, then we
# can skip the rest of the request/session checks.
if self.request.GET.get('reset-to-default-filters') == 'true':
pass
# If request has filter settings, grab those, then grab sort/pager
# settings from request or session.
elif self.filterable and self.request_has_settings('filter'):
self.update_filter_settings(settings, 'request')
if self.request_has_settings('sort'):
self.update_sort_settings(settings, 'request')
else:
self.update_sort_settings(settings, 'session')
self.update_page_settings(settings)
# If request has no filter settings but does have sort settings, grab
# those, then grab filter settings from session, then grab pager
# settings from request or session.
elif self.request_has_settings('sort'):
self.update_sort_settings(settings, 'request')
self.update_filter_settings(settings, 'session')
self.update_page_settings(settings)
# NOTE: These next two are functionally equivalent, but are kept
# separate to maintain the narrative...
# If request has no filter/sort settings but does have pager settings,
# grab those, then grab filter/sort settings from session.
elif self.request_has_settings('page'):
self.update_page_settings(settings)
self.update_filter_settings(settings, 'session')
self.update_sort_settings(settings, 'session')
# If request has no settings, grab all from session.
elif self.session_has_settings():
self.update_filter_settings(settings, 'session')
self.update_sort_settings(settings, 'session')
self.update_page_settings(settings)
# If no settings were found in request or session, don't store result.
else:
store = False
# Maybe store settings for next time.
if store:
self.persist_settings(settings, 'session')
# If request contained instruction to save current settings as defaults
# for the current user, then do that.
if self.request.GET.get('save-current-filters-as-defaults') == 'true':
self.persist_settings(settings, 'defaults')
# update ourself to reflect settings
if self.filterable:
for filtr in self.iter_filters():
filtr.active = settings['filter.{}.active'.format(filtr.key)]
filtr.verb = settings['filter.{}.verb'.format(filtr.key)]
filtr.value = settings['filter.{}.value'.format(filtr.key)]
if self.sortable:
self.sortkey = settings['sortkey']
self.sortdir = settings['sortdir']
if self.pageable:
self.pagesize = settings['pagesize']
self.page = settings['page']
def user_has_defaults(self):
"""
Check to see if the current user has default settings on file for this grid.
"""
user = self.request.user
if not user:
return False
# NOTE: we used to leverage `self.session` here, but sometimes we might
# be showing a grid of data from another system...so always use
# Tailbone Session now, for the settings. hopefully that didn't break
# anything...
session = Session()
if user not in session:
user = session.merge(user)
# User defaults should have all or nothing, so just check one key.
key = 'tailbone.{}.grid.{}.sortkey'.format(user.uuid, self.key)
return api.get_setting(session, key) is not None
def apply_user_defaults(self, settings):
"""
Update the given settings dict with user defaults, if any exist.
"""
def merge(key, normalize=lambda v: v):
skey = 'tailbone.{}.grid.{}.{}'.format(self.request.user.uuid, self.key, key)
value = api.get_setting(Session(), skey)
settings[key] = normalize(value)
if self.filterable:
for filtr in self.iter_filters():
merge('filter.{}.active'.format(filtr.key), lambda v: v == 'true')
merge('filter.{}.verb'.format(filtr.key))
merge('filter.{}.value'.format(filtr.key))
if self.sortable:
merge('sortkey')
merge('sortdir')
if self.pageable:
merge('pagesize', int)
merge('page', int)
def request_has_settings(self, type_):
"""
Determine if the current request (GET query string) contains any
filter/sort settings for the grid.
"""
if type_ == 'filter':
for filtr in self.iter_filters():
if filtr.key in self.request.GET:
return True
if 'filter' in self.request.GET: # user may be applying empty filters
return True
elif type_ == 'sort':
for key in ['sortkey', 'sortdir']:
if key in self.request.GET:
return True
elif type_ == 'page':
for key in ['pagesize', 'page']:
if key in self.request.GET:
return True
return False
def session_has_settings(self):
"""
Determine if the current session contains any settings for the grid.
"""
# session should have all or nothing, so just check a few keys which
# should be guaranteed present if anything has been stashed
for key in ['page', 'sortkey']:
if 'grid.{}.{}'.format(self.key, key) in self.request.session:
return True
return False
def get_setting(self, source, settings, key, normalize=lambda v: v, default=None):
"""
Get the effective value for a particular setting, preferring ``source``
but falling back to existing ``settings`` and finally the ``default``.
"""
if source not in ('request', 'session'):
raise ValueError("Invalid source identifier: {}".format(source))
# If source is query string, try that first.
if source == 'request':
value = self.request.GET.get(key)
if value is not None:
try:
value = normalize(value)
except ValueError:
pass
else:
return value
# Or, if source is session, try that first.
else:
value = self.request.session.get('grid.{}.{}'.format(self.key, key))
if value is not None:
return normalize(value)
# If source had nothing, try default/existing settings.
value = settings.get(key)
if value is not None:
try:
value = normalize(value)
except ValueError:
pass
else:
return value
# Okay then, default it is.
return default
def update_filter_settings(self, settings, source):
"""
Updates a settings dictionary according to filter settings data found
in either the GET query string, or session storage.
:param settings: Dictionary of initial settings, which is to be updated.
:param source: String identifying the source to consult for settings
data. Must be one of: ``('request', 'session')``.
"""
if not self.filterable:
return
for filtr in self.iter_filters():
prefix = 'filter.{}'.format(filtr.key)
if source == 'request':
# consider filter active if query string contains a value for it
settings['{}.active'.format(prefix)] = filtr.key in self.request.GET
settings['{}.verb'.format(prefix)] = self.get_setting(
source, settings, '{}.verb'.format(filtr.key), default='')
settings['{}.value'.format(prefix)] = self.get_setting(
source, settings, filtr.key, default='')
else: # source = session
settings['{}.active'.format(prefix)] = self.get_setting(
source, settings, '{}.active'.format(prefix),
normalize=lambda v: six.text_type(v).lower() == 'true', default=False)
settings['{}.verb'.format(prefix)] = self.get_setting(
source, settings, '{}.verb'.format(prefix), default='')
settings['{}.value'.format(prefix)] = self.get_setting(
source, settings, '{}.value'.format(prefix), default='')
def update_sort_settings(self, settings, source):
"""
Updates a settings dictionary according to sort settings data found in
either the GET query string, or session storage.
:param settings: Dictionary of initial settings, which is to be updated.
:param source: String identifying the source to consult for settings
data. Must be one of: ``('request', 'session')``.
"""
if not self.sortable:
return
settings['sortkey'] = self.get_setting(source, settings, 'sortkey')
settings['sortdir'] = self.get_setting(source, settings, 'sortdir')
def update_page_settings(self, settings):
"""
Updates a settings dictionary according to pager settings data found in
either the GET query string, or session storage.
Note that due to how the actual pager functions, the effective settings
will often come from *both* the request and session. This is so that
e.g. the page size will remain constant (coming from the session) while
the user jumps between pages (which only provides the single setting).
:param settings: Dictionary of initial settings, which is to be updated.
"""
if not self.pageable:
return
pagesize = self.request.GET.get('pagesize')
if pagesize is not None:
if pagesize.isdigit():
settings['pagesize'] = int(pagesize)
else:
pagesize = self.request.session.get('grid.{}.pagesize'.format(self.key))
if pagesize is not None:
settings['pagesize'] = pagesize
page = self.request.GET.get('page')
if page is not None:
if page.isdigit():
settings['page'] = page
else:
page = self.request.session.get('grid.{}.page'.format(self.key))
if page is not None:
settings['page'] = page
def persist_settings(self, settings, to='session'):
"""
Persist the given settings in some way, as defined by ``func``.
"""
def persist(key, value=lambda k: settings[k]):
if to == 'defaults':
skey = 'tailbone.{}.grid.{}.{}'.format(self.request.user.uuid, self.key, key)
api.save_setting(Session(), skey, value(key))
else: # to == session
skey = 'grid.{}.{}'.format(self.key, key)
self.request.session[skey] = value(key)
if self.filterable:
for filtr in self.iter_filters():
persist('filter.{}.active'.format(filtr.key), value=lambda k: six.text_type(settings[k]).lower())
persist('filter.{}.verb'.format(filtr.key))
persist('filter.{}.value'.format(filtr.key))
if self.sortable:
persist('sortkey')
persist('sortdir')
if self.pageable:
persist('pagesize')
persist('page')
def filter_data(self, data):
"""
Filter and return the given data set, according to current settings.
"""
for filtr in self.iter_active_filters():
# apply filter to data but save reference to original; if data is a
# SQLAlchemy query and wasn't modified, we don't need to bother
# with the underlying join (if there is one)
original = data
data = filtr.filter(data)
if filtr.key in self.joiners and filtr.key not in self.joined and (
not isinstance(data, orm.Query) or data is not original):
# this filter requires a join; apply that
data = self.joiners[filtr.key](data)
self.joined.add(filtr.key)
return data
def sort_data(self, data):
"""
Sort the given query according to current settings, and return the result.
"""
# Cannot sort unless we know which column to sort by.
if not self.sortkey:
return data
# Cannot sort unless we have a sort function.
sortfunc = self.sorters.get(self.sortkey)
if not sortfunc:
return data
# We can provide a default sort direction though.
sortdir = getattr(self, 'sortdir', 'asc')
if self.sortkey in self.joiners and self.sortkey not in self.joined:
data = self.joiners[self.sortkey](data)
self.joined.add(self.sortkey)
return sortfunc(data, sortdir)
def paginate_data(self, data):
"""
Paginate the given data set according to current settings, and return
the result.
"""
if self.model_class:
return SqlalchemyOrmPage(data,
items_per_page=self.pagesize,
page=self.page,
url_maker=URLMaker(self.request))
return data
def make_visible_data(self):
"""
Apply various settings to the raw data set, to produce a final data
set. This will page / sort / filter as necessary, according to the
grid's defaults and the current request etc.
"""
self.joined = set()
data = self.data
if self.filterable:
data = self.filter_data(data)
if self.sortable:
data = self.sort_data(data)
if self.pageable:
self.pager = self.paginate_data(data)
data = self.pager
return data
def render_complete(self, template='/newgrids/complete.mako', **kwargs):
"""
Render the complete grid, including filters.
"""
context = kwargs
context['grid'] = self
context.setdefault('allow_save_defaults', True)
return render(template, context)
def render_filters(self, template='/newgrids/filters.mako', **kwargs):
"""
Render the filters to a Unicode string, using the specified template.
Additional kwargs are passed along as context to the template.
"""
# Provide default data to filters form, so renderer can do some of the
# work for us.
data = {}
for filtr in self.iter_active_filters():
data['{}.active'.format(filtr.key)] = filtr.active
data['{}.verb'.format(filtr.key)] = filtr.verb
data[filtr.key] = filtr.value
form = newgrids.filters.GridFiltersForm(self.request, self.filters, defaults=data)
kwargs['request'] = self.request
kwargs['grid'] = self
kwargs['form'] = newgrids.filters.GridFiltersFormRenderer(form)
return render(template, kwargs)
# def get_div_attrs(self):
# """
# Returns a properly-formatted set of attributes which will be applied to
# the parent ``<div>`` element which contains the grid, when the grid is
# rendered.
# """
# classes = ['newgrid']
# if self.width == 'full':
# classes.append('full')
# if self.checkboxes:
# classes.append('selectable')
# attrs = {'class_': ' '.join(classes),
# 'data-url': self.request.current_route_url(_query=None),
# 'data-permalink': self.request.current_route_url()}
# if self.delete_speedbump:
# attrs['data-delete-speedbump'] = 'true'
# return attrs
def render_actions(self, row, i):
"""
Returns the rendered contents of the 'actions' column for a given row.
"""
main_actions = filter(None, [self.render_action(a, row, i) for a in self.main_actions])
more_actions = filter(None, [self.render_action(a, row, i) for a in self.more_actions])
if more_actions:
icon = HTML.tag('span', class_='ui-icon ui-icon-carat-1-e')
link = tags.link_to("More" + icon, '#', class_='more')
main_actions.append(link + HTML.tag('div', class_='more', c=more_actions))
return HTML.literal('').join(main_actions)
def render_action(self, action, row, i):
"""
Renders an action menu item (link) for the given row.
"""
url = action.get_url(row, i)
if url:
kwargs = {'class_': action.key, 'target': action.target}
if action.icon:
icon = HTML.tag('span', class_='ui-icon ui-icon-{}'.format(action.icon))
return tags.link_to(icon + action.label, url, **kwargs)
return tags.link_to(action.label, url, **kwargs)
def get_row_key(self, item):
"""
Must return a unique key for the given data item's row.
"""
mapper = orm.object_mapper(item)
if len(mapper.primary_key) == 1:
return getattr(item, mapper.primary_key[0].key)
raise NotImplementedError
def checkbox(self, item):
"""
Returns boolean indicating whether a checkbox should be rendererd for
the given data item's row.
"""
return True
def checked(self, item):
"""
Returns boolean indicating whether the given item's row checkbox should
be checked, for initial page load.
"""
return False
def render_checkbox(self, item):
"""
Renders a checkbox cell for the given item, if applicable.
"""
if not self.checkbox(item):
return ''
return tags.checkbox('checkbox-{}-{}'.format(self.key, self.get_row_key(item)),
checked=self.checked(item))
def get_pagesize_options(self):
# TODO: Make configurable or something...
return [5, 10, 20, 50, 100]
class CustomWebhelpersGrid(webhelpers2_grid.Grid):
"""
Implement column sorting links etc. for webhelpers2_grid
"""
def __init__(self, itemlist, columns, **kwargs):
self.mobile = kwargs.pop('mobile', False)
self.renderers = kwargs.pop('renderers', {})
self.linked_columns = kwargs.pop('linked_columns', [])
self.extra_record_class = kwargs.pop('extra_record_class', None)
super(CustomWebhelpersGrid, self).__init__(itemlist, columns, **kwargs)
def default_header_record_format(self, headers):
if self.mobile:
return HTML('')
return super(CustomWebhelpersGrid, self).default_header_record_format(headers)
def generate_header_link(self, column_number, column, label_text):
# display column header as simple no-op link; client-side JS takes care
# of the rest for us
label_text = tags.link_to(label_text, '#', data_sortkey=column)
# Is the current column the one we're ordering on?
if (column == self.order_column):
return self.default_header_ordered_column_format(column_number,
column,
label_text)
else:
return self.default_header_column_format(column_number, column,
label_text)
def default_record_format(self, i, record, columns):
if self.mobile:
return columns
kwargs = {
'class_': self.get_record_class(i, record, columns),
}
if hasattr(record, 'uuid'):
kwargs['data_uuid'] = record.uuid
return HTML.tag('tr', columns, **kwargs)
def get_record_class(self, i, record, columns):
if i % 2 == 0:
cls = 'even r{}'.format(i)
else:
cls = 'odd r{}'.format(i)
if self.extra_record_class:
extra = self.extra_record_class(record, i)
if extra:
cls = '{} {}'.format(cls, extra)
return cls
def get_column_value(self, column_number, i, record, column_name):
if self.renderers and column_name in self.renderers:
return self.renderers[column_name](record, column_name)
try:
return record[column_name]
except TypeError:
return getattr(record, column_name)
def default_column_format(self, column_number, i, record, column_name):
value = self.get_column_value(column_number, i, record, column_name)
if self.mobile:
url = self.url_generator(record, i)
return HTML.tag('li', tags.link_to(value, url))
if self.linked_columns and column_name in self.linked_columns:
url = self.url_generator(record, i)
value = tags.link_to(value, url)
class_name = 'c{}'.format(column_number)
return HTML.tag('td', value, class_=class_name)

53
tailbone/grids3/mobile.py Normal file
View file

@ -0,0 +1,53 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2017 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/>.
#
################################################################################
"""
Mobile Grids
"""
from __future__ import unicode_literals, absolute_import
from pyramid.renderers import render
from tailbone.grids3 import Grid
class MobileGrid(Grid):
"""
Base class for all mobile grids
"""
def render_filters(self, template='/mobile/newgrids/filters_simple.mako', **kwargs):
context = kwargs
context['request'] = self.request
context['grid'] = self
return render(template, context)
def render_grid(self, template='/mobile/newgrids/grid.mako', **kwargs):
context = kwargs
context['grid'] = self
return render(template, context)
def render_complete(self, template='/mobile/newgrids/complete.mako', **kwargs):
context = kwargs
context['grid'] = self
return render(template, context)

View file

@ -42,13 +42,13 @@ class MobileGrid(AlchemyGrid):
kwargs = {'c': column.label}
return HTML.tag('th', **kwargs)
def render_filters(self, template='/mobile/filters_simple.mako', **kwargs):
def render_filters(self, template='/mobile/newgrids/filters_simple.mako', **kwargs):
context = kwargs
context['request'] = self.request
context['grid'] = self
return render(template, context)
def render_complete(self, template='/mobile/grid_complete.mako', **kwargs):
def render_complete(self, template='/mobile/newgrids/complete.mako', **kwargs):
context = kwargs
context['grid'] = self
return render(template, context)

View file

@ -0,0 +1,71 @@
/********************************************************************************
* grids3.css
*
* Style tweaks for the new grids.
********************************************************************************/
/******************************
* thead
******************************/
.grid3 tr.header td {
border-right: 1px solid black;
border-bottom: 1px solid black;
font-weight: bold;
padding: 2px 3px;
text-align: center;
}
.grid3 tr.header a {
display: block;
padding-right: 18px;
}
.grid3 tr.header .asc,
.grid3 tr.header .dsc {
background-position: right center;
background-repeat: no-repeat;
}
.grid3 tr.header .asc {
background-image: url(../img/sort_arrow_up.png);
}
.grid3 tr.header .dsc {
background-image: url(../img/sort_arrow_down.png);
}
/******************************
* tbody
******************************/
.grid3 tr.odd {
background-color: #e0e0e0;
}
.grid3 tr.even {
background-color: White;
}
/* this is needed only as override? */
.newgrid.grid3 tbody tr:nth-child(odd) {
background-color: White;
}
.newgrid.grid3 tbody tr:nth-child(odd).hovering {
background-color: #bbbbbb;
}
.newgrid.grid3 tr:not(.header).notice.odd {
background-color: #fe8;
}
.newgrid.grid3 tr:not(.header).notice.even {
background-color: #fd6;
}
.newgrid.grid3 tr:not(.header).notice.hovering {
background-color: #ec7;
}

View file

@ -117,6 +117,19 @@
});
// Refresh data when user clicks a sortable column header.
if (this.grid.hasClass('grid3')) {
this.element.on('click', 'tr.header a', function() {
var td = $(this).parent();
var data = {
sortkey: $(this).data('sortkey'),
sortdir: (td.hasClass('asc')) ? 'desc' : 'asc',
page: 1,
partial: true
};
that.refresh(data);
return false;
});
} else {
this.element.on('click', 'thead th.sortable a', function() {
var th = $(this).parent();
var data = {
@ -128,6 +141,7 @@
that.refresh(data);
return false;
});
}
// Refresh data when user chooses a new page size setting.
this.element.on('change', '.pager #pagesize', function() {
@ -145,15 +159,39 @@
});
// Add hover highlight effect to grid rows during mouse-over.
this.element.on('mouseenter', 'tbody tr', function() {
this.element.on('mouseenter', 'tbody tr:not(.header)', function() {
$(this).addClass('hovering');
});
this.element.on('mouseleave', 'tbody tr', function() {
this.element.on('mouseleave', 'tbody tr:not(.header)', function() {
$(this).removeClass('hovering');
});
// Do some extra stuff for grids with checkboxes.
if (this.grid.hasClass('selectable')) {
// do some extra stuff for grids with checkboxes
if (this.grid.hasClass('grid3')) {
// (un-)check all rows when clicking check-all box in header
if (this.grid.find('tr.header td.checkbox input').length) {
this.element.on('click', 'tr.header td.checkbox input', function() {
var checked = $(this).prop('checked');
that.grid.find('tr:not(.header) td.checkbox input').prop('checked', checked);
});
}
// Select current row when clicked, unless clicking checkbox
// (since that already does select the row) or a link (since
// that does something completely different).
this.element.on('click', '.newgrid tr:not(.header) td.checkbox input', function(event) {
event.stopPropagation();
});
this.element.on('click', '.newgrid tr:not(.header) a', function(event) {
event.stopPropagation();
});
this.element.on('click', '.newgrid tr:not(.header)', function() {
$(this).find('td.checkbox input').click();
});
} else if (this.grid.hasClass('selectable')) { // pre-v3 newgrid.selectable
// (Un-)Check all rows when clicking check-all box in header.
this.element.on('click', 'thead th.checkbox input', function() {

View file

@ -324,18 +324,6 @@ $(function() {
});
/*
* TODO: this should be deprecated; for old grids only?
* Add "check all" functionality to tables with checkboxes.
*/
$('body').on('click', '.grid thead th.checkbox input[type="checkbox"]', function() {
var table = $(this).parents('table:first');
var checked = $(this).prop('checked');
table.find('tbody tr').each(function() {
$(this).find('td.checkbox input[type="checkbox"]').prop('checked', checked);
});
});
$('body').on('click', 'div.dialog button.close', function() {
var dialog = $(this).parents('div.dialog:first');
dialog.dialog('close');

View file

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
@ -26,6 +26,8 @@ Event Subscribers
from __future__ import unicode_literals, absolute_import
import six
import rattail
from rattail.db import model
from rattail.db.auth import has_permission
@ -79,6 +81,7 @@ def before_render(event):
renderer_globals['rattail'] = rattail
renderer_globals['tailbone'] = tailbone
renderer_globals['enum'] = request.rattail_config.get_enum()
renderer_globals['six'] = six
def add_inbox_count(event):

View file

@ -150,6 +150,7 @@
${h.stylesheet_link(request.static_url('tailbone:static/css/filters.css'))}
${h.stylesheet_link(request.static_url('tailbone:static/css/forms.css'))}
${h.stylesheet_link(request.static_url('tailbone:static/css/newgrids.css'))}
${h.stylesheet_link(request.static_url('tailbone:static/css/grids3.css'))}
</%def>
<%def name="jquery_smoothness_theme()">

View file

@ -1,8 +1,8 @@
## -*- coding: utf-8 -*-
<%inherit file="/master/index.mako" />
## -*- coding: utf-8; -*-
<%inherit file="/master2/index.mako" />
<%def name="head_tags()">
${parent.head_tags()}
<%def name="extra_javascript()">
${parent.extra_javascript()}
<script type="text/javascript">
$(function() {

View file

@ -0,0 +1,21 @@
## -*- coding: utf-8; -*-
<div class="newgrid grid3 full">
<table>
${grid.make_webhelpers_grid()}
</table>
% if grid.pageable and grid.pager:
<div class="pager">
<p class="showing">
${"showing {} thru {} of {:,d}".format(grid.pager.first_item, grid.pager.last_item, grid.pager.item_count)}
% if grid.pager.page_count > 1:
${"(page {} of {:,d})".format(grid.pager.page, grid.pager.page_count)}
% endif
</p>
<p class="page-links">
${h.select('pagesize', grid.pager.items_per_page, grid.get_pagesize_options())}
per page&nbsp;
${grid.pager.pager('$link_first $link_previous ~1~ $link_next $link_last', symbol_next='next', symbol_previous='prev')|n}
</p>
</div>
% endif
</div>

View file

@ -0,0 +1,55 @@
## -*- coding: utf-8; -*-
## ##############################################################################
##
## Default master 'index' template. Features a prominent data table and
## exposes a way to filter and sort the data, etc. Some index pages also
## include a "tools" section, just above the grid on the right.
##
## ##############################################################################
<%inherit file="/master/index.mako" />
<%def name="title()">${model_title_plural}</%def>
<%def name="context_menu_items()">
% if master.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>
## ${grid.render_complete(tools=capture(self.grid_tools).strip(), context_menu=capture(self.context_menu_items).strip())|n}
<div class="newgrid-wrapper">
<table class="grid-header">
<tbody>
<tr>
<td class="filters" rowspan="2">
% if grid.filterable:
## TODO: should this be variable sometimes?
${grid.render_filters(allow_save_defaults=True)|n}
% endif
</td>
<td class="menu">
<ul id="context-menu">
${self.context_menu_items()}
</ul>
</td>
</tr>
<tr>
<td class="tools">
<div class="grid-tools">
${self.grid_tools()}
</div><!-- grid-tools -->
</td>
</tr>
</tbody>
</table><!-- grid-header -->
${grid.render_grid()|n}
</div><!-- newgrid-wrapper -->

View file

@ -1,10 +1,10 @@
## -*- coding: utf-8 -*-
## -*- coding: utf-8; -*-
<%inherit file="/messages/index.mako" />
<%def name="title()">Message Archive</%def>
<%def name="head_tags()">
${parent.head_tags()}
<%def name="extra_javascript()">
${parent.extra_javascript()}
<script type="text/javascript">
destination = "Inbox";
</script>

View file

@ -3,8 +3,8 @@
<%def name="title()">Message Inbox</%def>
<%def name="head_tags()">
${parent.head_tags()}
<%def name="extra_javascript()">
${parent.extra_javascript()}
<script type="text/javascript">
destination = "Archive";
</script>

View file

@ -1,14 +1,14 @@
## -*- coding: utf-8 -*-
<%inherit file="/master/index.mako" />
## -*- coding: utf-8; -*-
<%inherit file="/master2/index.mako" />
<%def name="head_tags()">
${parent.head_tags()}
<%def name="extra_javascript()">
${parent.extra_javascript()}
<script type="text/javascript">
var destination = null;
function update_move_button() {
var count = $('.newgrid tbody td.checkbox input:checked').length;
var count = $('.newgrid tr:not(.header) td.checkbox input:checked').length;
$('form[name="move-selected"] button')
.button('option', 'label', "Move " + count + " selected to " + destination)
.button('option', 'disabled', count < 1);
@ -18,17 +18,17 @@
update_move_button();
$('.newgrid-wrapper').on('click', 'thead th.checkbox input', function() {
$('.newgrid-wrapper').on('change', 'tr.header td.checkbox input', function() {
update_move_button();
});
$('.newgrid-wrapper').on('click', 'tbody td.checkbox input', function() {
$('.newgrid-wrapper').on('click', 'tr:not(.header) td.checkbox input', function() {
update_move_button();
});
$('form[name="move-selected"]').submit(function() {
var uuids = [];
$('.newgrid tbody td.checkbox input:checked').each(function() {
$('.newgrid tr:not(.header) td.checkbox input:checked').each(function() {
uuids.push($(this).parents('tr:first').data('uuid'));
});
if (! uuids.length) {

View file

@ -1,8 +1,30 @@
## -*- coding: utf-8 -*-
<%inherit file="/master/view.mako" />
<%def name="head_tags()">
${parent.head_tags()}
<%def name="extra_javascript()">
${parent.extra_javascript()}
<script type="text/javascript">
$(function() {
$('.field-wrapper.recipients .more').click(function() {
$(this).hide();
$(this).siblings('.everyone').css('display', 'inline-block');
return false;
});
$('.field-wrapper.recipients .everyone').click(function() {
$(this).hide();
$(this).siblings('.more').show();
});
});
</script>
</%def>
<%def name="extra_styles()">
${parent.extra_styles()}
<style type="text/css">
.field-wrapper.recipients .everyone {
cursor: pointer;
@ -22,24 +44,6 @@
margin-bottom: 15px;
}
</style>
<script type="text/javascript">
$(function() {
$('.field-wrapper.recipients .more').click(function() {
$(this).hide();
$(this).siblings('.everyone').css('display', 'inline-block');
return false;
});
$('.field-wrapper.recipients .everyone').click(function() {
$(this).hide();
$(this).siblings('.more').show();
});
});
</script>
</%def>
<%def name="context_menu_items()">

View file

@ -0,0 +1,7 @@
## -*- coding: utf-8; -*-
% if grid.filterable:
${grid.render_filters()|n}
% endif
${grid.render_grid()|n}

View file

@ -1,13 +1,7 @@
## -*- coding: utf-8; -*-
% if grid.filterable:
${grid.render_filters()|n}
% endif
<ul data-role="listview">
% for obj in grid.iter_rows():
<li>${grid.listitem.render_readonly()}</li>
% endfor
${grid.make_webhelpers_grid()}
</ul>
## <table data-role="table" class="ui-responsive table-stroke">

View file

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

View file

@ -1,5 +1,5 @@
## -*- coding: utf-8 -*-
<%inherit file="/master/index.mako" />
## -*- coding: utf-8; -*-
<%inherit file="/master2/index.mako" />
<%def name="context_menu_items()">
${parent.context_menu_items()}

View file

@ -1,8 +1,8 @@
## -*- coding: utf-8 -*-
<%inherit file="/master/index.mako" />
<%inherit file="/master2/index.mako" />
<%def name="head_tags()">
${parent.head_tags()}
<%def name="extra_styles()">
${parent.extra_styles()}
<style type="text/css">
table.label-printing th {
@ -32,6 +32,10 @@
}
</style>
</%def>
<%def name="extra_javascript()">
${parent.extra_javascript()}
% if label_profiles and request.has_perm('products.print_labels'):
<script type="text/javascript">

View file

@ -28,6 +28,7 @@ from __future__ import unicode_literals, absolute_import
from .core import View
from .master import MasterView
from .master2 import MasterView2
# TODO: deprecate / remove some of this
from .autocomplete import AutocompleteView

View file

@ -27,3 +27,4 @@ Views for batches
from __future__ import unicode_literals, absolute_import
from .core import BatchMasterView, FileBatchMasterView
from .core2 import BatchMasterView2, FileBatchMasterView2

View file

@ -0,0 +1,126 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2017 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/>.
#
################################################################################
"""
Base views for maintaining batches
"""
from __future__ import unicode_literals, absolute_import
import six
from rattail.db import model
from tailbone import grids3 as grids
from tailbone.views import MasterView2
from tailbone.views.batch import BatchMasterView, FileBatchMasterView
from tailbone.views.batch.core import MobileBatchStatusFilter
class BatchMasterView2(MasterView2, BatchMasterView):
"""
Base class for all "batch master" views
"""
grid_columns = [
'id',
'created',
'created_by',
'rowcount',
'status_code',
'complete',
'executed',
'executed_by',
]
def configure_grid(self, g):
super(BatchMasterView2, self).configure_grid(g)
g.joiners['created_by'] = lambda q: q.join(model.User, model.User.uuid == self.model_class.created_by_uuid)
g.joiners['executed_by'] = lambda q: q.outerjoin(model.User, model.User.uuid == self.model_class.executed_by_uuid)
g.filters['executed'].default_active = True
g.filters['executed'].default_verb = 'is_null'
# TODO: not sure this todo is still relevant?
# TODO: in some cases grid has no sorters yet..e.g. when building query for bulk-delete
# if hasattr(g, 'sorters'):
g.sorters['created_by'] = g.make_sorter(model.User.username)
g.sorters['executed_by'] = g.make_sorter(model.User.username)
g.default_sortkey = 'id'
g.default_sortdir = 'desc'
g.set_enum('status_code', self.model_class.STATUS)
g.set_type('created', 'datetime')
g.set_type('executed', 'datetime')
g.set_renderer('id', self.render_batch_id)
g.set_link('id')
g.set_label('id', "Batch ID")
g.set_label('created_by', "Created by")
g.set_label('rowcount', "Rows")
g.set_label('status_code', "Status")
g.set_label('executed_by', "Executed by")
def render_batch_id(self, batch, column):
return batch.id_str
def configure_row_grid(self, g):
super(BatchMasterView2, self).configure_row_grid(g)
g.filters['status_code'].set_value_renderer(grids.filters.EnumValueRenderer(self.model_row_class.STATUS))
g.default_sortkey = 'sequence'
g.set_enum('status_code', self.model_row_class.STATUS)
g.set_renderer('status_code', self.render_row_status)
g.set_label('sequence', "Seq.")
g.set_label('status_code', "Status")
def render_row_status(self, row, column):
code = row.status_code
if code is None:
return ""
text = self.model_row_class.STATUS.get(code, six.text_type(code))
if row.status_text:
return HTML.tag('span', title=row.status_text, c=text)
return text
def make_mobile_filters(self):
"""
Returns a set of filters for the mobile grid.
"""
filters = grids.filters.GridFilterSet()
filters['status'] = MobileBatchStatusFilter(self.model_class, 'status', default_value='pending')
return filters
class FileBatchMasterView2(BatchMasterView2, FileBatchMasterView):
"""
Base class for all file-based "batch master" views
"""

View file

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
@ -29,7 +29,7 @@ from __future__ import unicode_literals, absolute_import
from rattail.db import model
from tailbone import forms
from tailbone.views.batch import BatchMasterView
from tailbone.views.batch import BatchMasterView2 as BatchMasterView
class PricingBatchView(BatchMasterView):
@ -46,6 +46,31 @@ class PricingBatchView(BatchMasterView):
rows_editable = True
bulk_deletable = True
grid_columns = [
'id',
'created',
'created_by',
'rowcount',
# 'status_code',
# 'complete',
'executed',
'executed_by',
]
row_grid_columns = [
'sequence',
'upc',
'brand_name',
'description',
'size',
'discounted_unit_cost',
'old_price',
'new_price',
'price_margin',
'price_diff',
'status_code',
]
def configure_fieldset(self, fs):
fs.configure(
include=[
@ -57,43 +82,25 @@ class PricingBatchView(BatchMasterView):
fs.executed_by,
])
def _preconfigure_row_grid(self, g):
super(PricingBatchView, self)._preconfigure_row_grid(g)
g.upc.set(label="UPC")
g.brand_name.set(label="Brand")
g.regular_unit_cost.set(label="Reg. Cost")
g.discounted_unit_cost.set(label="Disc. Cost")
g.old_price.set(renderer=forms.renderers.CurrencyFieldRenderer)
g.new_price.set(renderer=forms.renderers.CurrencyFieldRenderer)
g.price_margin.set(label="Margin")
g.price_markup.set(label="Markup")
g.price_diff.set(label="Diff", renderer=forms.renderers.CurrencyFieldRenderer)
def configure_row_grid(self, g):
g.configure(
include=[
g.sequence,
g.upc,
g.brand_name,
g.description,
g.size,
g.discounted_unit_cost,
g.old_price,
g.new_price,
g.price_margin,
g.price_diff,
g.status_code,
],
readonly=True)
super(PricingBatchView, self).configure_row_grid(g)
def row_grid_row_attrs(self, row, i):
attrs = {}
if row.status_code in (row.STATUS_PRICE_INCREASE,
row.STATUS_PRICE_DECREASE):
attrs['class_'] = 'notice'
elif row.status_code == row.STATUS_CANNOT_CALCULATE_PRICE:
attrs['class_'] = 'warning'
return attrs
g.set_type('old_price', 'currency')
g.set_type('new_price', 'currency')
g.set_type('price_diff', 'currency')
g.set_label('upc', "UPC")
g.set_label('brand_name', "Brand")
g.set_label('regular_unit_cost', "Reg. Cost")
g.set_label('price_margin', "Margin")
g.set_label('price_markup', "Markup")
g.set_label('price_diff', "Diff")
def row_grid_extra_class(self, row, i):
if row.status_code == row.STATUS_CANNOT_CALCULATE_PRICE:
return 'warning'
if row.status_code in (row.STATUS_PRICE_INCREASE, row.STATUS_PRICE_DECREASE):
return 'notice'
def _preconfigure_row_fieldset(self, fs):
super(PricingBatchView, self)._preconfigure_row_fieldset(fs)

View file

@ -37,9 +37,9 @@ import formalchemy
from pyramid.response import FileResponse
from webhelpers2.html import literal
from tailbone import newgrids as grids
from tailbone import grids3 as grids
from tailbone.db import Session
from tailbone.views import MasterView
from tailbone.views import MasterView2 as MasterView
from tailbone.forms.renderers.bouncer import BounceMessageFieldRenderer
@ -53,36 +53,39 @@ class EmailBouncesView(MasterView):
creatable = False
editable = False
grid_columns = [
'config_key',
'bounced',
'bounce_recipient_address',
'intended_recipient_address',
'processed_by',
]
def __init__(self, request):
super(EmailBouncesView, self).__init__(request)
self.handler_options = [('', '(any)')] + sorted(get_profile_keys(self.rattail_config))
self.handler_options = sorted(get_profile_keys(self.rattail_config))
def get_handler(self, bounce):
return get_handler(self.rattail_config, bounce.config_key)
def configure_grid(self, g):
super(EmailBouncesView, self).configure_grid(g)
g.joiners['processed_by'] = lambda q: q.outerjoin(model.User)
g.filters['config_key'].default_active = True
g.filters['config_key'].default_verb = 'equal'
g.filters['config_key'].label = "Source"
g.filters['config_key'].set_value_renderer(grids.filters.ChoiceValueRenderer(self.handler_options))
g.filters['bounce_recipient_address'].label = "Bounced To"
g.filters['intended_recipient_address'].label = "Intended For"
g.filters['processed'].default_active = True
g.filters['processed'].default_verb = 'is_null'
g.filters['processed_by'] = g.make_filter('processed_by', model.User.username)
g.sorters['processed_by'] = g.make_sorter(model.User.username)
g.default_sortkey = 'bounced'
g.default_sortdir = 'desc'
g.configure(
include=[
g.config_key.label("Source"),
g.bounced,
g.bounce_recipient_address.label("Bounced To"),
g.intended_recipient_address.label("Intended For"),
g.processed_by,
],
readonly=True)
g.set_label('config_key', "Source")
g.set_label('bounce_recipient_address', "Bounced To")
g.set_label('intended_recipient_address', "Intended For")
def configure_fieldset(self, fs):
bounce = fs.model

View file

@ -28,7 +28,7 @@ from __future__ import unicode_literals, absolute_import
from rattail.db import model
from tailbone.views import MasterView, AutocompleteView
from tailbone.views import MasterView2 as MasterView, AutocompleteView
class BrandsView(MasterView):
@ -36,16 +36,14 @@ class BrandsView(MasterView):
Master view for the Brand class.
"""
model_class = model.Brand
grid_columns = [
'name',
]
def configure_grid(self, g):
g.filters['name'].default_active = True
g.filters['name'].default_verb = 'contains'
g.default_sortkey = 'name'
g.configure(
include=[
g.name,
],
readonly=True)
def configure_fieldset(self, fs):
fs.configure(

View file

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
@ -29,7 +29,7 @@ from __future__ import unicode_literals, absolute_import
from rattail.db import model
from tailbone.db import Session
from tailbone.views import MasterView
from tailbone.views import MasterView2 as MasterView
class CustomerGroupsView(MasterView):
@ -38,17 +38,16 @@ class CustomerGroupsView(MasterView):
"""
model_class = model.CustomerGroup
model_title = "Customer Group"
grid_columns = [
'id',
'name',
]
def configure_grid(self, g):
g.filters['name'].default_active = True
g.filters['name'].default_verb = 'contains'
g.default_sortkey = 'name'
g.configure(
include=[
g.id.label("ID"),
g.name,
],
readonly=True)
g.set_label('id', "ID")
def configure_fieldset(self, fs):
fs.configure(

View file

@ -35,7 +35,7 @@ from pyramid.httpexceptions import HTTPNotFound
from tailbone import forms
from tailbone.db import Session
from tailbone.views import MasterView, AutocompleteView
from tailbone.views import MasterView2 as MasterView, AutocompleteView
from rattail.db import model
@ -47,8 +47,17 @@ class CustomersView(MasterView):
model_class = model.Customer
has_versions = True
supports_mobile = True
grid_columns = [
'id',
'number',
'name',
'phone',
'email',
]
def configure_grid(self, g):
super(CustomersView, self).configure_grid(g)
def _preconfigure_grid(self, g):
g.joiners['email'] = lambda q: q.outerjoin(model.CustomerEmailAddress, sa.and_(
model.CustomerEmailAddress.parent_uuid == model.Customer.uuid,
model.CustomerEmailAddress.preference == 1))
@ -66,23 +75,15 @@ class CustomersView(MasterView):
g.filters['name'].default_active = True
g.filters['name'].default_verb = 'contains'
g.filters['id'].label = "ID"
g.sorters['email'] = lambda q, d: q.order_by(getattr(model.CustomerEmailAddress.address, d)())
g.sorters['phone'] = lambda q, d: q.order_by(getattr(model.CustomerPhoneNumber.number, d)())
g.default_sortkey = 'name'
def configure_grid(self, g):
g.configure(
include=[
g.id.label("ID"),
g.number,
g.name,
g.phone.label("Phone Number"),
g.email.label("Email Address"),
],
readonly=True)
g.set_label('id', "ID")
g.set_label('phone', "Phone Number")
g.set_label('email', "Email Address")
def get_mobile_data(self, session=None):
# TODO: hacky!

View file

@ -31,7 +31,7 @@ import logging
from rattail.db import model
from tailbone.views import MasterView
from tailbone.views import MasterView2 as MasterView
log = logging.getLogger(__name__)
@ -48,18 +48,18 @@ class DataSyncChangesView(MasterView):
creatable = False
editable = False
grid_columns = [
'source',
'payload_type',
'payload_key',
'deletion',
'obtained',
'consumer',
]
def configure_grid(self, g):
super(DataSyncChangesView, self).configure_grid(g)
g.default_sortkey = 'obtained'
g.configure(
include=[
g.source,
g.payload_type,
g.payload_key,
g.deletion,
g.obtained,
g.consumer,
],
readonly=True)
def restart(self):
# TODO: Add better validation (e.g. CSRF) here?

View file

@ -28,8 +28,9 @@ from __future__ import unicode_literals, absolute_import
from rattail.db import model
from tailbone import newgrids as grids
from tailbone.views import MasterView, AutocompleteView
from tailbone import grids3 as grids
from tailbone.newgrids import AlchemyGrid
from tailbone.views import MasterView2 as MasterView, AutocompleteView
class DepartmentsView(MasterView):
@ -38,16 +39,16 @@ class DepartmentsView(MasterView):
"""
model_class = model.Department
grid_columns = [
'number',
'name',
]
def configure_grid(self, g):
super(DepartmentsView, self).configure_grid(g)
g.filters['name'].default_active = True
g.filters['name'].default_verb = 'contains'
g.default_sortkey = 'number'
g.configure(
include=[
g.number,
g.name,
],
readonly=True)
def configure_fieldset(self, fs):
fs.configure(
@ -67,7 +68,7 @@ class DepartmentsView(MasterView):
# shouldn't need a key for this one, for instance (no settings
# required), but there is plenty of room for improvement here.
employees = sorted(department.employees, key=unicode)
employees = grids.AlchemyGrid('departments.employees', self.request, data=employees, model_class=model.Employee,
employees = AlchemyGrid('departments.employees', self.request, data=employees, model_class=model.Employee,
main_actions=[
grids.GridAction('view', icon='zoomin',
url=lambda r, i: self.request.route_url('employees.view', uuid=r.uuid)),

View file

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
@ -26,36 +26,18 @@ Email Views
from __future__ import unicode_literals, absolute_import
import formalchemy
from formalchemy.helpers import text_area
from pyramid.httpexceptions import HTTPFound
from rattail import mail
from rattail.db import api
from rattail.config import parse_list
from tailbone import forms, newgrids as grids
import formalchemy
from formalchemy.helpers import text_area
from pyramid.httpexceptions import HTTPFound
from webhelpers2.html import HTML
from tailbone import forms
from tailbone.db import Session
from tailbone.views import MasterView, View
class BoolGridColumn(grids.GridColumn):
def render(self, value):
if value is None:
return ''
return 'Yes' if value else 'No'
class EmailListGridColumn(grids.GridColumn):
def render(self, value):
if not value:
return ''
recips = parse_list(value)
if len(recips) < 3:
return value
return "{}, ...".format(', '.join(recips[:2]))
from tailbone.views import View, MasterView2 as MasterView
class EmailListFieldRenderer(formalchemy.TextAreaFieldRenderer):
@ -75,13 +57,17 @@ class ProfilesView(MasterView):
model_title = "Email Profile"
model_key = 'key'
url_prefix = '/email/profiles'
grid_factory = grids.Grid
filterable = False
pageable = False
creatable = False
deletable = False
grid_columns = [
'key',
'prefix',
'subject',
'to',
'enabled',
]
def get_data(self, session=None):
data = []
@ -91,6 +77,23 @@ class ProfilesView(MasterView):
data.append(self.normalize(email))
return data
def configure_grid(self, g):
g.sorters['key'] = g.make_simple_sorter('key', foldcase=True)
g.sorters['prefix'] = g.make_simple_sorter('prefix', foldcase=True)
g.sorters['subject'] = g.make_simple_sorter('subject', foldcase=True)
g.sorters['to'] = g.make_simple_sorter('to', foldcase=True)
g.sorters['enabled'] = g.make_simple_sorter('enabled')
g.default_sortkey = 'key'
g.set_type('enabled', 'boolean')
g.set_link('key')
g.set_link('subject')
# Make edit link visible by default, no "More" actions.
if g.more_actions:
g.main_actions.append(g.more_actions.pop())
def normalize(self, email):
def get_recips(type_):
recips = email.get_recips(type_)
@ -112,26 +115,6 @@ class ProfilesView(MasterView):
'enabled': email.get_enabled(),
}
def configure_grid(self, g):
g.columns = [
grids.GridColumn('key'),
grids.GridColumn('prefix'),
grids.GridColumn('subject'),
EmailListGridColumn('to'),
BoolGridColumn('enabled'),
]
g.sorters['key'] = g.make_sorter('key', foldcase=True)
g.sorters['prefix'] = g.make_sorter('prefix', foldcase=True)
g.sorters['subject'] = g.make_sorter('subject', foldcase=True)
g.sorters['to'] = g.make_sorter('to', foldcase=True)
g.sorters['enabled'] = g.make_sorter('enabled')
g.default_sortkey = 'key'
# Make edit link visible by default, no "More" actions.
if g.more_actions:
g.main_actions.append(g.more_actions.pop())
def get_instance(self):
key = self.request.matchdict['key']
return self.normalize(mail.get_email(self.rattail_config, key))

View file

@ -32,9 +32,9 @@ from rattail.db import model
import formalchemy as fa
from tailbone import forms, newgrids as grids
from tailbone import forms, grids3 as grids
from tailbone.db import Session
from tailbone.views import MasterView, AutocompleteView
from tailbone.views import MasterView2 as MasterView, AutocompleteView
class EmployeesView(MasterView):
@ -44,7 +44,18 @@ class EmployeesView(MasterView):
model_class = model.Employee
has_versions = True
def _preconfigure_grid(self, g):
grid_columns = [
'id',
'first_name',
'last_name',
'phone',
'email',
'status',
]
def configure_grid(self, g):
super(EmployeesView, self).configure_grid(g)
g.joiners['phone'] = lambda q: q.outerjoin(model.EmployeePhoneNumber, sa.and_(
model.EmployeePhoneNumber.parent_uuid == model.Employee.uuid,
model.EmployeePhoneNumber.preference == 1))
@ -61,7 +72,6 @@ class EmployeesView(MasterView):
label="Phone Number")
if self.request.has_perm('employees.edit'):
g.filters['id'].label = "ID"
g.filters['status'].default_active = True
g.filters['status'].default_verb = 'equal'
g.filters['status'].default_value = self.enum.EMPLOYEE_STATUS_CURRENT
@ -84,25 +94,15 @@ class EmployeesView(MasterView):
g.default_sortkey = 'first_name'
g.append(forms.AssociationProxyField('first_name'))
g.append(forms.AssociationProxyField('last_name'))
g.set_enum('status', self.enum.EMPLOYEE_STATUS)
def configure_grid(self, g):
g.configure(
include=[
g.id.label("ID"),
g.first_name,
g.last_name,
g.phone.label("Phone Number"),
g.email.label("Email Address"),
g.status.with_renderer(forms.renderers.EnumFieldRenderer(self.enum.EMPLOYEE_STATUS)),
],
readonly=True)
g.set_label('id', "ID")
g.set_label('phone', "Phone Number")
g.set_label('email', "Email Address")
if not self.request.has_perm('employees.edit'):
del g.id
del g.status
g.hide_column('id')
g.hide_column('status')
def query(self, session):
q = session.query(model.Employee).join(model.Person)

View file

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
@ -34,7 +34,7 @@ import formalchemy as fa
from pyramid.response import FileResponse
from tailbone import forms
from tailbone.views import MasterView
from tailbone.views import MasterView2 as MasterView
class ExportMasterView(MasterView):
@ -45,6 +45,13 @@ class ExportMasterView(MasterView):
editable = False
export_has_file = False
grid_columns = [
'id',
'created',
'created_by',
'record_count',
]
def get_export_key(self):
if hasattr(self, 'export_key'):
return self.export_key
@ -56,26 +63,22 @@ class ExportMasterView(MasterView):
export.filename,
makedirs=makedirs)
def _preconfigure_grid(self, g):
g.filters['id'].label = "ID"
g.id.set(label="ID", renderer=forms.renderers.BatchIDFieldRenderer)
def configure_grid(self, g):
super(ExportMasterView, self).configure_grid(g)
g.joiners['created_by'] = lambda q: q.join(model.User)
g.sorters['created_by'] = g.make_sorter(model.User.username)
g.filters['created_by'] = g.make_filter('created_by', model.User.username,
label="Created by")
g.created_by.set(label="Created by", renderer=forms.renderers.UserFieldRenderer)
g.filters['created_by'] = g.make_filter('created_by', model.User.username)
g.default_sortkey = 'created'
g.default_sortdir = 'desc'
def configure_grid(self, g):
g.configure(
include=[
g.id,
g.created,
g.created_by,
g.record_count,
],
readonly=True)
g.set_renderer('id', self.render_id)
g.set_label('id', "ID")
g.set_label('created_by', "Created by")
def render_id(self, export, column):
return export.id_str
def _preconfigure_fieldset(self, fs):
fs.id.set(label="ID", renderer=forms.renderers.BatchIDFieldRenderer)

View file

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
@ -26,10 +26,10 @@ Family Views
from __future__ import unicode_literals, absolute_import
from tailbone.views import MasterView
from rattail.db import model
from tailbone.views import MasterView2 as MasterView
class FamiliesView(MasterView):
"""
@ -39,17 +39,15 @@ class FamiliesView(MasterView):
model_title_plural = "Families"
route_prefix = 'families'
grid_key = 'families'
grid_columns = [
'code',
'name',
]
def configure_grid(self, g):
g.filters['name'].default_active = True
g.filters['name'].default_verb = 'contains'
g.default_sortkey = 'code'
g.configure(
include=[
g.code,
g.name,
],
readonly=True)
def configure_fieldset(self, fs):
fs.configure(

View file

@ -38,7 +38,7 @@ from webhelpers2.html import tags
from tailbone import forms
from tailbone.db import Session
from tailbone.views.batch import FileBatchMasterView
from tailbone.views.batch import FileBatchMasterView2 as FileBatchMasterView
ACTION_OPTIONS = OrderedDict([
@ -84,27 +84,37 @@ class HandheldBatchView(FileBatchMasterView):
rows_creatable = False
rows_editable = True
grid_columns = [
'id',
'device_type',
'device_name',
'created',
'created_by',
'rowcount',
'status_code',
'executed',
]
row_grid_columns = [
'sequence',
'upc',
'brand_name',
'description',
'size',
'cases',
'units',
'status_code',
]
def configure_grid(self, g):
super(HandheldBatchView, self).configure_grid(g)
device_types = OrderedDict(sorted(self.enum.HANDHELD_DEVICE_TYPE.items(),
key=lambda item: item[1]))
g.configure(
include=[
g.id,
g.device_type.with_renderer(forms.renderers.EnumFieldRenderer(device_types)),
g.device_name,
g.created,
g.created_by,
g.rowcount,
g.status_code,
g.executed,
],
readonly=True)
g.set_enum('device_type', device_types)
def row_attrs(self, batch, i):
attrs = {}
def grid_extra_class(self, batch, i):
if batch.status_code is not None and batch.status_code != batch.STATUS_OK:
attrs['class_'] = 'notice'
return attrs
return 'notice'
def _preconfigure_fieldset(self, fs):
super(HandheldBatchView, self)._preconfigure_fieldset(fs)
@ -145,32 +155,16 @@ class HandheldBatchView(FileBatchMasterView):
kwargs['device_name'] = batch.device_name
return kwargs
def _preconfigure_row_grid(self, g):
super(HandheldBatchView, self)._preconfigure_row_grid(g)
g.upc.set(label="UPC")
g.brand_name.set(label="Brand")
g.cases.set(renderer=forms.renderers.QuantityFieldRenderer)
g.units.set(renderer=forms.renderers.QuantityFieldRenderer)
def configure_row_grid(self, g):
g.configure(
include=[
g.sequence,
g.upc,
g.brand_name,
g.description,
g.size,
g.cases,
g.units,
g.status_code,
],
readonly=True)
super(HandheldBatchView, self).configure_row_grid(g)
g.set_type('cases', 'quantity')
g.set_type('units', 'quantity')
g.set_label('upc', "UPC")
g.set_label('brand_name', "Brand")
def row_grid_row_attrs(self, row, i):
attrs = {}
def row_grid_extra_class(self, row, i):
if row.status_code == row.STATUS_PRODUCT_NOT_FOUND:
attrs['class_'] = 'warning'
return attrs
return 'warning'
def _preconfigure_row_fieldset(self, fs):
super(HandheldBatchView, self)._preconfigure_row_fieldset(fs)

View file

@ -28,6 +28,8 @@ from __future__ import unicode_literals, absolute_import
import re
import six
from rattail import pod
from rattail.db import model, api
from rattail.time import localtime
@ -36,10 +38,9 @@ from rattail.util import pretty_quantity
import formalchemy as fa
import formencode as fe
from webhelpers2.html import tags
from tailbone import forms
from tailbone.views.batch import BatchMasterView
from tailbone.views.batch import BatchMasterView2 as BatchMasterView
class InventoryBatchView(BatchMasterView):
@ -54,28 +55,42 @@ class InventoryBatchView(BatchMasterView):
creatable = False
mobile_creatable = True
grid_columns = [
'id',
'created',
'created_by',
'rowcount',
'executed',
'executed_by',
'mode',
]
model_row_class = model.InventoryBatchRow
rows_editable = True
def _preconfigure_grid(self, g):
super(InventoryBatchView, self)._preconfigure_grid(g)
g.mode.set(renderer=forms.renderers.EnumFieldRenderer(self.enum.INVENTORY_MODE),
label="Count Mode")
row_grid_columns = [
'sequence',
'upc',
'brand_name',
'description',
'size',
'cases',
'units',
'unit_cost',
'status_code',
]
def configure_grid(self, g):
g.configure(include=[
g.id,
g.created,
g.created_by,
g.rowcount,
g.executed,
g.executed_by,
g.mode,
], readonly=True)
super(InventoryBatchView, self).configure_grid(g)
g.set_enum('mode', self.enum.INVENTORY_MODE)
g.set_label('mode', "Count Mode")
def configure_mobile_grid(self, g):
super(InventoryBatchView, self).configure_mobile_grid(g)
g.listitem.set(renderer=InventoryBatchRenderer)
def render_mobile_listitem(self, batch, i):
return "({}) {} rows - {}, {}".format(
batch.id_str,
"?" if batch.rowcount is None else batch.rowcount,
batch.created_by,
localtime(self.request.rattail_config, batch.created, from_utc=True).strftime('%Y-%m-%d'))
def _preconfigure_fieldset(self, fs):
super(InventoryBatchView, self)._preconfigure_fieldset(fs)
@ -202,45 +217,26 @@ class InventoryBatchView(BatchMasterView):
return self.render_to_response('view_row', context, mobile=True)
def _preconfigure_row_grid(self, g):
super(InventoryBatchView, self)._preconfigure_row_grid(g)
g.upc.set(label="UPC")
g.brand_name.set(label="Brand")
g.cases.set(renderer=forms.renderers.QuantityFieldRenderer)
g.units.set(renderer=forms.renderers.QuantityFieldRenderer)
g.status_code.set(label="Status")
g.unit_cost.set(renderer=forms.renderers.CurrencyFieldRenderer)
def configure_row_grid(self, g):
g.configure(
include=[
g.sequence,
g.upc,
g.brand_name,
g.description,
g.size,
g.cases,
g.units,
g.unit_cost,
g.status_code,
],
readonly=True)
super(InventoryBatchView, self).configure_row_grid(g)
def row_grid_row_attrs(self, row, i):
attrs = {}
g.set_renderer('cases', 'quantity')
g.set_renderer('units', 'quantity')
g.set_renderer('unit_cost', 'currency')
g.set_label('upc', "UPC")
g.set_label('brand_name', "Brand")
g.set_label('status_code', "Status")
def row_grid_extra_class(self, row, i):
if row.status_code == row.STATUS_PRODUCT_NOT_FOUND:
attrs['class_'] = 'warning'
return attrs
return 'warning'
def render_mobile_row_listitem(self, row, **kwargs):
if row is None:
return ''
def render_mobile_row_listitem(self, row, i):
description = row.product.full_description if row.product else row.description
unit_uom = 'LB' if row.product and row.product.weighed else 'EA'
qty = "{} {}".format(pretty_quantity(row.cases or row.units), 'CS' if row.cases else unit_uom)
title = "({}) {} - {}".format(row.upc.pretty(), description, qty)
url = self.request.route_url('mobile.batch.inventory.rows.view', uuid=row.uuid)
return tags.link_to(title, url)
return "({}) {} - {}".format(row.upc.pretty(), description, qty)
def _preconfigure_row_fieldset(self, fs):
super(InventoryBatchView, self)._preconfigure_row_fieldset(fs)
@ -281,19 +277,6 @@ class InventoryBatchView(BatchMasterView):
permission='{}.create'.format(row_permission_prefix))
class InventoryBatchRenderer(fa.FieldRenderer):
def render_readonly(self, **kwargs):
batch = self.raw_value
title = "({}) {} rows - {}, {}".format(
batch.id_str,
"?" if batch.rowcount is None else batch.rowcount,
batch.created_by,
localtime(self.request.rattail_config, batch.created, from_utc=True).strftime('%Y-%m-%d'))
url = self.request.route_url('mobile.batch.inventory.view', uuid=batch.uuid)
return tags.link_to(title, url)
class ValidBatchRow(forms.validators.ModelValidator):
model_class = model.InventoryBatchRow

View file

@ -31,7 +31,7 @@ from rattail.db import model
import formalchemy as fa
from tailbone import forms
from tailbone.views.batch import BatchMasterView
from tailbone.views.batch import BatchMasterView2 as BatchMasterView
class LabelBatchView(BatchMasterView):
@ -49,6 +49,19 @@ class LabelBatchView(BatchMasterView):
rows_editable = True
cloneable = True
row_grid_columns = [
'sequence',
'upc',
'brand_name',
'description',
'size',
'regular_price',
'sale_price',
'label_profile',
'label_quantity',
'status_code',
]
def _preconfigure_fieldset(self, fs):
super(LabelBatchView, self)._preconfigure_fieldset(fs)
fs.append(fa.Field('handheld_batches', renderer=forms.renderers.HandheldBatchesFieldRenderer, readonly=True,
@ -69,35 +82,17 @@ class LabelBatchView(BatchMasterView):
if self.viewing and not batch._handhelds:
del fs.handheld_batches
def _preconfigure_row_grid(self, g):
super(LabelBatchView, self)._preconfigure_row_grid(g)
g.upc.set(label="UPC")
g.brand_name.set(label="Brand")
g.regular_price.set(label="Reg Price")
g.label_profile.set(label="Label Type")
g.label_quantity.set(label="Qty")
def configure_row_grid(self, g):
g.configure(
include=[
g.sequence,
g.upc,
g.brand_name,
g.description,
g.size,
g.regular_price,
g.sale_price,
g.label_profile,
g.label_quantity,
g.status_code,
],
readonly=True)
super(LabelBatchView, self).configure_row_grid(g)
g.set_label('upc', "UPC")
g.set_label('brand_name', "Brand")
g.set_label('regular_price', "Reg Price")
g.set_label('label_profile', "Label Type")
g.set_label('label_quantity', "Qty")
def row_grid_row_attrs(self, row, i):
attrs = {}
def row_grid_extra_class(self, row, i):
if row.status_code != row.STATUS_OK:
attrs['class_'] = 'warning'
return attrs
return 'warning'
def _preconfigure_row_fieldset(self, fs):
fs.sequence.set(readonly=True)

View file

@ -32,7 +32,7 @@ from pyramid.httpexceptions import HTTPFound
from tailbone import forms
from tailbone.db import Session
from tailbone.views import MasterView
from tailbone.views import MasterView2 as MasterView
class ProfilesView(MasterView):
@ -42,17 +42,17 @@ class ProfilesView(MasterView):
model_class = model.LabelProfile
model_title = "Label Profile"
url_prefix = '/labels/profiles'
grid_columns = [
'ordinal',
'code',
'description',
'visible',
]
def configure_grid(self, g):
super(ProfilesView, self).configure_grid(g)
g.default_sortkey = 'ordinal'
g.configure(
include=[
g.ordinal,
g.code,
g.description,
g.visible,
],
readonly=True)
g.set_type('visible', 'boolean')
def configure_fieldset(self, fs):
fs.printer_spec.set(renderer=forms.renderers.StrippedTextFieldRenderer)

View file

@ -129,7 +129,7 @@ class MasterView(View):
return self.redirect(self.request.current_route_url(_query=None))
# Stash some grid stats, for possible use when generating URLs.
if grid.pageable and grid.pager:
if grid.pageable and hasattr(grid, 'pager'):
self.first_visible_grid_index = grid.pager.first_item
# Return grid only, if partial page was requested.
@ -170,6 +170,15 @@ class MasterView(View):
"""
return getattr(cls, 'mobile_grid_factory', MobileGrid)
@classmethod
def get_mobile_row_grid_factory(cls):
"""
Must return a callable to be used when creating new mobile grid
instances. Instead of overriding this, you can set
:attr:`mobile_grid_factory`. Default factory is :class:`MobileGrid`.
"""
return getattr(cls, 'mobile_row_grid_factory', MobileGrid)
@classmethod
def get_mobile_grid_key(cls):
"""
@ -643,7 +652,7 @@ class MasterView(View):
kwargs.setdefault('request', self.request)
kwargs.setdefault('model_class', self.model_row_class)
kwargs = self.make_mobile_row_grid_kwargs(**kwargs)
factory = self.get_mobile_grid_factory()
factory = self.get_mobile_row_grid_factory()
grid = factory(**kwargs)
self.configure_mobile_row_grid(grid)
grid.load_settings()
@ -703,7 +712,7 @@ class MasterView(View):
kwargs['instance'] = parent
kwargs = self.make_row_grid_kwargs(**kwargs)
key = '{}.{}'.format(self.get_grid_key(), self.request.matchdict[self.get_model_key()])
factory = self.get_grid_factory()
factory = self.get_row_grid_factory()
grid = factory(key, self.request, data=data, model_class=self.model_row_class, **kwargs)
self._preconfigure_row_grid(grid)
self.configure_row_grid(grid)
@ -1253,6 +1262,15 @@ class MasterView(View):
"""
return getattr(cls, 'grid_factory', AlchemyGrid)
@classmethod
def get_row_grid_factory(cls):
"""
Must return a callable to be used when creating new row grid instances.
Instead of overriding this, you can set :attr:`row_grid_factory`.
Default factory is :class:`AlchemyGrid`.
"""
return getattr(cls, 'row_grid_factory', AlchemyGrid)
@classmethod
def get_grid_key(cls):
"""
@ -1787,12 +1805,14 @@ class MasterView(View):
def configure_row_fieldset(self, fs):
fs.configure()
def get_row_action_url(self, action, row):
def get_row_action_url(self, action, row, mobile=False):
"""
Generate a URL for the given action on the given row.
"""
return self.request.route_url('{}.{}'.format(self.get_row_route_prefix(), action),
**self.get_row_action_route_kwargs(row))
route_name = '{}.{}'.format(self.get_row_route_prefix(), action)
if mobile:
route_name = 'mobile.{}'.format(route_name)
return self.request.route_url(route_name, **self.get_row_action_route_kwargs(row))
def get_row_action_route_kwargs(self, row):
"""

337
tailbone/views/master2.py Normal file
View file

@ -0,0 +1,337 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2017 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/>.
#
################################################################################
"""
Master View
"""
from __future__ import unicode_literals, absolute_import
from tailbone import grids3 as grids
from tailbone.views import MasterView
class MasterView2(MasterView):
"""
Base "master" view class. All model master views should derive from this.
"""
sortable = True
rows_pageable = True
mobile_pageable = True
def get_fallback_templates(self, template, mobile=False):
if mobile:
return [
'/mobile/master/{}.mako'.format(template),
]
return [
'/master2/{}.mako'.format(template),
'/master/{}.mako'.format(template),
]
@classmethod
def get_grid_factory(cls):
"""
Returns the grid factory or class which is to be used when creating new
grid instances.
"""
return getattr(cls, 'grid_factory', grids.Grid)
@classmethod
def get_row_grid_factory(cls):
"""
Returns the grid factory or class which is to be used when creating new
row grid instances.
"""
return getattr(cls, 'row_grid_factory', grids.Grid)
@classmethod
def get_mobile_grid_factory(cls):
"""
Must return a callable to be used when creating new mobile grid
instances. Instead of overriding this, you can set
:attr:`mobile_grid_factory`. Default factory is :class:`MobileGrid`.
"""
return getattr(cls, 'mobile_grid_factory', grids.MobileGrid)
@classmethod
def get_mobile_row_grid_factory(cls):
"""
Must return a callable to be used when creating new mobile row grid
instances. Instead of overriding this, you can set
:attr:`mobile_row_grid_factory`. Default factory is :class:`MobileGrid`.
"""
return getattr(cls, 'mobile_row_grid_factory', grids.MobileGrid)
def make_grid(self, factory=None, key=None, data=None, columns=None, **kwargs):
"""
Creates a new grid instance
"""
if factory is None:
factory = self.get_grid_factory()
if key is None:
key = self.get_grid_key()
if data is None:
data = self.get_data(session=kwargs.get('session'))
if columns is None:
columns = self.get_grid_columns()
kwargs.setdefault('request', self.request)
kwargs = self.make_grid_kwargs(**kwargs)
grid = factory(key, data, columns, **kwargs)
self.configure_grid(grid)
grid.load_settings()
return grid
def make_row_grid(self, factory=None, key=None, data=None, columns=None, **kwargs):
"""
Make and return a new (configured) rows grid instance.
"""
instance = kwargs.pop('instance', None)
if not instance:
instance = self.get_instance()
if factory is None:
factory = self.get_row_grid_factory()
if key is None:
key = '{}.{}'.format(self.get_grid_key(), self.request.matchdict[self.get_model_key()])
if data is None:
data = self.get_row_data(instance)
if columns is None:
columns = self.get_row_grid_columns()
kwargs.setdefault('request', self.request)
kwargs = self.make_row_grid_kwargs(**kwargs)
grid = factory(key, data, columns, **kwargs)
self.configure_row_grid(grid)
grid.load_settings()
return grid
def make_mobile_grid(self, factory=None, key=None, data=None, columns=None, **kwargs):
"""
Creates a new mobile grid instance
"""
if factory is None:
factory = self.get_mobile_grid_factory()
if key is None:
key = self.get_mobile_grid_key()
if data is None:
data = self.get_mobile_data(session=kwargs.get('session'))
if columns is None:
columns = self.get_mobile_grid_columns()
kwargs.setdefault('request', self.request)
kwargs.setdefault('mobile', True)
kwargs = self.make_mobile_grid_kwargs(**kwargs)
grid = factory(key, data, columns, **kwargs)
self.configure_mobile_grid(grid)
grid.load_settings()
return grid
def make_mobile_row_grid(self, factory=None, key=None, data=None, columns=None, **kwargs):
"""
Make a new (configured) rows grid instance for mobile.
"""
instance = kwargs.pop('instance', self.get_instance())
if factory is None:
factory = self.get_mobile_row_grid_factory()
if key is None:
key = 'mobile.{}.{}'.format(self.get_grid_key(), self.request.matchdict[self.get_model_key()])
if data is None:
data = self.get_mobile_row_data(instance)
if columns is None:
columns = self.get_mobile_row_grid_columns()
kwargs.setdefault('request', self.request)
kwargs.setdefault('mobile', True)
kwargs = self.make_mobile_row_grid_kwargs(**kwargs)
grid = factory(key, data, columns, **kwargs)
self.configure_mobile_row_grid(grid)
grid.load_settings()
return grid
def get_grid_columns(self):
if hasattr(self, 'grid_columns'):
return self.grid_columns
# TODO
raise NotImplementedError
def get_row_grid_columns(self):
if hasattr(self, 'row_grid_columns'):
return self.row_grid_columns
# TODO
raise NotImplementedError
def get_mobile_grid_columns(self):
if hasattr(self, 'mobile_grid_columns'):
return self.mobile_grid_columns
# TODO
return ['listitem']
def get_mobile_row_grid_columns(self):
if hasattr(self, 'mobile_row_grid_columns'):
return self.mobile_row_grid_columns
# TODO
return ['listitem']
def make_grid_kwargs(self, **kwargs):
"""
Return a dictionary of kwargs to be passed to the factory when creating
new grid instances.
"""
defaults = {
'model_class': getattr(self, 'model_class', None),
# 'width': 'full',
'filterable': self.filterable,
'sortable': self.sortable,
'pageable': self.pageable,
'extra_row_class': self.grid_extra_class,
'url': lambda obj: self.get_action_url('view', obj),
'checkboxes': self.checkboxes or (
self.mergeable and self.request.has_perm('{}.merge'.format(self.get_permission_prefix()))),
}
if 'main_actions' not in kwargs and 'more_actions' not in kwargs:
main, more = self.get_grid_actions()
defaults['main_actions'] = main
defaults['more_actions'] = more
defaults.update(kwargs)
return defaults
def make_row_grid_kwargs(self, **kwargs):
"""
Return a dict of kwargs to be used when constructing a new rows grid.
"""
route_prefix = self.get_row_route_prefix()
permission_prefix = self.get_row_permission_prefix()
defaults = {
'model_class': self.model_row_class,
# 'width': 'full',
'filterable': self.rows_filterable,
'sortable': self.rows_sortable,
'pageable': self.rows_pageable,
'default_pagesize': self.rows_default_pagesize,
'extra_row_class': self.row_grid_extra_class,
}
if self.has_rows and 'main_actions' not in defaults:
actions = []
# view action
if self.rows_viewable:
view = lambda r, i: self.get_row_action_url('view', r)
actions.append(grids.GridAction('view', icon='zoomin', url=view))
# edit action
if self.rows_editable:
actions.append(grids.GridAction('edit', icon='pencil', url=self.row_edit_action_url))
# delete action
if self.rows_deletable and self.request.has_perm('{}.delete_row'.format(permission_prefix)):
actions.append(grids.GridAction('delete', icon='trash', url=self.row_delete_action_url))
defaults['delete_speedbump'] = self.rows_deletable_speedbump
defaults['main_actions'] = actions
defaults.update(kwargs)
return defaults
def make_mobile_grid_kwargs(self, **kwargs):
"""
Must return a dictionary of kwargs to be passed to the factory when
creating new mobile grid instances.
"""
defaults = {
'model_class': getattr(self, 'model_class', None),
'pageable': self.mobile_pageable,
'sortable': False,
'filterable': self.mobile_filterable,
'renderers': self.make_mobile_grid_renderers(),
'url': lambda obj: self.get_action_url('view', obj, mobile=True),
}
# TODO: this seems wrong..
if self.mobile_filterable:
defaults['filters'] = self.make_mobile_filters()
defaults.update(kwargs)
return defaults
def make_mobile_row_grid_kwargs(self, **kwargs):
"""
Must return a dictionary of kwargs to be passed to the factory when
creating new mobile *row* grid instances.
"""
defaults = {
'model_class': self.model_row_class,
# TODO
'pageable': self.pageable,
'sortable': False,
'filterable': self.mobile_rows_filterable,
'renderers': self.make_mobile_row_grid_renderers(),
'url': lambda obj: self.get_row_action_url('view', obj, mobile=True),
}
# TODO: this seems wrong..
if self.mobile_rows_filterable:
defaults['filters'] = self.make_mobile_row_filters()
defaults.update(kwargs)
return defaults
def make_mobile_grid_renderers(self):
return {
'listitem': self.render_mobile_listitem,
}
def render_mobile_listitem(self, obj, i):
return obj
def make_mobile_row_grid_renderers(self):
return {
'listitem': self.render_mobile_row_listitem,
}
def render_mobile_row_listitem(self, obj, i):
return obj
def grid_extra_class(self, obj, i):
"""
Returns string of extra class(es) for the table row corresponding to
the given object, or ``None``.
"""
def row_grid_extra_class(self, obj, i):
"""
Returns string of extra class(es) for the table row corresponding to
the given row object, or ``None``.
"""
def configure_grid(self, grid):
pass
def configure_row_grid(self, grid):
pass
def configure_mobile_grid(self, grid):
pass
def configure_mobile_row_grid(self, grid):
pass

View file

@ -28,6 +28,7 @@ from __future__ import unicode_literals, absolute_import
import json
import pytz
import six
from rattail import enum
from rattail.db import model
@ -40,16 +41,8 @@ from webhelpers2.html import tags, HTML
from tailbone import forms
from tailbone.db import Session
from tailbone.views import MasterView
class SubjectFieldRenderer(formalchemy.FieldRenderer):
def render_readonly(self, **kwargs):
subject = self.raw_value
if not subject:
return ''
return tags.link_to(subject, self.request.route_url('messages.view', uuid=self.field.parent.model.uuid))
from tailbone.views import MasterView2 as MasterView
from tailbone.util import raw_datetime
class SenderFieldRenderer(forms.renderers.UserFieldRenderer):
@ -106,23 +99,6 @@ class RecipientsFieldRenderer(formalchemy.FieldRenderer):
return ', '.join(recips)
class TerseRecipientsFieldRenderer(formalchemy.FieldRenderer):
def render_readonly(self, **kwargs):
recipients = self.raw_value
if not recipients:
return ''
message = self.field.parent.model
recips = [r for r in recipients if r.recipient is not self.request.user]
recips = sorted([r.recipient.display_name for r in recips])
if len(recips) < len(recipients) and (
message.sender is not self.request.user or not recips):
recips.insert(0, 'you')
if len(recips) < 5:
return ', '.join(recips)
return "{}, ...".format(', '.join(recips[:4]))
class MessagesView(MasterView):
"""
Base class for message views.
@ -133,6 +109,7 @@ class MessagesView(MasterView):
checkboxes = True
replying = False
reply_header_sent_format = '%a %d %b %Y at %I:%M %p'
grid_columns = ['subject', 'sender', 'recipients', 'sent']
def get_index_title(self):
if self.listing:
@ -176,37 +153,49 @@ class MessagesView(MasterView):
.outerjoin(model.MessageRecipient)\
.filter(model.MessageRecipient.recipient == self.request.user)
def _preconfigure_grid(self, g):
def configure_grid(self, g):
g.joiners['sender'] = lambda q: q.join(model.User, model.User.uuid == model.Message.sender_uuid).outerjoin(model.Person)
g.filters['sender'] = g.make_filter('sender', model.Person.display_name,
default_active=True, default_verb='contains')
g.sorters['sender'] = g.make_sorter(model.Person.display_name)
g.sender.set(label="From", renderer=SenderFieldRenderer)
g.filters['subject'].default_active = True
g.filters['subject'].default_verb = 'contains'
g.subject.set(renderer=SubjectFieldRenderer)
g.recipients.set(label="To", renderer=TerseRecipientsFieldRenderer)
g.default_sortkey = 'sent'
g.default_sortdir = 'desc'
def configure_grid(self, g):
g.configure(
include=[
g.subject,
g.sender,
g.recipients,
g.sent,
],
readonly=True)
g.set_renderer('sent', self.render_sent)
g.set_renderer('sender', self.render_sender)
g.set_renderer('recipients', self.render_recipients)
def row_attrs(self, row, i):
recip = self.get_recipient(row)
if recip:
return {'data-uuid': recip.uuid}
return {}
g.set_link('subject')
g.set_label('sender', "From")
g.set_label('recipients', "To")
def render_sent(self, message, column_name):
return raw_datetime(self.rattail_config, message.sent)
def render_sender(self, message, column_name):
sender = message.sender
if sender is self.request.user:
return 'you'
return six.text_type(sender)
def render_recipients(self, message, column_name):
recipients = message.recipients
if recipients:
recips = [r for r in recipients if r.recipient is not self.request.user]
recips = sorted([r.recipient.display_name for r in recips])
if len(recips) < len(recipients) and (
message.sender is not self.request.user or not recips):
recips.insert(0, "you")
if len(recips) < 5:
return ", ".join(recips)
return "{}, ...".format(', '.join(recips[:4]))
return ""
def make_form(self, instance, **kwargs):
form = super(MessagesView, self).make_form(instance, **kwargs)
@ -398,9 +387,11 @@ class MessagesView(MasterView):
if uuids:
new_status = enum.MESSAGE_STATUS_INBOX if dest == 'inbox' else enum.MESSAGE_STATUS_ARCHIVE
for uuid in uuids:
recip = Session.query(model.MessageRecipient).get(uuid) if uuid else None
if recip and recip.recipient is self.request.user:
if recip.status != new_status:
recip = self.Session.query(model.MessageRecipient)\
.filter(model.MessageRecipient.message_uuid == uuid)\
.filter(model.MessageRecipient.recipient_uuid == self.request.user.uuid)\
.first()
if recip and recip.status != new_status:
recip.status = new_status
route = 'messages.{}'.format('archive' if dest == 'inbox' else 'inbox')
return self.redirect(self.request.route_url(route))
@ -480,8 +471,8 @@ class SentView(MessagesView):
return session.query(model.Message)\
.filter(model.Message.sender == self.request.user)
def _preconfigure_grid(self, g):
super(SentView, self)._preconfigure_grid(g)
def configure_grid(self, g):
super(SentView, self).configure_grid(g)
g.filters['sender'].default_active = False
g.joiners['recipients'] = lambda q: q.join(model.MessageRecipient)\
.join(model.User, model.User.uuid == model.MessageRecipient.recipient_uuid)\

View file

@ -33,7 +33,7 @@ from pyramid.httpexceptions import HTTPFound, HTTPNotFound
from webhelpers2.html import HTML, tags
from tailbone import forms
from tailbone.views import MasterView, AutocompleteView
from tailbone.views import MasterView2 as MasterView, AutocompleteView
from rattail.db import model
@ -63,7 +63,17 @@ class PeopleView(MasterView):
route_prefix = 'people'
has_versions = True
def _preconfigure_grid(self, g):
grid_columns = [
'display_name',
'first_name',
'last_name',
'phone',
'email',
]
def configure_grid(self, g):
super(PeopleView, self).configure_grid(g)
g.joiners['email'] = lambda q: q.outerjoin(model.PersonEmailAddress, sa.and_(
model.PersonEmailAddress.parent_uuid == model.Person.uuid,
model.PersonEmailAddress.preference == 1))
@ -71,13 +81,11 @@ class PeopleView(MasterView):
model.PersonPhoneNumber.parent_uuid == model.Person.uuid,
model.PersonPhoneNumber.preference == 1))
g.filters['email'] = g.make_filter('email', model.PersonEmailAddress.address,
label="Email Address")
g.filters['phone'] = g.make_filter('phone', model.PersonPhoneNumber.number,
label="Phone Number")
g.filters['email'] = g.make_filter('email', model.PersonEmailAddress.address)
g.filters['phone'] = g.make_filter('phone', model.PersonPhoneNumber.number)
g.joiners['customer_id'] = lambda q: q.outerjoin(model.CustomerPerson).outerjoin(model.Customer)
g.filters['customer_id'] = g.make_filter('customer_id', model.Customer.id, label="Customer ID")
g.filters['customer_id'] = g.make_filter('customer_id', model.Customer.id)
g.filters['first_name'].default_active = True
g.filters['first_name'].default_verb = 'contains'
@ -90,16 +98,10 @@ class PeopleView(MasterView):
g.default_sortkey = 'display_name'
def configure_grid(self, g):
g.configure(
include=[
g.display_name.label("Full Name"),
g.first_name,
g.last_name,
g.phone.label("Phone Number"),
g.email.label("Email Address"),
],
readonly=True)
g.set_label('display_name', "Full Name")
g.set_label('phone', "Phone Number")
g.set_label('email', "Email Address")
g.set_label('customer_id', "Customer ID")
def get_instance(self):
# TODO: I don't recall why this fallback check for a vendor contact

View file

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
@ -30,7 +30,7 @@ import copy
import wtforms
from tailbone.views import MasterView
from tailbone.views import MasterView2 as MasterView
class PrincipalMasterView(MasterView):

View file

@ -44,11 +44,11 @@ import wtforms
import formalchemy as fa
from pyramid import httpexceptions
from pyramid.renderers import render_to_response
from webhelpers2.html import tags
from webhelpers2.html import tags, HTML
from tailbone import forms, newgrids as grids
from tailbone import forms, grids3 as grids
from tailbone.db import Session
from tailbone.views import MasterView, AutocompleteView
from tailbone.views import MasterView2 as MasterView, AutocompleteView
from tailbone.progress import SessionProgress
@ -71,21 +71,6 @@ from tailbone.progress import SessionProgress
# return query
class DescriptionFieldRenderer(fa.TextFieldRenderer):
"""
Renderer for product descriptions within the grid; adds hyperlink.
"""
def render_readonly(self, **kwargs):
description = self.raw_value
if description is None:
return ''
if kwargs.get('link') and description:
product = self.field.parent.model
description = tags.link_to(description, kwargs['link'](product))
return description
class ProductsView(MasterView):
"""
Master view for the Product class.
@ -93,6 +78,17 @@ class ProductsView(MasterView):
model_class = model.Product
supports_mobile = True
grid_columns = [
'upc',
'brand',
'description',
'size',
'subdepartment',
'vendor',
'regular_price',
'current_price',
]
# child_version_classes = [
# (model.ProductCode, 'product_uuid'),
# (model.ProductCost, 'product_uuid'),
@ -133,7 +129,9 @@ class ProductsView(MasterView):
return query
def _preconfigure_grid(self, g):
def configure_grid(self, g):
super(ProductsView, self).configure_grid(g)
def join_vendor(q):
return q.outerjoin(model.ProductCost,
sa.and_(
@ -185,7 +183,6 @@ class ProductsView(MasterView):
g.filters['upc'].default_active = True
g.filters['upc'].default_verb = 'equal'
g.filters['upc'].label = "UPC"
g.filters['description'].default_active = True
g.filters['description'].default_verb = 'contains'
g.filters['brand'] = g.make_filter('brand', model.Brand.name,
@ -196,51 +193,64 @@ class ProductsView(MasterView):
g.filters['subdepartment'] = g.make_filter('subdepartment', model.Subdepartment.name)
g.filters['report_code'] = g.make_filter('report_code', model.ReportCode.name)
g.filters['code'] = g.make_filter('code', model.ProductCode.code)
g.filters['vendor'] = g.make_filter('vendor', model.Vendor.name, label="Vendor (preferred)")
g.filters['vendor_any'] = g.make_filter('vendor_any', self.VendorAny.name, label="Vendor (any)")
g.filters['vendor'] = g.make_filter('vendor', model.Vendor.name)
g.filters['vendor_any'] = g.make_filter('vendor_any', self.VendorAny.name)
# factory=VendorAnyFilter, joiner=join_vendor_any)
g.filters['vendor_code'] = g.make_filter('vendor_code', ProductCostCode.code)
g.filters['vendor_code_any'] = g.make_filter('vendor_code_any', ProductCostCodeAny.code)
product_link = lambda p: self.get_action_url('view', p)
g.upc.set(label="UPC", renderer=forms.renderers.GPCFieldRenderer)
g.upc.attrs(link=product_link)
g.description.set(renderer=DescriptionFieldRenderer)
g.description.attrs(link=product_link)
g.regular_price.set(label="Reg. Price", renderer=forms.renderers.PriceFieldRenderer)
g.current_price.set(label="Cur. Price", renderer=forms.renderers.PriceFieldRenderer)
g.vendor.set(label="Pref. Vendor")
g.joiners['cost'] = lambda q: q.outerjoin(model.ProductCost,
sa.and_(
model.ProductCost.product_uuid == model.Product.uuid,
model.ProductCost.preference == 1))
g.sorters['cost'] = g.make_sorter(model.ProductCost.unit_cost)
g.filters['cost'] = g.make_filter('cost', model.ProductCost.unit_cost)
g.cost.set(renderer=forms.renderers.CostFieldRenderer)
g.default_sortkey = 'upc'
if self.print_labels and self.request.has_perm('products.print_labels'):
g.more_actions.append(grids.GridAction('print_label', icon='print'))
def configure_grid(self, g):
g.configure(
include=[
g.upc,
g.brand,
g.description,
g.size,
g.subdepartment,
g.vendor,
g.regular_price,
g.current_price,
],
readonly=True)
g.set_type('upc', 'gpc')
g.set_renderer('regular_price', self.render_price)
g.set_renderer('current_price', self.render_price)
g.set_renderer('cost', self.render_cost)
g.set_link('upc')
g.set_link('description')
g.set_label('upc', "UPC")
g.set_label('vendor', "Vendor (preferred)")
g.set_label('vendor_any', "Vendor (any)")
g.set_label('regular_price', "Reg. Price")
g.set_label('current_price', "Cur. Price")
g.set_label('vendor', "Pref. Vendor")
def render_price(self, product, column):
price = product[column]
if price:
if not product.not_for_sale:
if price.price is not None and price.pack_price is not None:
if price.multiple > 1:
return HTML("$ {:0.2f} / {}&nbsp; ($ {:0.2f} / {})".format(
price.price, price.multiple,
price.pack_price, price.pack_multiple))
return HTML("$ {:0.2f}&nbsp; ($ {:0.2f} / {})".format(
price.price, price.pack_price, price.pack_multiple))
if price.price is not None:
if price.multiple > 1:
return "$ {:0.2f} / {}".format(price.price, price.multiple)
return "$ {:0.2f}".format(price.price)
if price.pack_price is not None:
return "$ {:0.2f} / {}".format(price.pack_price, price.pack_multiple)
return ""
def render_cost(self, product, column):
cost = product.cost
if not cost:
return ""
return "'${:0.2f}".format(cost.unit_cost)
def template_kwargs_index(self, **kwargs):
if self.print_labels:
@ -250,19 +260,15 @@ class ProductsView(MasterView):
.all()
return kwargs
def row_attrs(self, row, i):
attrs = {'uuid': row.uuid}
def grid_extra_class(self, product, i):
classes = []
if row.not_for_sale:
if product.not_for_sale:
classes.append('not-for-sale')
if row.deleted:
if product.deleted:
classes.append('deleted')
if classes:
attrs['class_'] = ' '.join(classes)
return attrs
return ' '.join(classes)
def get_instance(self):
key = self.request.matchdict['uuid']

View file

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
@ -29,4 +29,4 @@ from __future__ import unicode_literals, absolute_import
def includeme(config):
config.include('tailbone.views.purchases.core')
config.include('tailbone.views.purchases.batch')
config.include('tailbone.views.purchases.credits')

File diff suppressed because it is too large Load diff

View file

@ -33,7 +33,7 @@ from webhelpers2.html import HTML, tags
from tailbone import forms
from tailbone.db import Session
from tailbone.views import MasterView
from tailbone.views import MasterView2 as MasterView
class BatchesFieldRenderer(fa.FieldRenderer):
@ -73,6 +73,32 @@ class PurchaseView(MasterView):
model_row_class = model.PurchaseItem
row_model_title = 'Purchase Item'
grid_columns = [
'store',
'vendor',
'department',
'buyer',
'date_ordered',
'date_received',
'invoice_number',
'status',
]
row_grid_columns = [
'sequence',
'upc',
'item_id',
'brand_name',
'description',
'size',
'cases_ordered',
'units_ordered',
'cases_received',
'units_received',
'po_total',
'invoice_total',
]
def get_instance_title(self, purchase):
if purchase.status >= self.enum.PURCHASE_STATUS_COSTED:
if purchase.invoice_date:
@ -90,7 +116,9 @@ class PurchaseView(MasterView):
return "{} (ordered)".format(purchase.vendor)
return unicode(purchase)
def _preconfigure_grid(self, g):
def configure_grid(self, g):
super(PurchaseView, self).configure_grid(g)
g.joiners['store'] = lambda q: q.join(model.Store)
g.filters['store'] = g.make_filter('store', model.Store.name)
g.sorters['store'] = g.make_sorter(model.Store.name)
@ -116,24 +144,11 @@ class PurchaseView(MasterView):
g.default_sortkey = 'date_ordered'
g.default_sortdir = 'desc'
g.date_ordered.set(label="Ordered")
g.date_received.set(label="Received")
g.invoice_number.set(label="Invoice No.")
g.status.set(renderer=forms.renderers.EnumFieldRenderer(self.enum.PURCHASE_STATUS))
g.set_enum('status', self.enum.PURCHASE_STATUS)
def configure_grid(self, g):
g.configure(
include=[
g.store,
g.vendor,
g.department,
g.buyer,
g.date_ordered,
g.date_received,
g.invoice_number,
g.status,
],
readonly=True)
g.set_label('date_ordered', "Ordered")
g.set_label('date_received', "Received")
g.set_label('invoice_number', "Invoice No.")
def _preconfigure_fieldset(self, fs):
fs.store.set(renderer=forms.renderers.StoreFieldRenderer)
@ -189,43 +204,36 @@ class PurchaseView(MasterView):
return Session.query(model.PurchaseItem)\
.filter(model.PurchaseItem.purchase == purchase)
def _preconfigure_row_grid(self, g):
g.default_sortkey = 'sequence'
g.sequence.set(label="Seq")
g.upc.set(label="UPC")
g.brand_name.set(label="Brand")
g.cases_ordered.set(label="Cases Ord.", renderer=forms.renderers.QuantityFieldRenderer)
g.units_ordered.set(label="Units Ord.", renderer=forms.renderers.QuantityFieldRenderer)
g.cases_received.set(label="Cases Rec.", renderer=forms.renderers.QuantityFieldRenderer)
g.units_received.set(label="Units Rec.", renderer=forms.renderers.QuantityFieldRenderer)
g.po_total.set(label="Total", renderer=forms.renderers.CurrencyFieldRenderer)
g.invoice_total.set(label="Total", renderer=forms.renderers.CurrencyFieldRenderer)
def configure_row_grid(self, g):
super(PurchaseView, self).configure_row_grid(g)
g.default_sortkey = 'sequence'
g.set_type('cases_ordered', 'quantity')
g.set_type('units_ordered', 'quantity')
g.set_type('cases_received', 'quantity')
g.set_type('units_received', 'quantity')
g.set_type('po_total', 'currency')
g.set_type('invoice_total', 'currency')
g.set_label('sequence', "Seq")
g.set_label('upc', "UPC")
g.set_label('brand_name', "Brand")
g.set_label('cases_ordered', "Cases Ord.")
g.set_label('units_ordered', "Units Ord.")
g.set_label('cases_received', "Cases Rec.")
g.set_label('units_received', "Units Rec.")
g.set_label('po_total', "Total")
g.set_label('invoice_total', "Total")
purchase = self.get_instance()
g.configure(
include=[
g.sequence,
g.upc,
g.item_id,
g.brand_name,
g.description,
g.size,
g.cases_ordered,
g.units_ordered,
g.cases_received,
g.units_received,
g.po_total,
g.invoice_total,
],
readonly=True)
if purchase.status == self.enum.PURCHASE_STATUS_ORDERED:
del g.cases_received
del g.units_received
del g.invoice_total
g.hide_column('cases_received')
g.hide_column('units_received')
g.hide_column('invoice_total')
elif purchase.status in (self.enum.PURCHASE_STATUS_RECEIVED,
self.enum.PURCHASE_STATUS_COSTED):
del g.po_total
g.hide_column('po_total')
def _preconfigure_row_fieldset(self, fs):
fs.vendor_code.set(label="Vendor Item Code")

View file

@ -29,7 +29,7 @@ from __future__ import unicode_literals, absolute_import
from rattail.db import model
from tailbone import forms
from tailbone.views import MasterView
from tailbone.views import MasterView2 as MasterView
class PurchaseCreditView(MasterView):
@ -42,7 +42,21 @@ class PurchaseCreditView(MasterView):
creatable = False
editable = False
def _preconfigure_grid(self, g):
grid_columns = [
'vendor',
'upc',
'brand_name',
'description',
'size',
'cases_shorted',
'units_shorted',
'credit_type',
'date_received',
'status',
]
def configure_grid(self, g):
super(PurchaseCreditView, self).configure_grid(g)
g.joiners['vendor'] = lambda q: q.outerjoin(model.Vendor)
g.sorters['vendor'] = g.make_sorter(model.Vendor.name)
@ -50,28 +64,15 @@ class PurchaseCreditView(MasterView):
g.default_sortkey = 'date_received'
g.default_sortdir = 'desc'
g.upc.set(label="UPC")
g.brand_name.set(label="Brand")
g.cases_shorted.set(label="Cases", renderer=forms.renderers.QuantityFieldRenderer)
g.units_shorted.set(label="Units", renderer=forms.renderers.QuantityFieldRenderer)
g.credit_type.set(label="Type")
g.date_received.set(label="Date")
g.set_type('cases_shorted', 'quantity')
g.set_type('units_shorted', 'quantity')
def configure_grid(self, g):
g.configure(
include=[
g.vendor,
g.upc,
g.brand_name,
g.description,
g.size,
g.cases_shorted,
g.units_shorted,
g.credit_type,
g.date_received,
g.status,
],
readonly=True)
g.set_label('upc', "UPC")
g.set_label('brand_name', "Brand")
g.set_label('cases_shorted', "Cases")
g.set_label('units_shorted', "Units")
g.set_label('credit_type', "Type")
g.set_label('date_received', "Date")
def includeme(config):

View file

@ -36,7 +36,7 @@ import formalchemy as fa
from pyramid import httpexceptions
from tailbone import forms
from tailbone.views.batch import BatchMasterView
from tailbone.views.batch import BatchMasterView2 as BatchMasterView
class PurchasingBatchView(BatchMasterView):
@ -47,6 +47,34 @@ class PurchasingBatchView(BatchMasterView):
model_row_class = model.PurchaseBatchRow
default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler'
grid_columns = [
'id',
'vendor',
'department',
'buyer',
'date_ordered',
'created',
'created_by',
'executed',
]
# row_grid_columns = [
# 'sequence',
# 'upc',
# # 'item_id',
# 'brand_name',
# 'description',
# 'size',
# 'cases_ordered',
# 'units_ordered',
# 'cases_received',
# 'units_received',
# 'po_total',
# 'invoice_total',
# 'credits',
# 'status_code',
# ]
@property
def batch_mode(self):
raise NotImplementedError("Please define `batch_mode` for your purchasing batch view")
@ -55,9 +83,8 @@ class PurchasingBatchView(BatchMasterView):
return session.query(model.PurchaseBatch)\
.filter(model.PurchaseBatch.mode == self.batch_mode)
def _preconfigure_grid(self, g):
super(PurchasingBatchView, self)._preconfigure_grid(g)
del g.filters['mode']
def configure_grid(self, g):
super(PurchasingBatchView, self).configure_grid(g)
g.joiners['vendor'] = lambda q: q.join(model.Vendor)
g.filters['vendor'] = g.make_filter('vendor', model.Vendor.name,
@ -77,22 +104,8 @@ class PurchasingBatchView(BatchMasterView):
g.filters['complete'].default_active = True
g.filters['complete'].default_verb = 'is_true'
g.date_ordered.set(label="Ordered")
g.date_received.set(label="Received")
def configure_grid(self, g):
g.configure(
include=[
g.id,
g.vendor,
g.department,
g.buyer,
g.date_ordered,
g.created,
g.created_by,
g.executed,
],
readonly=True)
g.set_label('date_ordered', "Ordered")
g.set_label('date_received', "Received")
# def make_form(self, batch, **kwargs):
# if self.creating:
@ -299,65 +312,36 @@ class PurchasingBatchView(BatchMasterView):
# query = super(PurchasingBatchView, self).get_row_data(batch)
# return query.options(orm.joinedload(model.PurchaseBatchRow.credits))
def _preconfigure_row_grid(self, g):
super(PurchasingBatchView, self)._preconfigure_row_grid(g)
g.filters['upc'].label = "UPC"
g.filters['brand_name'].label = "Brand"
g.upc.set(label="UPC")
g.brand_name.set(label="Brand")
g.cases_ordered.set(label="Cases Ord.", renderer=forms.renderers.QuantityFieldRenderer)
g.units_ordered.set(label="Units Ord.", renderer=forms.renderers.QuantityFieldRenderer)
g.cases_received.set(label="Cases Rec.", renderer=forms.renderers.QuantityFieldRenderer)
g.units_received.set(label="Units Rec.", renderer=forms.renderers.QuantityFieldRenderer)
g.po_total.set(label="Total", renderer=forms.renderers.CurrencyFieldRenderer)
g.invoice_total.set(label="Total", renderer=forms.renderers.CurrencyFieldRenderer)
g.append(fa.Field('has_credits', type=fa.types.Boolean, label="Credits?",
value=lambda row: bool(row.credits)))
def configure_row_grid(self, g):
batch = self.get_instance()
super(PurchasingBatchView, self).configure_row_grid(g)
g.configure(
include=[
g.sequence,
g.upc,
# g.item_id,
g.brand_name,
g.description,
g.size,
g.cases_ordered,
g.units_ordered,
g.cases_received,
g.units_received,
g.po_total,
g.invoice_total,
g.has_credits,
g.status_code,
],
readonly=True)
g.set_type('upc', 'gpc')
g.set_type('cases_ordered', 'quantity')
g.set_type('units_ordered', 'quantity')
g.set_type('cases_received', 'quantity')
g.set_type('units_received', 'quantity')
g.set_type('po_total', 'currency')
g.set_type('invoice_total', 'currency')
g.set_type('credits', 'boolean')
if batch.mode == self.enum.PURCHASE_BATCH_MODE_ORDERING:
del g.cases_received
del g.units_received
del g.has_credits
del g.invoice_total
elif batch.mode in (self.enum.PURCHASE_BATCH_MODE_RECEIVING,
self.enum.PURCHASE_BATCH_MODE_COSTING):
del g.po_total
g.set_label('upc', "UPC")
g.set_label('brand_name', "Brand")
g.set_label('cases_ordered', "Cases Ord.")
g.set_label('units_ordered', "Units Ord.")
g.set_label('cases_received', "Cases Rec.")
g.set_label('units_received', "Units Rec.")
g.set_label('po_total', "Total")
g.set_label('invoice_total', "Total")
g.set_label('credits', "Credits?")
def make_row_grid_tools(self, batch):
return self.make_default_row_grid_tools(batch)
def row_grid_row_attrs(self, row, i):
attrs = {}
def row_grid_extra_class(self, row, i):
if row.status_code == row.STATUS_PRODUCT_NOT_FOUND:
attrs['class_'] = 'warning'
elif row.status_code in (row.STATUS_INCOMPLETE,
row.STATUS_ORDERED_RECEIVED_DIFFER):
attrs['class_'] = 'notice'
return attrs
return 'warning'
if row.status_code in (row.STATUS_INCOMPLETE, row.STATUS_ORDERED_RECEIVED_DIFFER):
return 'notice'
def _preconfigure_row_fieldset(self, fs):
super(PurchasingBatchView, self)._preconfigure_row_fieldset(fs)

View file

@ -51,6 +51,23 @@ class OrderingBatchView(PurchasingBatchView):
model_title = "Ordering Batch"
model_title_plural = "Ordering Batches"
row_grid_columns = [
'sequence',
'upc',
# 'item_id',
'brand_name',
'description',
'size',
'cases_ordered',
'units_ordered',
# 'cases_received',
# 'units_received',
'po_total',
# 'invoice_total',
# 'credits',
'status_code',
]
order_form_header_columns = [
"UPC",
"Brand",

View file

@ -39,7 +39,7 @@ import formalchemy as fa
import formencode as fe
from webhelpers2.html import tags
from tailbone import forms, newgrids as grids
from tailbone import forms, grids3 as grids
from tailbone.views.purchasing import PurchasingBatchView
@ -93,10 +93,36 @@ class ReceivingBatchView(PurchasingBatchView):
mobile_creatable = True
mobile_rows_filterable = True
row_grid_columns = [
'sequence',
'upc',
# 'item_id',
'brand_name',
'description',
'size',
'cases_ordered',
'units_ordered',
'cases_received',
'units_received',
# 'po_total',
'invoice_total',
'credits',
'status_code',
]
@property
def batch_mode(self):
return self.enum.PURCHASE_BATCH_MODE_RECEIVING
def render_mobile_listitem(self, batch, i):
title = "({}) {} for ${:0,.2f} - {}, {}".format(
batch.id_str,
batch.vendor,
batch.po_total or 0,
batch.department,
batch.created_by)
return title
def make_mobile_row_filters(self):
"""
Returns a set of filters for the mobile row grid.
@ -158,10 +184,6 @@ class ReceivingBatchView(PurchasingBatchView):
kwargs['sms_transaction_number'] = batch.sms_transaction_number
return kwargs
def configure_mobile_grid(self, g):
super(ReceivingBatchView, self).configure_mobile_grid(g)
g.listitem.set(renderer=ReceivingBatchRenderer)
def configure_mobile_fieldset(self, fs):
fs.configure(include=[
fs.vendor.with_renderer(fa.TextFieldRenderer),
@ -178,13 +200,9 @@ class ReceivingBatchView(PurchasingBatchView):
else:
del fs.complete
def render_mobile_row_listitem(self, row, **kwargs):
if row is None:
return ''
def render_mobile_row_listitem(self, row, i):
description = row.product.full_description if row.product else row.description
title = "({}) {}".format(row.upc.pretty(), description)
url = self.request.route_url('mobile.receiving.rows.view', uuid=row.uuid)
return tags.link_to(title, url)
return "({}) {}".format(row.upc.pretty(), description)
# TODO: this view can create new rows, with only a GET query. that should
# probably be changed to require POST; for now we just require the "create
@ -344,20 +362,6 @@ class ReceivingBatchView(PurchasingBatchView):
cls._defaults(config)
class ReceivingBatchRenderer(fa.FieldRenderer):
def render_readonly(self, **kwargs):
batch = self.raw_value
title = "({}) {} for ${:0,.2f} - {}, {}".format(
batch.id_str,
batch.vendor,
batch.po_total or 0,
batch.department,
batch.created_by)
url = self.request.route_url('mobile.receiving.view', uuid=batch.uuid)
return tags.link_to(title, url)
class ValidBatchRow(forms.validators.ModelValidator):
model_class = model.PurchaseBatchRow

View file

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
@ -26,10 +26,10 @@ Report Code Views
from __future__ import unicode_literals, absolute_import
from tailbone.views import MasterView
from rattail.db import model
from tailbone.views import MasterView2 as MasterView
class ReportCodesView(MasterView):
"""
@ -38,16 +38,16 @@ class ReportCodesView(MasterView):
model_class = model.ReportCode
model_title = "Report Code"
grid_columns = [
'code',
'name',
]
def configure_grid(self, g):
super(ReportCodesView, self).configure_grid(g)
g.filters['name'].default_active = True
g.filters['name'].default_verb = 'contains'
g.default_sortkey = 'code'
g.configure(
include=[
g.code,
g.name,
],
readonly=True)
def configure_fieldset(self, fs):
fs.configure(

View file

@ -204,16 +204,13 @@ class ReportOutputView(ExportMasterView):
url_prefix = '/reports/generated'
downloadable = True
def configure_grid(self, g):
g.configure(
include=[
g.id,
g.report_name,
g.filename,
g.created,
g.created_by,
],
readonly=True)
grid_columns = [
'id',
'report_name',
'filename',
'created',
'created_by',
]
def _preconfigure_fieldset(self, fs):
super(ReportOutputView, self)._preconfigure_fieldset(fs)

View file

@ -34,8 +34,9 @@ from rattail.db.auth import has_permission, administrator_role, guest_role, auth
import formalchemy as fa
from formalchemy.fields import IntegerFieldRenderer
from tailbone import forms, newgrids as grids
from tailbone import forms, grids3 as grids
from tailbone.db import Session
from tailbone.newgrids import AlchemyGrid
from tailbone.views.principal import PrincipalMasterView
@ -45,19 +46,17 @@ class RolesView(PrincipalMasterView):
"""
model_class = model.Role
def _preconfigure_grid(self, g):
grid_columns = [
'name',
'session_timeout',
]
def configure_grid(self, g):
super(RolesView, self).configure_grid(g)
g.filters['name'].default_active = True
g.filters['name'].default_verb = 'contains'
g.default_sortkey = 'name'
def configure_grid(self, g):
g.configure(
include=[
g.name,
g.session_timeout,
],
readonly=True)
def _preconfigure_fieldset(self, fs):
fs.append(PermissionsField('permissions'))
permissions = self.request.registry.settings.get('tailbone_permissions', {})
@ -84,7 +83,7 @@ class RolesView(PrincipalMasterView):
# for this one, for instance (no settings required), but there is
# plenty of room for improvement here.
users = sorted(role.users, key=lambda u: u.username)
users = grids.AlchemyGrid('roles.users', self.request, data=users, model_class=model.User,
users = AlchemyGrid('roles.users', self.request, data=users, model_class=model.User,
main_actions=[
grids.GridAction('view', icon='zoomin',
url=lambda r, i: self.request.route_url('users.view', uuid=r.uuid)),

View file

@ -33,7 +33,7 @@ from rattail.db import model
import formalchemy as fa
from tailbone.db import Session
from tailbone.views import MasterView
from tailbone.views import MasterView2 as MasterView
def unique_name(value, field):
@ -48,17 +48,16 @@ class SettingsView(MasterView):
"""
model_class = model.Setting
feedback = re.compile(r'^rattail\.mail\.user_feedback\..*')
grid_columns = [
'name',
'value',
]
def configure_grid(self, g):
g.filters['name'].default_active = True
g.filters['name'].default_verb = 'contains'
g.default_sortkey = 'name'
g.configure(
include=[
g.name,
g.value,
],
readonly=True)
g.set_link('name')
def _preconfigure_fieldset(self, fs):
fs.name.set(validate=unique_name)

View file

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
@ -36,7 +36,7 @@ from rattail.time import localtime
import formalchemy
from tailbone import forms
from tailbone.views import MasterView
from tailbone.views import MasterView2 as MasterView
class ShiftLengthField(formalchemy.Field):
@ -53,6 +53,14 @@ class ShiftLengthField(formalchemy.Field):
return humanize.naturaldelta(shift.end_time - shift.start_time)
def render_shift_length(shift, column):
if not shift.start_time or not shift.end_time:
return ""
if shift.end_time < shift.start_time:
return "??"
return humanize.naturaldelta(shift.end_time - shift.start_time)
class ScheduledShiftsView(MasterView):
"""
Master view for employee scheduled shifts.
@ -60,24 +68,25 @@ class ScheduledShiftsView(MasterView):
model_class = model.ScheduledShift
url_prefix = '/shifts/scheduled'
grid_columns = [
'employee',
'store',
'start_time',
'end_time',
'length',
]
def configure_grid(self, g):
g.joiners['employee'] = lambda q: q.join(model.Employee).join(model.Person)
g.filters['employee'] = g.make_filter('employee', model.Person.display_name,
default_active=True, default_verb='contains',
label="Employee Name")
default_active=True, default_verb='contains')
g.default_sortkey = 'start_time'
g.default_sortdir = 'desc'
g.append(ShiftLengthField('length'))
g.configure(
include=[
g.employee,
g.store,
g.start_time,
g.end_time,
g.length,
],
readonly=True)
g.set_renderer('length', render_shift_length)
g.set_label('employee', "Employee Name")
def configure_fieldset(self, fs):
fs.append(ShiftLengthField('length'))
@ -98,37 +107,37 @@ class WorkedShiftsView(MasterView):
model_class = model.WorkedShift
url_prefix = '/shifts/worked'
def _preconfigure_grid(self, g):
grid_columns = [
'employee',
'store',
'start_time',
'end_time',
'length',
]
def configure_grid(self, g):
super(WorkedShiftsView, self).configure_grid(g)
g.joiners['employee'] = lambda q: q.join(model.Employee).join(model.Person)
g.filters['employee'] = g.make_filter('employee', model.Person.display_name,
label="Employee Name")
g.filters['employee'] = g.make_filter('employee', model.Person.display_name)
g.sorters['employee'] = g.make_sorter(model.Person.display_name)
g.joiners['store'] = lambda q: q.join(model.Store)
g.filters['store'] = g.make_filter('store', model.Store.name, label="Store Name")
g.filters['store'] = g.make_filter('store', model.Store.name)
g.sorters['store'] = g.make_sorter(model.Store.name)
g.filters['punch_in'].label = "Start Time"
g.filters['punch_out'].label = "End Time"
# TODO: these sorters should be automatic once we fix the schema
g.sorters['start_time'] = g.make_sorter(model.WorkedShift.punch_in)
g.sorters['end_time'] = g.make_sorter(model.WorkedShift.punch_out)
g.default_sortkey = 'start_time'
g.default_sortdir = 'desc'
g.append(ShiftLengthField('length'))
g.set_renderer('length', render_shift_length)
def configure_grid(self, g):
g.configure(
include=[
g.employee,
g.store,
g.start_time,
g.end_time,
g.length,
],
readonly=True)
g.set_label('employee', "Employee Name")
g.set_label('store', "Store Name")
g.set_label('punch_in', "Start Time")
g.set_label('punch_out', "End Time")
def get_instance_title(self, shift):
time = shift.start_time or shift.end_time

View file

@ -30,7 +30,7 @@ import sqlalchemy as sa
from rattail.db import model
from tailbone.views import MasterView
from tailbone.views import MasterView2 as MasterView
class StoresView(MasterView):
@ -39,6 +39,12 @@ class StoresView(MasterView):
"""
model_class = model.Store
has_versions = True
grid_columns = [
'id',
'name',
'phone',
'email',
]
def configure_grid(self, g):
@ -49,28 +55,21 @@ class StoresView(MasterView):
model.StorePhoneNumber.parent_uuid == model.Store.uuid,
model.StorePhoneNumber.preference == 1))
g.filters['email'] = g.make_filter('email', model.StoreEmailAddress.address,
label="Email Address")
g.filters['phone'] = g.make_filter('phone', model.StorePhoneNumber.number,
label="Phone Number")
g.filters['phone'] = g.make_filter('phone', model.StorePhoneNumber.number)
g.filters['email'] = g.make_filter('email', model.StoreEmailAddress.address)
g.filters['name'].default_active = True
g.filters['name'].default_verb = 'contains'
g.filters['id'].label = "ID"
g.sorters['email'] = lambda q, d: q.order_by(getattr(model.StoreEmailAddress.address, d)())
g.sorters['phone'] = lambda q, d: q.order_by(getattr(model.StorePhoneNumber.number, d)())
g.sorters['phone'] = g.make_sorter(model.StorePhoneNumber.number)
g.sorters['email'] = g.make_sorter(model.StoreEmailAddress.address)
g.default_sortkey = 'id'
g.configure(
include=[
g.id.label("ID"),
g.name,
g.phone.label("Phone Number"),
g.email.label("Email Address"),
],
readonly=True)
g.set_link('id')
g.set_link('name')
g.set_label('id', "ID")
g.set_label('phone', "Phone Number")
g.set_label('email', "Email Address")
def configure_fieldset(self, fs):
fs.configure(

View file

@ -29,7 +29,7 @@ from __future__ import unicode_literals, absolute_import
from rattail.db import model
from tailbone.db import Session
from tailbone.views import MasterView
from tailbone.views import MasterView2 as MasterView
class SubdepartmentsView(MasterView):
@ -37,6 +37,13 @@ class SubdepartmentsView(MasterView):
Master view for the Subdepartment class.
"""
model_class = model.Subdepartment
grid_columns = [
'number',
'name',
'department',
]
mergeable = True
merge_additive_fields = [
'product_count',
@ -49,16 +56,10 @@ class SubdepartmentsView(MasterView):
]
def configure_grid(self, g):
super(SubdepartmentsView, self).configure_grid(g)
g.filters['name'].default_active = True
g.filters['name'].default_verb = 'contains'
g.default_sortkey = 'name'
g.configure(
include=[
g.number,
g.name,
g.department,
],
readonly=True)
def configure_fieldset(self, fs):
fs.configure(

View file

@ -26,8 +26,7 @@ Views with info about the underlying Rattail tables
from __future__ import unicode_literals, absolute_import
from tailbone import newgrids as grids
from tailbone.views import MasterView
from tailbone.views import MasterView2 as MasterView
class TablesView(MasterView):
@ -41,10 +40,14 @@ class TablesView(MasterView):
editable = False
deletable = False
viewable = False
grid_factory = grids.Grid
filterable = False
pageable = False
grid_columns = [
'name',
'row_count',
]
def get_data(self, **kwargs):
"""
Fetch existing table names and estimate row counts via PG SQL
@ -58,13 +61,8 @@ class TablesView(MasterView):
return [dict(name=row[1], row_count=row[2]) for row in result]
def configure_grid(self, g):
g.columns = [
grids.GridColumn('name'),
grids.GridColumn('row_count'),
]
g.sorters['name'] = g.make_sorter('name', foldcase=True)
g.sorters['row_count'] = g.make_sorter('row_count')
g.sorters['name'] = g.make_simple_sorter('name', foldcase=True)
g.sorters['row_count'] = g.make_simple_sorter('row_count')
g.default_sortkey = 'name'

View file

@ -69,25 +69,27 @@ class TempmonClientView(MasterView):
route_prefix = 'tempmon.clients'
url_prefix = '/tempmon/clients'
def _preconfigure_grid(self, g):
grid_columns = [
'config_key',
'hostname',
'location',
'delay',
'enabled',
'online',
]
def configure_grid(self, g):
super(TempmonClientView, self).configure_grid(g)
g.filters['hostname'].default_active = True
g.filters['hostname'].default_verb = 'contains'
g.filters['location'].default_active = True
g.filters['location'].default_verb = 'contains'
g.default_sortkey = 'config_key'
g.config_key.set(label="Key")
def configure_grid(self, g):
g.configure(
include=[
g.config_key,
g.hostname,
g.location,
g.delay,
g.enabled,
g.online,
],
readonly=True)
g.set_type('enabled', 'boolean')
g.set_type('online', 'boolean')
g.set_label('config_key', "Key")
def _preconfigure_fieldset(self, fs):
fs.config_key.set(validate=unique_config_key)

View file

@ -33,7 +33,7 @@ from tailbone import views
from tailbone.db import TempmonSession
class MasterView(views.MasterView):
class MasterView(views.MasterView2):
"""
Base class for tempmon views.
"""

View file

@ -55,26 +55,29 @@ class TempmonProbeView(MasterView):
route_prefix = 'tempmon.probes'
url_prefix = '/tempmon/probes'
def _preconfigure_grid(self, g):
grid_columns = [
'client',
'config_key',
'appliance_type',
'description',
'device_path',
'enabled',
'status',
]
def configure_grid(self, g):
super(TempmonProbeView, self).configure_grid(g)
g.joiners['client'] = lambda q: q.join(tempmon.Client)
g.sorters['client'] = g.make_sorter(tempmon.Client.config_key)
g.default_sortkey = 'client'
g.config_key.set(label="Key")
g.appliance_type.set(renderer=forms.renderers.EnumFieldRenderer(self.enum.TEMPMON_APPLIANCE_TYPE))
g.status.set(renderer=forms.renderers.EnumFieldRenderer(self.enum.TEMPMON_PROBE_STATUS))
def configure_grid(self, g):
g.configure(
include=[
g.client,
g.config_key,
g.appliance_type,
g.description,
g.device_path,
g.enabled,
g.status,
],
readonly=True)
g.set_enum('appliance_type', self.enum.TEMPMON_APPLIANCE_TYPE)
g.set_enum('status', self.enum.TEMPMON_PROBE_STATUS)
g.set_type('enabled', 'boolean')
g.set_label('config_key', "Key")
def _preconfigure_fieldset(self, fs):
fs.config_key.set(validate=unique_config_key)

View file

@ -47,39 +47,45 @@ class TempmonReadingView(MasterView):
creatable = False
editable = False
grid_columns = [
'client_key',
'client_host',
'probe',
'taken',
'degrees_f',
]
def query(self, session):
return session.query(tempmon.Reading)\
.join(tempmon.Client)\
.options(orm.joinedload(tempmon.Reading.client))
def _preconfigure_grid(self, g):
def configure_grid(self, g):
super(TempmonReadingView, self).configure_grid(g)
g.append(fa.Field('client_key', value=lambda r: r.client.config_key))
g.sorters['client_key'] = g.make_sorter(tempmon.Client.config_key)
g.filters['client_key'] = g.make_filter('client_key', tempmon.Client.config_key)
g.append(fa.Field('client_host', value=lambda r: r.client.hostname))
g.sorters['client_host'] = g.make_sorter(tempmon.Client.hostname)
g.filters['client_host'] = g.make_filter('client_host', tempmon.Client.hostname)
g.joiners['probe'] = lambda q: q.join(tempmon.Probe,
tempmon.Probe.uuid == tempmon.Reading.probe_uuid)
g.joiners['probe'] = lambda q: q.join(tempmon.Probe, tempmon.Probe.uuid == tempmon.Reading.probe_uuid)
g.sorters['probe'] = g.make_sorter(tempmon.Probe.description)
g.filters['probe'] = g.make_filter('probe', tempmon.Probe.description)
g.default_sortkey = 'taken'
g.default_sortdir = 'desc'
def configure_grid(self, g):
g.configure(
include=[
g.client_key,
g.client_host,
g.probe,
g.taken,
g.degrees_f,
],
readonly=True)
g.set_type('taken', 'datetime')
g.set_renderer('client_key', self.render_client_key)
g.set_renderer('client_host', self.render_client_host)
def render_client_key(self, reading, column):
return reading.client.config_key
def render_client_host(self, reading, column):
return reading.client.hostname
def _preconfigure_fieldset(self, fs):
fs.client.set(label="TempMon Client", renderer=ClientFieldRenderer)

View file

@ -32,7 +32,7 @@ from rattail.time import localtime
from tailbone import forms
from tailbone.db import TrainwreckSession
from tailbone.views import MasterView
from tailbone.views import MasterView2 as MasterView
class TransactionView(MasterView):
@ -49,11 +49,35 @@ class TransactionView(MasterView):
editable = False
deletable = False
grid_columns = [
'start_time',
'system',
'terminal_id',
'receipt_number',
'customer_id',
'customer_name',
'total',
]
has_rows = True
# model_row_class = trainwreck.TransactionItem
rows_default_pagesize = 100
def _preconfigure_grid(self, g):
row_grid_columns = [
'sequence',
'item_type',
'item_id',
'department_number',
'description',
'unit_quantity',
'subtotal',
'tax',
'total',
'void',
]
def configure_grid(self, g):
super(TransactionView, self).configure_grid(g)
g.filters['receipt_number'].default_active = True
g.filters['receipt_number'].default_verb = 'equal'
g.filters['start_time'].default_active = True
@ -62,24 +86,11 @@ class TransactionView(MasterView):
g.default_sortkey = 'start_time'
g.default_sortdir = 'desc'
g.system.set(renderer=forms.renderers.EnumFieldRenderer(self.enum.TRAINWRECK_SYSTEM))
g.terminal_id.set(label="Terminal")
g.receipt_number.set(label="Receipt No.")
g.customer_id.set(label="Customer ID")
g.total.set(renderer=forms.renderers.CurrencyFieldRenderer)
def configure_grid(self, g):
g.configure(
include=[
g.start_time,
g.system,
g.terminal_id,
g.receipt_number,
g.customer_id,
g.customer_name,
g.total,
],
readonly=True)
g.set_enum('system', self.enum.TRAINWRECK_SYSTEM)
g.set_type('total', 'currency')
g.set_label('terminal_id', "Terminal")
g.set_label('receipt_number', "Receipt No.")
g.set_label('customer_id', "Customer ID")
def _preconfigure_fieldset(self, fs):
fs.system.set(renderer=forms.renderers.EnumFieldRenderer(self.enum.TRAINWRECK_SYSTEM))
@ -117,32 +128,18 @@ class TransactionView(MasterView):
def get_parent(self, item):
return item.transaction
def _preconfigure_row_grid(self, g):
def configure_row_grid(self, g):
super(TransactionView, self).configure_row_grid(g)
g.default_sortkey = 'sequence'
g.item_id.set(label="Item ID")
g.department_number.set(label="Dept. No.")
g.unit_quantity.set(renderer=forms.renderers.QuantityFieldRenderer)
g.subtotal.set(renderer=forms.renderers.CurrencyFieldRenderer)
g.discounted_subtotal.set(renderer=forms.renderers.CurrencyFieldRenderer)
g.tax.set(renderer=forms.renderers.CurrencyFieldRenderer)
g.total.set(renderer=forms.renderers.CurrencyFieldRenderer)
g.set_type('unit_quantity', 'quantity')
g.set_type('subtotal', 'currency')
g.set_type('discounted_subtotal', 'currency')
g.set_type('tax', 'currency')
g.set_type('total', 'currency')
def configure_row_grid(self, g):
g.configure(
include=[
g.sequence,
g.item_type,
g.item_id,
g.department_number,
g.description,
g.unit_quantity,
g.subtotal,
g.tax,
g.total,
g.void,
],
readonly=True)
g.set_label('item_id', "Item ID")
g.set_label('department_number', "Dept. No.")
def _preconfigure_row_fieldset(self, fs):
fs.item_id.set(label="Item ID")

View file

@ -142,11 +142,18 @@ class UsersView(PrincipalMasterView):
'active',
]
grid_columns = [
'username',
'person',
]
def query(self, session):
return session.query(model.User)\
.options(orm.joinedload(model.User.person))
def configure_grid(self, g):
super(UsersView, self).configure_grid(g)
g.joiners['person'] = lambda q: q.outerjoin(model.Person)
del g.filters['password']
@ -155,7 +162,7 @@ class UsersView(PrincipalMasterView):
g.filters['username'].default_verb = 'contains'
g.filters['active'].default_active = True
g.filters['active'].default_verb = 'is_true'
g.filters['person'] = g.make_filter('person', model.Person.display_name, label="Person's Name",
g.filters['person'] = g.make_filter('person', model.Person.display_name,
default_active=True, default_verb='contains')
g.filters['password'] = g.make_filter('password', model.User.password,
verbs=['is_null', 'is_not_null'])
@ -163,13 +170,7 @@ class UsersView(PrincipalMasterView):
g.sorters['person'] = lambda q, d: q.order_by(getattr(model.Person.display_name, d)())
g.default_sortkey = 'username'
g.person.set(label="Person's Name")
g.configure(
include=[
g.username,
g.person,
],
readonly=True)
g.set_label('person', "Person's Name")
def _preconfigure_fieldset(self, fs):
fs.username.set(renderer=forms.renderers.StrippedTextFieldRenderer, validate=unique_username)

View file

@ -35,7 +35,7 @@ import formalchemy
from tailbone import forms
from tailbone.db import Session
from tailbone.views.batch import FileBatchMasterView
from tailbone.views.batch import FileBatchMasterView2 as FileBatchMasterView
log = logging.getLogger(__name__)
@ -52,28 +52,40 @@ class VendorCatalogsView(FileBatchMasterView):
editable = False
rows_bulk_deletable = True
grid_columns = [
'created',
'created_by',
'vendor',
'effective',
'filename',
'executed',
]
row_grid_columns = [
'sequence',
'upc',
'brand_name',
'description',
'size',
'vendor_code',
'old_unit_cost',
'unit_cost',
'unit_cost_diff',
'status_code',
]
def get_parsers(self):
if not hasattr(self, 'parsers'):
self.parsers = sorted(iter_catalog_parsers(), key=lambda p: p.display)
return self.parsers
def configure_grid(self, g):
super(VendorCatalogsView, self).configure_grid(g)
g.joiners['vendor'] = lambda q: q.join(model.Vendor)
g.filters['vendor'] = g.make_filter('vendor', model.Vendor.name,
default_active=True, default_verb='contains')
g.sorters['vendor'] = g.make_sorter(model.Vendor.name)
g.configure(
include=[
g.created,
g.created_by,
g.vendor,
g.effective,
g.filename,
g.executed,
],
readonly=True)
def get_instance_title(self, batch):
return unicode(batch.vendor)
@ -115,28 +127,18 @@ class VendorCatalogsView(FileBatchMasterView):
return kwargs
def configure_row_grid(self, g):
g.configure(
include=[
g.sequence,
g.upc.label("UPC"),
g.brand_name.label("Brand"),
g.description,
g.size,
g.vendor_code,
g.old_unit_cost.label("Old Cost"),
g.unit_cost.label("New Cost"),
g.unit_cost_diff.label("Diff."),
g.status_code,
],
readonly=True)
super(VendorCatalogsView, self).configure_row_grid(g)
g.set_label('upc', "UPC")
g.set_label('brand_name', "Brand")
g.set_label('old_unit_cost', "Old Cost")
g.set_label('unit_cost', "New Cost")
g.set_label('unit_cost_diff', "Diff.")
def row_grid_row_attrs(self, row, i):
attrs = {}
if row.status_code in (row.STATUS_NEW_COST, row.STATUS_UPDATE_COST):
attrs['class_'] = 'notice'
def row_grid_extra_class(self, row, i):
if row.status_code == row.STATUS_PRODUCT_NOT_FOUND:
attrs['class_'] = 'warning'
return attrs
return 'warning'
if row.status_code in (row.STATUS_NEW_COST, row.STATUS_UPDATE_COST):
return 'notice'
def template_kwargs_create(self, **kwargs):
parsers = self.get_parsers()

View file

@ -30,7 +30,7 @@ from rattail.db import model
from tailbone import forms
from tailbone.db import Session
from tailbone.views import MasterView, AutocompleteView
from tailbone.views import MasterView2 as MasterView, AutocompleteView
class VendorsView(MasterView):
@ -40,22 +40,24 @@ class VendorsView(MasterView):
model_class = model.Vendor
has_versions = True
grid_columns = [
'id',
'name',
'phone',
'email',
'contact',
]
def configure_grid(self, g):
super(VendorsView, self).configure_grid(g)
g.filters['name'].default_active = True
g.filters['name'].default_verb = 'contains'
g.filters['id'].label = "ID"
g.default_sortkey = 'name'
g.append(forms.AssociationProxyField('contact'))
g.configure(
include=[
g.id.label("ID"),
g.name,
g.phone.label("Phone Number"),
g.email.label("Email Address"),
g.contact,
],
readonly=True)
g.set_label('id', "ID")
g.set_label('phone', "Phone Number")
g.set_label('email', "Email Address")
def configure_fieldset(self, fs):
fs.append(forms.AssociationProxyField('contact'))

View file

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
@ -32,7 +32,7 @@ from rattail.vendors.invoices import iter_invoice_parsers, require_invoice_parse
import formalchemy
from tailbone.db import Session
from tailbone.views.batch import FileBatchMasterView
from tailbone.views.batch import FileBatchMasterView2 as FileBatchMasterView
class VendorInvoicesView(FileBatchMasterView):
@ -44,23 +44,36 @@ class VendorInvoicesView(FileBatchMasterView):
default_handler_spec = 'rattail.batch.vendorinvoice:VendorInvoiceHandler'
url_prefix = '/vendors/invoices'
grid_columns = [
'created',
'created_by',
'vendor',
'filename',
'executed',
]
row_grid_columns = [
'sequence',
'upc',
'brand_name',
'description',
'size',
'vendor_code',
'shipped_cases',
'shipped_units',
'unit_cost',
'status_code',
]
def get_instance_title(self, batch):
return unicode(batch.vendor)
def configure_grid(self, g):
super(VendorInvoicesView, self).configure_grid(g)
g.joiners['vendor'] = lambda q: q.join(model.Vendor)
g.filters['vendor'] = g.make_filter('vendor', model.Vendor.name,
default_active=True, default_verb='contains')
g.sorters['vendor'] = g.make_sorter(model.Vendor.name)
g.configure(
include=[
g.created,
g.created_by,
g.vendor,
g.filename,
g.executed,
],
readonly=True)
def configure_fieldset(self, fs):
fs.purchase_order_number.set(label=self.handler.po_number_title)
@ -123,34 +136,21 @@ class VendorInvoicesView(FileBatchMasterView):
return True
def configure_row_grid(self, g):
g.filters['upc'].label = "UPC"
g.filters['brand_name'].label = "Brand"
g.configure(
include=[
g.sequence,
g.upc.label("UPC"),
g.brand_name.label("Brand"),
g.description,
g.size,
g.vendor_code,
g.shipped_cases.label("Cases"),
g.shipped_units.label("Units"),
g.unit_cost,
g.status_code,
],
readonly=True)
super(VendorInvoicesView, self).configure_row_grid(g)
g.set_label('upc', "UPC")
g.set_label('brand_name', "Brand")
g.set_label('shipped_cases', "Cases")
g.set_label('shipped_units', "Units")
def row_grid_row_attrs(self, row, i):
attrs = {}
if row.status_code in (row.STATUS_NOT_IN_PURCHASE,
row.STATUS_NOT_IN_INVOICE,
row.STATUS_DIFFERS_FROM_PURCHASE):
attrs['class_'] = 'notice'
def row_grid_extra_class(self, row, i):
if row.status_code in (row.STATUS_NOT_IN_DB,
row.STATUS_COST_NOT_IN_DB,
row.STATUS_NO_CASE_QUANTITY):
attrs['class_'] = 'warning'
return attrs
return 'warning'
if row.status_code in (row.STATUS_NOT_IN_PURCHASE,
row.STATUS_NOT_IN_INVOICE,
row.STATUS_DIFFERS_FROM_PURCHASE):
return 'notice'
def includeme(config):