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:
parent
f244c2934b
commit
5b1ae27a10
1
setup.py
1
setup.py
|
@ -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
|
||||
]
|
||||
|
|
33
tailbone/grids3/__init__.py
Normal file
33
tailbone/grids3/__init__.py
Normal 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
919
tailbone/grids3/core.py
Normal 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
53
tailbone/grids3/mobile.py
Normal 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)
|
|
@ -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)
|
||||
|
|
71
tailbone/static/css/grids3.css
Normal file
71
tailbone/static/css/grids3.css
Normal 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;
|
||||
}
|
46
tailbone/static/js/jquery.ui.tailbone.js
vendored
46
tailbone/static/js/jquery.ui.tailbone.js
vendored
|
@ -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() {
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()">
|
||||
|
|
|
@ -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() {
|
||||
|
||||
|
|
21
tailbone/templates/grids3/grid.mako
Normal file
21
tailbone/templates/grids3/grid.mako
Normal 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
|
||||
${grid.pager.pager('$link_first $link_previous ~1~ $link_next $link_last', symbol_next='next', symbol_previous='prev')|n}
|
||||
</p>
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
55
tailbone/templates/master2/index.mako
Normal file
55
tailbone/templates/master2/index.mako
Normal 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 -->
|
||||
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()">
|
||||
|
|
7
tailbone/templates/mobile/newgrids/complete.mako
Normal file
7
tailbone/templates/mobile/newgrids/complete.mako
Normal file
|
@ -0,0 +1,7 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
|
||||
% if grid.filterable:
|
||||
${grid.render_filters()|n}
|
||||
% endif
|
||||
|
||||
${grid.render_grid()|n}
|
|
@ -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">
|
|
@ -1,3 +1,4 @@
|
|||
## -*- coding: utf-8 -*-
|
||||
<%inherit file="/master/index.mako" />
|
||||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/master2/index.mako" />
|
||||
|
||||
${parent.body()}
|
||||
|
|
|
@ -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()}
|
||||
|
|
|
@ -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">
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -27,3 +27,4 @@ Views for batches
|
|||
from __future__ import unicode_literals, absolute_import
|
||||
|
||||
from .core import BatchMasterView, FileBatchMasterView
|
||||
from .core2 import BatchMasterView2, FileBatchMasterView2
|
||||
|
|
126
tailbone/views/batch/core2.py
Normal file
126
tailbone/views/batch/core2.py
Normal 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
|
||||
"""
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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)),
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
337
tailbone/views/master2.py
Normal 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
|
|
@ -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)\
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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} / {} ($ {:0.2f} / {})".format(
|
||||
price.price, price.multiple,
|
||||
price.pack_price, price.pack_multiple))
|
||||
return HTML("$ {:0.2f} ($ {: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']
|
||||
|
|
|
@ -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
|
@ -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")
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
66
tailbone/views/vendors/catalogs.py
vendored
66
tailbone/views/vendors/catalogs.py
vendored
|
@ -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()
|
||||
|
|
26
tailbone/views/vendors/core.py
vendored
26
tailbone/views/vendors/core.py
vendored
|
@ -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'))
|
||||
|
|
70
tailbone/views/vendors/invoices.py
vendored
70
tailbone/views/vendors/invoices.py
vendored
|
@ -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):
|
||||
|
|
Loading…
Reference in a new issue