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
|
'transaction', # 1.2.0
|
||||||
'waitress', # 0.8.1
|
'waitress', # 0.8.1
|
||||||
'WebHelpers2', # 2.0
|
'WebHelpers2', # 2.0
|
||||||
|
'webhelpers2_grid', # 0.1
|
||||||
'WTForms', # 2.1
|
'WTForms', # 2.1
|
||||||
'zope.sqlalchemy', # 0.7
|
'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}
|
kwargs = {'c': column.label}
|
||||||
return HTML.tag('th', **kwargs)
|
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 = kwargs
|
||||||
context['request'] = self.request
|
context['request'] = self.request
|
||||||
context['grid'] = self
|
context['grid'] = self
|
||||||
return render(template, context)
|
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 = kwargs
|
||||||
context['grid'] = self
|
context['grid'] = self
|
||||||
return render(template, context)
|
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.
|
// 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() {
|
this.element.on('click', 'thead th.sortable a', function() {
|
||||||
var th = $(this).parent();
|
var th = $(this).parent();
|
||||||
var data = {
|
var data = {
|
||||||
|
@ -128,6 +141,7 @@
|
||||||
that.refresh(data);
|
that.refresh(data);
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Refresh data when user chooses a new page size setting.
|
// Refresh data when user chooses a new page size setting.
|
||||||
this.element.on('change', '.pager #pagesize', function() {
|
this.element.on('change', '.pager #pagesize', function() {
|
||||||
|
@ -145,15 +159,39 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add hover highlight effect to grid rows during mouse-over.
|
// 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).addClass('hovering');
|
||||||
});
|
});
|
||||||
this.element.on('mouseleave', 'tbody tr', function() {
|
this.element.on('mouseleave', 'tbody tr:not(.header)', function() {
|
||||||
$(this).removeClass('hovering');
|
$(this).removeClass('hovering');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Do some extra stuff for grids with checkboxes.
|
// do some extra stuff for grids with checkboxes
|
||||||
if (this.grid.hasClass('selectable')) {
|
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.
|
// (Un-)Check all rows when clicking check-all box in header.
|
||||||
this.element.on('click', 'thead th.checkbox input', function() {
|
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() {
|
$('body').on('click', 'div.dialog button.close', function() {
|
||||||
var dialog = $(this).parents('div.dialog:first');
|
var dialog = $(this).parents('div.dialog:first');
|
||||||
dialog.dialog('close');
|
dialog.dialog('close');
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8; -*-
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
|
@ -26,6 +26,8 @@ Event Subscribers
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
import rattail
|
import rattail
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
from rattail.db.auth import has_permission
|
from rattail.db.auth import has_permission
|
||||||
|
@ -79,6 +81,7 @@ def before_render(event):
|
||||||
renderer_globals['rattail'] = rattail
|
renderer_globals['rattail'] = rattail
|
||||||
renderer_globals['tailbone'] = tailbone
|
renderer_globals['tailbone'] = tailbone
|
||||||
renderer_globals['enum'] = request.rattail_config.get_enum()
|
renderer_globals['enum'] = request.rattail_config.get_enum()
|
||||||
|
renderer_globals['six'] = six
|
||||||
|
|
||||||
|
|
||||||
def add_inbox_count(event):
|
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/filters.css'))}
|
||||||
${h.stylesheet_link(request.static_url('tailbone:static/css/forms.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/newgrids.css'))}
|
||||||
|
${h.stylesheet_link(request.static_url('tailbone:static/css/grids3.css'))}
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="jquery_smoothness_theme()">
|
<%def name="jquery_smoothness_theme()">
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
## -*- coding: utf-8 -*-
|
## -*- coding: utf-8; -*-
|
||||||
<%inherit file="/master/index.mako" />
|
<%inherit file="/master2/index.mako" />
|
||||||
|
|
||||||
<%def name="head_tags()">
|
<%def name="extra_javascript()">
|
||||||
${parent.head_tags()}
|
${parent.extra_javascript()}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$(function() {
|
$(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" />
|
<%inherit file="/messages/index.mako" />
|
||||||
|
|
||||||
<%def name="title()">Message Archive</%def>
|
<%def name="title()">Message Archive</%def>
|
||||||
|
|
||||||
<%def name="head_tags()">
|
<%def name="extra_javascript()">
|
||||||
${parent.head_tags()}
|
${parent.extra_javascript()}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
destination = "Inbox";
|
destination = "Inbox";
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
|
|
||||||
<%def name="title()">Message Inbox</%def>
|
<%def name="title()">Message Inbox</%def>
|
||||||
|
|
||||||
<%def name="head_tags()">
|
<%def name="extra_javascript()">
|
||||||
${parent.head_tags()}
|
${parent.extra_javascript()}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
destination = "Archive";
|
destination = "Archive";
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
## -*- coding: utf-8 -*-
|
## -*- coding: utf-8; -*-
|
||||||
<%inherit file="/master/index.mako" />
|
<%inherit file="/master2/index.mako" />
|
||||||
|
|
||||||
<%def name="head_tags()">
|
<%def name="extra_javascript()">
|
||||||
${parent.head_tags()}
|
${parent.extra_javascript()}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
|
||||||
var destination = null;
|
var destination = null;
|
||||||
|
|
||||||
function update_move_button() {
|
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')
|
$('form[name="move-selected"] button')
|
||||||
.button('option', 'label', "Move " + count + " selected to " + destination)
|
.button('option', 'label', "Move " + count + " selected to " + destination)
|
||||||
.button('option', 'disabled', count < 1);
|
.button('option', 'disabled', count < 1);
|
||||||
|
@ -18,17 +18,17 @@
|
||||||
|
|
||||||
update_move_button();
|
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();
|
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();
|
update_move_button();
|
||||||
});
|
});
|
||||||
|
|
||||||
$('form[name="move-selected"]').submit(function() {
|
$('form[name="move-selected"]').submit(function() {
|
||||||
var uuids = [];
|
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'));
|
uuids.push($(this).parents('tr:first').data('uuid'));
|
||||||
});
|
});
|
||||||
if (! uuids.length) {
|
if (! uuids.length) {
|
||||||
|
|
|
@ -1,8 +1,30 @@
|
||||||
## -*- coding: utf-8 -*-
|
## -*- coding: utf-8 -*-
|
||||||
<%inherit file="/master/view.mako" />
|
<%inherit file="/master/view.mako" />
|
||||||
|
|
||||||
<%def name="head_tags()">
|
<%def name="extra_javascript()">
|
||||||
${parent.head_tags()}
|
${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">
|
<style type="text/css">
|
||||||
.field-wrapper.recipients .everyone {
|
.field-wrapper.recipients .everyone {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -22,24 +44,6 @@
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
</style>
|
</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>
|
||||||
|
|
||||||
<%def name="context_menu_items()">
|
<%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; -*-
|
## -*- coding: utf-8; -*-
|
||||||
|
|
||||||
% if grid.filterable:
|
|
||||||
${grid.render_filters()|n}
|
|
||||||
% endif
|
|
||||||
|
|
||||||
<ul data-role="listview">
|
<ul data-role="listview">
|
||||||
% for obj in grid.iter_rows():
|
${grid.make_webhelpers_grid()}
|
||||||
<li>${grid.listitem.render_readonly()}</li>
|
|
||||||
% endfor
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
## <table data-role="table" class="ui-responsive table-stroke">
|
## <table data-role="table" class="ui-responsive table-stroke">
|
|
@ -1,3 +1,4 @@
|
||||||
## -*- coding: utf-8 -*-
|
## -*- coding: utf-8; -*-
|
||||||
<%inherit file="/master/index.mako" />
|
<%inherit file="/master2/index.mako" />
|
||||||
|
|
||||||
${parent.body()}
|
${parent.body()}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
## -*- coding: utf-8 -*-
|
## -*- coding: utf-8; -*-
|
||||||
<%inherit file="/master/index.mako" />
|
<%inherit file="/master2/index.mako" />
|
||||||
|
|
||||||
<%def name="context_menu_items()">
|
<%def name="context_menu_items()">
|
||||||
${parent.context_menu_items()}
|
${parent.context_menu_items()}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
## -*- coding: utf-8 -*-
|
## -*- coding: utf-8 -*-
|
||||||
<%inherit file="/master/index.mako" />
|
<%inherit file="/master2/index.mako" />
|
||||||
|
|
||||||
<%def name="head_tags()">
|
<%def name="extra_styles()">
|
||||||
${parent.head_tags()}
|
${parent.extra_styles()}
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
|
|
||||||
table.label-printing th {
|
table.label-printing th {
|
||||||
|
@ -32,6 +32,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="extra_javascript()">
|
||||||
|
${parent.extra_javascript()}
|
||||||
% if label_profiles and request.has_perm('products.print_labels'):
|
% if label_profiles and request.has_perm('products.print_labels'):
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,7 @@ from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
from .core import View
|
from .core import View
|
||||||
from .master import MasterView
|
from .master import MasterView
|
||||||
|
from .master2 import MasterView2
|
||||||
|
|
||||||
# TODO: deprecate / remove some of this
|
# TODO: deprecate / remove some of this
|
||||||
from .autocomplete import AutocompleteView
|
from .autocomplete import AutocompleteView
|
||||||
|
|
|
@ -27,3 +27,4 @@ Views for batches
|
||||||
from __future__ import unicode_literals, absolute_import
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
from .core import BatchMasterView, FileBatchMasterView
|
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
|
# Rattail -- Retail Software Framework
|
||||||
|
@ -29,7 +29,7 @@ from __future__ import unicode_literals, absolute_import
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
|
|
||||||
from tailbone import forms
|
from tailbone import forms
|
||||||
from tailbone.views.batch import BatchMasterView
|
from tailbone.views.batch import BatchMasterView2 as BatchMasterView
|
||||||
|
|
||||||
|
|
||||||
class PricingBatchView(BatchMasterView):
|
class PricingBatchView(BatchMasterView):
|
||||||
|
@ -46,6 +46,31 @@ class PricingBatchView(BatchMasterView):
|
||||||
rows_editable = True
|
rows_editable = True
|
||||||
bulk_deletable = 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):
|
def configure_fieldset(self, fs):
|
||||||
fs.configure(
|
fs.configure(
|
||||||
include=[
|
include=[
|
||||||
|
@ -57,43 +82,25 @@ class PricingBatchView(BatchMasterView):
|
||||||
fs.executed_by,
|
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):
|
def configure_row_grid(self, g):
|
||||||
g.configure(
|
super(PricingBatchView, self).configure_row_grid(g)
|
||||||
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)
|
|
||||||
|
|
||||||
def row_grid_row_attrs(self, row, i):
|
g.set_type('old_price', 'currency')
|
||||||
attrs = {}
|
g.set_type('new_price', 'currency')
|
||||||
if row.status_code in (row.STATUS_PRICE_INCREASE,
|
g.set_type('price_diff', 'currency')
|
||||||
row.STATUS_PRICE_DECREASE):
|
|
||||||
attrs['class_'] = 'notice'
|
g.set_label('upc', "UPC")
|
||||||
elif row.status_code == row.STATUS_CANNOT_CALCULATE_PRICE:
|
g.set_label('brand_name', "Brand")
|
||||||
attrs['class_'] = 'warning'
|
g.set_label('regular_unit_cost', "Reg. Cost")
|
||||||
return attrs
|
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):
|
def _preconfigure_row_fieldset(self, fs):
|
||||||
super(PricingBatchView, self)._preconfigure_row_fieldset(fs)
|
super(PricingBatchView, self)._preconfigure_row_fieldset(fs)
|
||||||
|
|
|
@ -37,9 +37,9 @@ import formalchemy
|
||||||
from pyramid.response import FileResponse
|
from pyramid.response import FileResponse
|
||||||
from webhelpers2.html import literal
|
from webhelpers2.html import literal
|
||||||
|
|
||||||
from tailbone import newgrids as grids
|
from tailbone import grids3 as grids
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.views import MasterView
|
from tailbone.views import MasterView2 as MasterView
|
||||||
from tailbone.forms.renderers.bouncer import BounceMessageFieldRenderer
|
from tailbone.forms.renderers.bouncer import BounceMessageFieldRenderer
|
||||||
|
|
||||||
|
|
||||||
|
@ -53,36 +53,39 @@ class EmailBouncesView(MasterView):
|
||||||
creatable = False
|
creatable = False
|
||||||
editable = False
|
editable = False
|
||||||
|
|
||||||
|
grid_columns = [
|
||||||
|
'config_key',
|
||||||
|
'bounced',
|
||||||
|
'bounce_recipient_address',
|
||||||
|
'intended_recipient_address',
|
||||||
|
'processed_by',
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self, request):
|
def __init__(self, request):
|
||||||
super(EmailBouncesView, self).__init__(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):
|
def get_handler(self, bounce):
|
||||||
return get_handler(self.rattail_config, bounce.config_key)
|
return get_handler(self.rattail_config, bounce.config_key)
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
|
super(EmailBouncesView, self).configure_grid(g)
|
||||||
|
|
||||||
g.joiners['processed_by'] = lambda q: q.outerjoin(model.User)
|
g.joiners['processed_by'] = lambda q: q.outerjoin(model.User)
|
||||||
g.filters['config_key'].default_active = True
|
g.filters['config_key'].default_active = True
|
||||||
g.filters['config_key'].default_verb = 'equal'
|
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['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_active = True
|
||||||
g.filters['processed'].default_verb = 'is_null'
|
g.filters['processed'].default_verb = 'is_null'
|
||||||
g.filters['processed_by'] = g.make_filter('processed_by', model.User.username)
|
g.filters['processed_by'] = g.make_filter('processed_by', model.User.username)
|
||||||
g.sorters['processed_by'] = g.make_sorter(model.User.username)
|
g.sorters['processed_by'] = g.make_sorter(model.User.username)
|
||||||
g.default_sortkey = 'bounced'
|
g.default_sortkey = 'bounced'
|
||||||
g.default_sortdir = 'desc'
|
g.default_sortdir = 'desc'
|
||||||
g.configure(
|
|
||||||
include=[
|
g.set_label('config_key', "Source")
|
||||||
g.config_key.label("Source"),
|
g.set_label('bounce_recipient_address', "Bounced To")
|
||||||
g.bounced,
|
g.set_label('intended_recipient_address', "Intended For")
|
||||||
g.bounce_recipient_address.label("Bounced To"),
|
|
||||||
g.intended_recipient_address.label("Intended For"),
|
|
||||||
g.processed_by,
|
|
||||||
],
|
|
||||||
readonly=True)
|
|
||||||
|
|
||||||
def configure_fieldset(self, fs):
|
def configure_fieldset(self, fs):
|
||||||
bounce = fs.model
|
bounce = fs.model
|
||||||
|
|
|
@ -28,7 +28,7 @@ from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
|
|
||||||
from tailbone.views import MasterView, AutocompleteView
|
from tailbone.views import MasterView2 as MasterView, AutocompleteView
|
||||||
|
|
||||||
|
|
||||||
class BrandsView(MasterView):
|
class BrandsView(MasterView):
|
||||||
|
@ -36,16 +36,14 @@ class BrandsView(MasterView):
|
||||||
Master view for the Brand class.
|
Master view for the Brand class.
|
||||||
"""
|
"""
|
||||||
model_class = model.Brand
|
model_class = model.Brand
|
||||||
|
grid_columns = [
|
||||||
|
'name',
|
||||||
|
]
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
g.filters['name'].default_active = True
|
g.filters['name'].default_active = True
|
||||||
g.filters['name'].default_verb = 'contains'
|
g.filters['name'].default_verb = 'contains'
|
||||||
g.default_sortkey = 'name'
|
g.default_sortkey = 'name'
|
||||||
g.configure(
|
|
||||||
include=[
|
|
||||||
g.name,
|
|
||||||
],
|
|
||||||
readonly=True)
|
|
||||||
|
|
||||||
def configure_fieldset(self, fs):
|
def configure_fieldset(self, fs):
|
||||||
fs.configure(
|
fs.configure(
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8; -*-
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
|
@ -29,7 +29,7 @@ from __future__ import unicode_literals, absolute_import
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
|
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.views import MasterView
|
from tailbone.views import MasterView2 as MasterView
|
||||||
|
|
||||||
|
|
||||||
class CustomerGroupsView(MasterView):
|
class CustomerGroupsView(MasterView):
|
||||||
|
@ -38,17 +38,16 @@ class CustomerGroupsView(MasterView):
|
||||||
"""
|
"""
|
||||||
model_class = model.CustomerGroup
|
model_class = model.CustomerGroup
|
||||||
model_title = "Customer Group"
|
model_title = "Customer Group"
|
||||||
|
grid_columns = [
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
]
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
g.filters['name'].default_active = True
|
g.filters['name'].default_active = True
|
||||||
g.filters['name'].default_verb = 'contains'
|
g.filters['name'].default_verb = 'contains'
|
||||||
g.default_sortkey = 'name'
|
g.default_sortkey = 'name'
|
||||||
g.configure(
|
g.set_label('id', "ID")
|
||||||
include=[
|
|
||||||
g.id.label("ID"),
|
|
||||||
g.name,
|
|
||||||
],
|
|
||||||
readonly=True)
|
|
||||||
|
|
||||||
def configure_fieldset(self, fs):
|
def configure_fieldset(self, fs):
|
||||||
fs.configure(
|
fs.configure(
|
||||||
|
|
|
@ -35,7 +35,7 @@ from pyramid.httpexceptions import HTTPNotFound
|
||||||
|
|
||||||
from tailbone import forms
|
from tailbone import forms
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.views import MasterView, AutocompleteView
|
from tailbone.views import MasterView2 as MasterView, AutocompleteView
|
||||||
|
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
|
|
||||||
|
@ -47,8 +47,17 @@ class CustomersView(MasterView):
|
||||||
model_class = model.Customer
|
model_class = model.Customer
|
||||||
has_versions = True
|
has_versions = True
|
||||||
supports_mobile = 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_(
|
g.joiners['email'] = lambda q: q.outerjoin(model.CustomerEmailAddress, sa.and_(
|
||||||
model.CustomerEmailAddress.parent_uuid == model.Customer.uuid,
|
model.CustomerEmailAddress.parent_uuid == model.Customer.uuid,
|
||||||
model.CustomerEmailAddress.preference == 1))
|
model.CustomerEmailAddress.preference == 1))
|
||||||
|
@ -66,23 +75,15 @@ class CustomersView(MasterView):
|
||||||
|
|
||||||
g.filters['name'].default_active = True
|
g.filters['name'].default_active = True
|
||||||
g.filters['name'].default_verb = 'contains'
|
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['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.sorters['phone'] = lambda q, d: q.order_by(getattr(model.CustomerPhoneNumber.number, d)())
|
||||||
|
|
||||||
g.default_sortkey = 'name'
|
g.default_sortkey = 'name'
|
||||||
|
|
||||||
def configure_grid(self, g):
|
g.set_label('id', "ID")
|
||||||
g.configure(
|
g.set_label('phone', "Phone Number")
|
||||||
include=[
|
g.set_label('email', "Email Address")
|
||||||
g.id.label("ID"),
|
|
||||||
g.number,
|
|
||||||
g.name,
|
|
||||||
g.phone.label("Phone Number"),
|
|
||||||
g.email.label("Email Address"),
|
|
||||||
],
|
|
||||||
readonly=True)
|
|
||||||
|
|
||||||
def get_mobile_data(self, session=None):
|
def get_mobile_data(self, session=None):
|
||||||
# TODO: hacky!
|
# TODO: hacky!
|
||||||
|
|
|
@ -31,7 +31,7 @@ import logging
|
||||||
|
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
|
|
||||||
from tailbone.views import MasterView
|
from tailbone.views import MasterView2 as MasterView
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
@ -48,18 +48,18 @@ class DataSyncChangesView(MasterView):
|
||||||
creatable = False
|
creatable = False
|
||||||
editable = False
|
editable = False
|
||||||
|
|
||||||
|
grid_columns = [
|
||||||
|
'source',
|
||||||
|
'payload_type',
|
||||||
|
'payload_key',
|
||||||
|
'deletion',
|
||||||
|
'obtained',
|
||||||
|
'consumer',
|
||||||
|
]
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
|
super(DataSyncChangesView, self).configure_grid(g)
|
||||||
g.default_sortkey = 'obtained'
|
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):
|
def restart(self):
|
||||||
# TODO: Add better validation (e.g. CSRF) here?
|
# 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 rattail.db import model
|
||||||
|
|
||||||
from tailbone import newgrids as grids
|
from tailbone import grids3 as grids
|
||||||
from tailbone.views import MasterView, AutocompleteView
|
from tailbone.newgrids import AlchemyGrid
|
||||||
|
from tailbone.views import MasterView2 as MasterView, AutocompleteView
|
||||||
|
|
||||||
|
|
||||||
class DepartmentsView(MasterView):
|
class DepartmentsView(MasterView):
|
||||||
|
@ -38,16 +39,16 @@ class DepartmentsView(MasterView):
|
||||||
"""
|
"""
|
||||||
model_class = model.Department
|
model_class = model.Department
|
||||||
|
|
||||||
|
grid_columns = [
|
||||||
|
'number',
|
||||||
|
'name',
|
||||||
|
]
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
|
super(DepartmentsView, self).configure_grid(g)
|
||||||
g.filters['name'].default_active = True
|
g.filters['name'].default_active = True
|
||||||
g.filters['name'].default_verb = 'contains'
|
g.filters['name'].default_verb = 'contains'
|
||||||
g.default_sortkey = 'number'
|
g.default_sortkey = 'number'
|
||||||
g.configure(
|
|
||||||
include=[
|
|
||||||
g.number,
|
|
||||||
g.name,
|
|
||||||
],
|
|
||||||
readonly=True)
|
|
||||||
|
|
||||||
def configure_fieldset(self, fs):
|
def configure_fieldset(self, fs):
|
||||||
fs.configure(
|
fs.configure(
|
||||||
|
@ -67,7 +68,7 @@ class DepartmentsView(MasterView):
|
||||||
# shouldn't need a key for this one, for instance (no settings
|
# shouldn't need a key for this one, for instance (no settings
|
||||||
# required), but there is plenty of room for improvement here.
|
# required), but there is plenty of room for improvement here.
|
||||||
employees = sorted(department.employees, key=unicode)
|
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=[
|
main_actions=[
|
||||||
grids.GridAction('view', icon='zoomin',
|
grids.GridAction('view', icon='zoomin',
|
||||||
url=lambda r, i: self.request.route_url('employees.view', uuid=r.uuid)),
|
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
|
# Rattail -- Retail Software Framework
|
||||||
|
@ -26,36 +26,18 @@ Email Views
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
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 import mail
|
||||||
from rattail.db import api
|
from rattail.db import api
|
||||||
from rattail.config import parse_list
|
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.db import Session
|
||||||
from tailbone.views import MasterView, View
|
from tailbone.views import View, MasterView2 as MasterView
|
||||||
|
|
||||||
|
|
||||||
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]))
|
|
||||||
|
|
||||||
|
|
||||||
class EmailListFieldRenderer(formalchemy.TextAreaFieldRenderer):
|
class EmailListFieldRenderer(formalchemy.TextAreaFieldRenderer):
|
||||||
|
@ -75,13 +57,17 @@ class ProfilesView(MasterView):
|
||||||
model_title = "Email Profile"
|
model_title = "Email Profile"
|
||||||
model_key = 'key'
|
model_key = 'key'
|
||||||
url_prefix = '/email/profiles'
|
url_prefix = '/email/profiles'
|
||||||
|
|
||||||
grid_factory = grids.Grid
|
|
||||||
filterable = False
|
filterable = False
|
||||||
pageable = False
|
pageable = False
|
||||||
|
|
||||||
creatable = False
|
creatable = False
|
||||||
deletable = False
|
deletable = False
|
||||||
|
grid_columns = [
|
||||||
|
'key',
|
||||||
|
'prefix',
|
||||||
|
'subject',
|
||||||
|
'to',
|
||||||
|
'enabled',
|
||||||
|
]
|
||||||
|
|
||||||
def get_data(self, session=None):
|
def get_data(self, session=None):
|
||||||
data = []
|
data = []
|
||||||
|
@ -91,6 +77,23 @@ class ProfilesView(MasterView):
|
||||||
data.append(self.normalize(email))
|
data.append(self.normalize(email))
|
||||||
return data
|
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 normalize(self, email):
|
||||||
def get_recips(type_):
|
def get_recips(type_):
|
||||||
recips = email.get_recips(type_)
|
recips = email.get_recips(type_)
|
||||||
|
@ -112,26 +115,6 @@ class ProfilesView(MasterView):
|
||||||
'enabled': email.get_enabled(),
|
'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):
|
def get_instance(self):
|
||||||
key = self.request.matchdict['key']
|
key = self.request.matchdict['key']
|
||||||
return self.normalize(mail.get_email(self.rattail_config, key))
|
return self.normalize(mail.get_email(self.rattail_config, key))
|
||||||
|
|
|
@ -32,9 +32,9 @@ from rattail.db import model
|
||||||
|
|
||||||
import formalchemy as fa
|
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.db import Session
|
||||||
from tailbone.views import MasterView, AutocompleteView
|
from tailbone.views import MasterView2 as MasterView, AutocompleteView
|
||||||
|
|
||||||
|
|
||||||
class EmployeesView(MasterView):
|
class EmployeesView(MasterView):
|
||||||
|
@ -44,7 +44,18 @@ class EmployeesView(MasterView):
|
||||||
model_class = model.Employee
|
model_class = model.Employee
|
||||||
has_versions = True
|
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_(
|
g.joiners['phone'] = lambda q: q.outerjoin(model.EmployeePhoneNumber, sa.and_(
|
||||||
model.EmployeePhoneNumber.parent_uuid == model.Employee.uuid,
|
model.EmployeePhoneNumber.parent_uuid == model.Employee.uuid,
|
||||||
model.EmployeePhoneNumber.preference == 1))
|
model.EmployeePhoneNumber.preference == 1))
|
||||||
|
@ -61,7 +72,6 @@ class EmployeesView(MasterView):
|
||||||
label="Phone Number")
|
label="Phone Number")
|
||||||
|
|
||||||
if self.request.has_perm('employees.edit'):
|
if self.request.has_perm('employees.edit'):
|
||||||
g.filters['id'].label = "ID"
|
|
||||||
g.filters['status'].default_active = True
|
g.filters['status'].default_active = True
|
||||||
g.filters['status'].default_verb = 'equal'
|
g.filters['status'].default_verb = 'equal'
|
||||||
g.filters['status'].default_value = self.enum.EMPLOYEE_STATUS_CURRENT
|
g.filters['status'].default_value = self.enum.EMPLOYEE_STATUS_CURRENT
|
||||||
|
@ -84,25 +94,15 @@ class EmployeesView(MasterView):
|
||||||
|
|
||||||
g.default_sortkey = 'first_name'
|
g.default_sortkey = 'first_name'
|
||||||
|
|
||||||
g.append(forms.AssociationProxyField('first_name'))
|
g.set_enum('status', self.enum.EMPLOYEE_STATUS)
|
||||||
g.append(forms.AssociationProxyField('last_name'))
|
|
||||||
|
|
||||||
def configure_grid(self, g):
|
g.set_label('id', "ID")
|
||||||
|
g.set_label('phone', "Phone Number")
|
||||||
g.configure(
|
g.set_label('email', "Email Address")
|
||||||
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)
|
|
||||||
|
|
||||||
if not self.request.has_perm('employees.edit'):
|
if not self.request.has_perm('employees.edit'):
|
||||||
del g.id
|
g.hide_column('id')
|
||||||
del g.status
|
g.hide_column('status')
|
||||||
|
|
||||||
def query(self, session):
|
def query(self, session):
|
||||||
q = session.query(model.Employee).join(model.Person)
|
q = session.query(model.Employee).join(model.Person)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8; -*-
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
|
@ -34,7 +34,7 @@ import formalchemy as fa
|
||||||
from pyramid.response import FileResponse
|
from pyramid.response import FileResponse
|
||||||
|
|
||||||
from tailbone import forms
|
from tailbone import forms
|
||||||
from tailbone.views import MasterView
|
from tailbone.views import MasterView2 as MasterView
|
||||||
|
|
||||||
|
|
||||||
class ExportMasterView(MasterView):
|
class ExportMasterView(MasterView):
|
||||||
|
@ -45,6 +45,13 @@ class ExportMasterView(MasterView):
|
||||||
editable = False
|
editable = False
|
||||||
export_has_file = False
|
export_has_file = False
|
||||||
|
|
||||||
|
grid_columns = [
|
||||||
|
'id',
|
||||||
|
'created',
|
||||||
|
'created_by',
|
||||||
|
'record_count',
|
||||||
|
]
|
||||||
|
|
||||||
def get_export_key(self):
|
def get_export_key(self):
|
||||||
if hasattr(self, 'export_key'):
|
if hasattr(self, 'export_key'):
|
||||||
return self.export_key
|
return self.export_key
|
||||||
|
@ -56,26 +63,22 @@ class ExportMasterView(MasterView):
|
||||||
export.filename,
|
export.filename,
|
||||||
makedirs=makedirs)
|
makedirs=makedirs)
|
||||||
|
|
||||||
def _preconfigure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
g.filters['id'].label = "ID"
|
super(ExportMasterView, self).configure_grid(g)
|
||||||
g.id.set(label="ID", renderer=forms.renderers.BatchIDFieldRenderer)
|
|
||||||
g.joiners['created_by'] = lambda q: q.join(model.User)
|
g.joiners['created_by'] = lambda q: q.join(model.User)
|
||||||
g.sorters['created_by'] = g.make_sorter(model.User.username)
|
g.sorters['created_by'] = g.make_sorter(model.User.username)
|
||||||
g.filters['created_by'] = g.make_filter('created_by', 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.default_sortkey = 'created'
|
g.default_sortkey = 'created'
|
||||||
g.default_sortdir = 'desc'
|
g.default_sortdir = 'desc'
|
||||||
|
|
||||||
def configure_grid(self, g):
|
g.set_renderer('id', self.render_id)
|
||||||
g.configure(
|
|
||||||
include=[
|
g.set_label('id', "ID")
|
||||||
g.id,
|
g.set_label('created_by', "Created by")
|
||||||
g.created,
|
|
||||||
g.created_by,
|
def render_id(self, export, column):
|
||||||
g.record_count,
|
return export.id_str
|
||||||
],
|
|
||||||
readonly=True)
|
|
||||||
|
|
||||||
def _preconfigure_fieldset(self, fs):
|
def _preconfigure_fieldset(self, fs):
|
||||||
fs.id.set(label="ID", renderer=forms.renderers.BatchIDFieldRenderer)
|
fs.id.set(label="ID", renderer=forms.renderers.BatchIDFieldRenderer)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8; -*-
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
|
@ -26,10 +26,10 @@ Family Views
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
from tailbone.views import MasterView
|
|
||||||
|
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
|
|
||||||
|
from tailbone.views import MasterView2 as MasterView
|
||||||
|
|
||||||
|
|
||||||
class FamiliesView(MasterView):
|
class FamiliesView(MasterView):
|
||||||
"""
|
"""
|
||||||
|
@ -39,17 +39,15 @@ class FamiliesView(MasterView):
|
||||||
model_title_plural = "Families"
|
model_title_plural = "Families"
|
||||||
route_prefix = 'families'
|
route_prefix = 'families'
|
||||||
grid_key = 'families'
|
grid_key = 'families'
|
||||||
|
grid_columns = [
|
||||||
|
'code',
|
||||||
|
'name',
|
||||||
|
]
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
g.filters['name'].default_active = True
|
g.filters['name'].default_active = True
|
||||||
g.filters['name'].default_verb = 'contains'
|
g.filters['name'].default_verb = 'contains'
|
||||||
g.default_sortkey = 'code'
|
g.default_sortkey = 'code'
|
||||||
g.configure(
|
|
||||||
include=[
|
|
||||||
g.code,
|
|
||||||
g.name,
|
|
||||||
],
|
|
||||||
readonly=True)
|
|
||||||
|
|
||||||
def configure_fieldset(self, fs):
|
def configure_fieldset(self, fs):
|
||||||
fs.configure(
|
fs.configure(
|
||||||
|
|
|
@ -38,7 +38,7 @@ from webhelpers2.html import tags
|
||||||
|
|
||||||
from tailbone import forms
|
from tailbone import forms
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.views.batch import FileBatchMasterView
|
from tailbone.views.batch import FileBatchMasterView2 as FileBatchMasterView
|
||||||
|
|
||||||
|
|
||||||
ACTION_OPTIONS = OrderedDict([
|
ACTION_OPTIONS = OrderedDict([
|
||||||
|
@ -84,27 +84,37 @@ class HandheldBatchView(FileBatchMasterView):
|
||||||
rows_creatable = False
|
rows_creatable = False
|
||||||
rows_editable = True
|
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):
|
def configure_grid(self, g):
|
||||||
|
super(HandheldBatchView, self).configure_grid(g)
|
||||||
device_types = OrderedDict(sorted(self.enum.HANDHELD_DEVICE_TYPE.items(),
|
device_types = OrderedDict(sorted(self.enum.HANDHELD_DEVICE_TYPE.items(),
|
||||||
key=lambda item: item[1]))
|
key=lambda item: item[1]))
|
||||||
g.configure(
|
g.set_enum('device_type', device_types)
|
||||||
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)
|
|
||||||
|
|
||||||
def row_attrs(self, batch, i):
|
def grid_extra_class(self, batch, i):
|
||||||
attrs = {}
|
|
||||||
if batch.status_code is not None and batch.status_code != batch.STATUS_OK:
|
if batch.status_code is not None and batch.status_code != batch.STATUS_OK:
|
||||||
attrs['class_'] = 'notice'
|
return 'notice'
|
||||||
return attrs
|
|
||||||
|
|
||||||
def _preconfigure_fieldset(self, fs):
|
def _preconfigure_fieldset(self, fs):
|
||||||
super(HandheldBatchView, self)._preconfigure_fieldset(fs)
|
super(HandheldBatchView, self)._preconfigure_fieldset(fs)
|
||||||
|
@ -145,32 +155,16 @@ class HandheldBatchView(FileBatchMasterView):
|
||||||
kwargs['device_name'] = batch.device_name
|
kwargs['device_name'] = batch.device_name
|
||||||
return kwargs
|
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):
|
def configure_row_grid(self, g):
|
||||||
g.configure(
|
super(HandheldBatchView, self).configure_row_grid(g)
|
||||||
include=[
|
g.set_type('cases', 'quantity')
|
||||||
g.sequence,
|
g.set_type('units', 'quantity')
|
||||||
g.upc,
|
g.set_label('upc', "UPC")
|
||||||
g.brand_name,
|
g.set_label('brand_name', "Brand")
|
||||||
g.description,
|
|
||||||
g.size,
|
|
||||||
g.cases,
|
|
||||||
g.units,
|
|
||||||
g.status_code,
|
|
||||||
],
|
|
||||||
readonly=True)
|
|
||||||
|
|
||||||
def row_grid_row_attrs(self, row, i):
|
def row_grid_extra_class(self, row, i):
|
||||||
attrs = {}
|
|
||||||
if row.status_code == row.STATUS_PRODUCT_NOT_FOUND:
|
if row.status_code == row.STATUS_PRODUCT_NOT_FOUND:
|
||||||
attrs['class_'] = 'warning'
|
return 'warning'
|
||||||
return attrs
|
|
||||||
|
|
||||||
def _preconfigure_row_fieldset(self, fs):
|
def _preconfigure_row_fieldset(self, fs):
|
||||||
super(HandheldBatchView, self)._preconfigure_row_fieldset(fs)
|
super(HandheldBatchView, self)._preconfigure_row_fieldset(fs)
|
||||||
|
|
|
@ -28,6 +28,8 @@ from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
from rattail import pod
|
from rattail import pod
|
||||||
from rattail.db import model, api
|
from rattail.db import model, api
|
||||||
from rattail.time import localtime
|
from rattail.time import localtime
|
||||||
|
@ -36,10 +38,9 @@ from rattail.util import pretty_quantity
|
||||||
|
|
||||||
import formalchemy as fa
|
import formalchemy as fa
|
||||||
import formencode as fe
|
import formencode as fe
|
||||||
from webhelpers2.html import tags
|
|
||||||
|
|
||||||
from tailbone import forms
|
from tailbone import forms
|
||||||
from tailbone.views.batch import BatchMasterView
|
from tailbone.views.batch import BatchMasterView2 as BatchMasterView
|
||||||
|
|
||||||
|
|
||||||
class InventoryBatchView(BatchMasterView):
|
class InventoryBatchView(BatchMasterView):
|
||||||
|
@ -54,28 +55,42 @@ class InventoryBatchView(BatchMasterView):
|
||||||
creatable = False
|
creatable = False
|
||||||
mobile_creatable = True
|
mobile_creatable = True
|
||||||
|
|
||||||
|
grid_columns = [
|
||||||
|
'id',
|
||||||
|
'created',
|
||||||
|
'created_by',
|
||||||
|
'rowcount',
|
||||||
|
'executed',
|
||||||
|
'executed_by',
|
||||||
|
'mode',
|
||||||
|
]
|
||||||
|
|
||||||
model_row_class = model.InventoryBatchRow
|
model_row_class = model.InventoryBatchRow
|
||||||
rows_editable = True
|
rows_editable = True
|
||||||
|
|
||||||
def _preconfigure_grid(self, g):
|
row_grid_columns = [
|
||||||
super(InventoryBatchView, self)._preconfigure_grid(g)
|
'sequence',
|
||||||
g.mode.set(renderer=forms.renderers.EnumFieldRenderer(self.enum.INVENTORY_MODE),
|
'upc',
|
||||||
label="Count Mode")
|
'brand_name',
|
||||||
|
'description',
|
||||||
|
'size',
|
||||||
|
'cases',
|
||||||
|
'units',
|
||||||
|
'unit_cost',
|
||||||
|
'status_code',
|
||||||
|
]
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
g.configure(include=[
|
super(InventoryBatchView, self).configure_grid(g)
|
||||||
g.id,
|
g.set_enum('mode', self.enum.INVENTORY_MODE)
|
||||||
g.created,
|
g.set_label('mode', "Count Mode")
|
||||||
g.created_by,
|
|
||||||
g.rowcount,
|
|
||||||
g.executed,
|
|
||||||
g.executed_by,
|
|
||||||
g.mode,
|
|
||||||
], readonly=True)
|
|
||||||
|
|
||||||
def configure_mobile_grid(self, g):
|
def render_mobile_listitem(self, batch, i):
|
||||||
super(InventoryBatchView, self).configure_mobile_grid(g)
|
return "({}) {} rows - {}, {}".format(
|
||||||
g.listitem.set(renderer=InventoryBatchRenderer)
|
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):
|
def _preconfigure_fieldset(self, fs):
|
||||||
super(InventoryBatchView, self)._preconfigure_fieldset(fs)
|
super(InventoryBatchView, self)._preconfigure_fieldset(fs)
|
||||||
|
@ -202,45 +217,26 @@ class InventoryBatchView(BatchMasterView):
|
||||||
|
|
||||||
return self.render_to_response('view_row', context, mobile=True)
|
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):
|
def configure_row_grid(self, g):
|
||||||
g.configure(
|
super(InventoryBatchView, self).configure_row_grid(g)
|
||||||
include=[
|
|
||||||
g.sequence,
|
|
||||||
g.upc,
|
|
||||||
g.brand_name,
|
|
||||||
g.description,
|
|
||||||
g.size,
|
|
||||||
g.cases,
|
|
||||||
g.units,
|
|
||||||
g.unit_cost,
|
|
||||||
g.status_code,
|
|
||||||
],
|
|
||||||
readonly=True)
|
|
||||||
|
|
||||||
def row_grid_row_attrs(self, row, i):
|
g.set_renderer('cases', 'quantity')
|
||||||
attrs = {}
|
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:
|
if row.status_code == row.STATUS_PRODUCT_NOT_FOUND:
|
||||||
attrs['class_'] = 'warning'
|
return 'warning'
|
||||||
return attrs
|
|
||||||
|
|
||||||
def render_mobile_row_listitem(self, row, **kwargs):
|
def render_mobile_row_listitem(self, row, i):
|
||||||
if row is None:
|
|
||||||
return ''
|
|
||||||
description = row.product.full_description if row.product else row.description
|
description = row.product.full_description if row.product else row.description
|
||||||
unit_uom = 'LB' if row.product and row.product.weighed else 'EA'
|
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)
|
qty = "{} {}".format(pretty_quantity(row.cases or row.units), 'CS' if row.cases else unit_uom)
|
||||||
title = "({}) {} - {}".format(row.upc.pretty(), description, qty)
|
return "({}) {} - {}".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)
|
|
||||||
|
|
||||||
def _preconfigure_row_fieldset(self, fs):
|
def _preconfigure_row_fieldset(self, fs):
|
||||||
super(InventoryBatchView, self)._preconfigure_row_fieldset(fs)
|
super(InventoryBatchView, self)._preconfigure_row_fieldset(fs)
|
||||||
|
@ -281,19 +277,6 @@ class InventoryBatchView(BatchMasterView):
|
||||||
permission='{}.create'.format(row_permission_prefix))
|
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):
|
class ValidBatchRow(forms.validators.ModelValidator):
|
||||||
model_class = model.InventoryBatchRow
|
model_class = model.InventoryBatchRow
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ from rattail.db import model
|
||||||
import formalchemy as fa
|
import formalchemy as fa
|
||||||
|
|
||||||
from tailbone import forms
|
from tailbone import forms
|
||||||
from tailbone.views.batch import BatchMasterView
|
from tailbone.views.batch import BatchMasterView2 as BatchMasterView
|
||||||
|
|
||||||
|
|
||||||
class LabelBatchView(BatchMasterView):
|
class LabelBatchView(BatchMasterView):
|
||||||
|
@ -49,6 +49,19 @@ class LabelBatchView(BatchMasterView):
|
||||||
rows_editable = True
|
rows_editable = True
|
||||||
cloneable = 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):
|
def _preconfigure_fieldset(self, fs):
|
||||||
super(LabelBatchView, self)._preconfigure_fieldset(fs)
|
super(LabelBatchView, self)._preconfigure_fieldset(fs)
|
||||||
fs.append(fa.Field('handheld_batches', renderer=forms.renderers.HandheldBatchesFieldRenderer, readonly=True,
|
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:
|
if self.viewing and not batch._handhelds:
|
||||||
del fs.handheld_batches
|
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):
|
def configure_row_grid(self, g):
|
||||||
g.configure(
|
super(LabelBatchView, self).configure_row_grid(g)
|
||||||
include=[
|
g.set_label('upc', "UPC")
|
||||||
g.sequence,
|
g.set_label('brand_name', "Brand")
|
||||||
g.upc,
|
g.set_label('regular_price', "Reg Price")
|
||||||
g.brand_name,
|
g.set_label('label_profile', "Label Type")
|
||||||
g.description,
|
g.set_label('label_quantity', "Qty")
|
||||||
g.size,
|
|
||||||
g.regular_price,
|
|
||||||
g.sale_price,
|
|
||||||
g.label_profile,
|
|
||||||
g.label_quantity,
|
|
||||||
g.status_code,
|
|
||||||
],
|
|
||||||
readonly=True)
|
|
||||||
|
|
||||||
def row_grid_row_attrs(self, row, i):
|
def row_grid_extra_class(self, row, i):
|
||||||
attrs = {}
|
|
||||||
if row.status_code != row.STATUS_OK:
|
if row.status_code != row.STATUS_OK:
|
||||||
attrs['class_'] = 'warning'
|
return 'warning'
|
||||||
return attrs
|
|
||||||
|
|
||||||
def _preconfigure_row_fieldset(self, fs):
|
def _preconfigure_row_fieldset(self, fs):
|
||||||
fs.sequence.set(readonly=True)
|
fs.sequence.set(readonly=True)
|
||||||
|
|
|
@ -32,7 +32,7 @@ from pyramid.httpexceptions import HTTPFound
|
||||||
|
|
||||||
from tailbone import forms
|
from tailbone import forms
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.views import MasterView
|
from tailbone.views import MasterView2 as MasterView
|
||||||
|
|
||||||
|
|
||||||
class ProfilesView(MasterView):
|
class ProfilesView(MasterView):
|
||||||
|
@ -42,17 +42,17 @@ class ProfilesView(MasterView):
|
||||||
model_class = model.LabelProfile
|
model_class = model.LabelProfile
|
||||||
model_title = "Label Profile"
|
model_title = "Label Profile"
|
||||||
url_prefix = '/labels/profiles'
|
url_prefix = '/labels/profiles'
|
||||||
|
grid_columns = [
|
||||||
|
'ordinal',
|
||||||
|
'code',
|
||||||
|
'description',
|
||||||
|
'visible',
|
||||||
|
]
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
|
super(ProfilesView, self).configure_grid(g)
|
||||||
g.default_sortkey = 'ordinal'
|
g.default_sortkey = 'ordinal'
|
||||||
g.configure(
|
g.set_type('visible', 'boolean')
|
||||||
include=[
|
|
||||||
g.ordinal,
|
|
||||||
g.code,
|
|
||||||
g.description,
|
|
||||||
g.visible,
|
|
||||||
],
|
|
||||||
readonly=True)
|
|
||||||
|
|
||||||
def configure_fieldset(self, fs):
|
def configure_fieldset(self, fs):
|
||||||
fs.printer_spec.set(renderer=forms.renderers.StrippedTextFieldRenderer)
|
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))
|
return self.redirect(self.request.current_route_url(_query=None))
|
||||||
|
|
||||||
# Stash some grid stats, for possible use when generating URLs.
|
# 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
|
self.first_visible_grid_index = grid.pager.first_item
|
||||||
|
|
||||||
# Return grid only, if partial page was requested.
|
# Return grid only, if partial page was requested.
|
||||||
|
@ -170,6 +170,15 @@ class MasterView(View):
|
||||||
"""
|
"""
|
||||||
return getattr(cls, 'mobile_grid_factory', MobileGrid)
|
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
|
@classmethod
|
||||||
def get_mobile_grid_key(cls):
|
def get_mobile_grid_key(cls):
|
||||||
"""
|
"""
|
||||||
|
@ -643,7 +652,7 @@ class MasterView(View):
|
||||||
kwargs.setdefault('request', self.request)
|
kwargs.setdefault('request', self.request)
|
||||||
kwargs.setdefault('model_class', self.model_row_class)
|
kwargs.setdefault('model_class', self.model_row_class)
|
||||||
kwargs = self.make_mobile_row_grid_kwargs(**kwargs)
|
kwargs = self.make_mobile_row_grid_kwargs(**kwargs)
|
||||||
factory = self.get_mobile_grid_factory()
|
factory = self.get_mobile_row_grid_factory()
|
||||||
grid = factory(**kwargs)
|
grid = factory(**kwargs)
|
||||||
self.configure_mobile_row_grid(grid)
|
self.configure_mobile_row_grid(grid)
|
||||||
grid.load_settings()
|
grid.load_settings()
|
||||||
|
@ -703,7 +712,7 @@ class MasterView(View):
|
||||||
kwargs['instance'] = parent
|
kwargs['instance'] = parent
|
||||||
kwargs = self.make_row_grid_kwargs(**kwargs)
|
kwargs = self.make_row_grid_kwargs(**kwargs)
|
||||||
key = '{}.{}'.format(self.get_grid_key(), self.request.matchdict[self.get_model_key()])
|
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)
|
grid = factory(key, self.request, data=data, model_class=self.model_row_class, **kwargs)
|
||||||
self._preconfigure_row_grid(grid)
|
self._preconfigure_row_grid(grid)
|
||||||
self.configure_row_grid(grid)
|
self.configure_row_grid(grid)
|
||||||
|
@ -1253,6 +1262,15 @@ class MasterView(View):
|
||||||
"""
|
"""
|
||||||
return getattr(cls, 'grid_factory', AlchemyGrid)
|
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
|
@classmethod
|
||||||
def get_grid_key(cls):
|
def get_grid_key(cls):
|
||||||
"""
|
"""
|
||||||
|
@ -1787,12 +1805,14 @@ class MasterView(View):
|
||||||
def configure_row_fieldset(self, fs):
|
def configure_row_fieldset(self, fs):
|
||||||
fs.configure()
|
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.
|
Generate a URL for the given action on the given row.
|
||||||
"""
|
"""
|
||||||
return self.request.route_url('{}.{}'.format(self.get_row_route_prefix(), action),
|
route_name = '{}.{}'.format(self.get_row_route_prefix(), action)
|
||||||
**self.get_row_action_route_kwargs(row))
|
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):
|
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 json
|
||||||
import pytz
|
import pytz
|
||||||
|
import six
|
||||||
|
|
||||||
from rattail import enum
|
from rattail import enum
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
|
@ -40,16 +41,8 @@ from webhelpers2.html import tags, HTML
|
||||||
|
|
||||||
from tailbone import forms
|
from tailbone import forms
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.views import MasterView
|
from tailbone.views import MasterView2 as MasterView
|
||||||
|
from tailbone.util import raw_datetime
|
||||||
|
|
||||||
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))
|
|
||||||
|
|
||||||
|
|
||||||
class SenderFieldRenderer(forms.renderers.UserFieldRenderer):
|
class SenderFieldRenderer(forms.renderers.UserFieldRenderer):
|
||||||
|
@ -106,23 +99,6 @@ class RecipientsFieldRenderer(formalchemy.FieldRenderer):
|
||||||
return ', '.join(recips)
|
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):
|
class MessagesView(MasterView):
|
||||||
"""
|
"""
|
||||||
Base class for message views.
|
Base class for message views.
|
||||||
|
@ -133,6 +109,7 @@ class MessagesView(MasterView):
|
||||||
checkboxes = True
|
checkboxes = True
|
||||||
replying = False
|
replying = False
|
||||||
reply_header_sent_format = '%a %d %b %Y at %I:%M %p'
|
reply_header_sent_format = '%a %d %b %Y at %I:%M %p'
|
||||||
|
grid_columns = ['subject', 'sender', 'recipients', 'sent']
|
||||||
|
|
||||||
def get_index_title(self):
|
def get_index_title(self):
|
||||||
if self.listing:
|
if self.listing:
|
||||||
|
@ -176,37 +153,49 @@ class MessagesView(MasterView):
|
||||||
.outerjoin(model.MessageRecipient)\
|
.outerjoin(model.MessageRecipient)\
|
||||||
.filter(model.MessageRecipient.recipient == self.request.user)
|
.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.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,
|
g.filters['sender'] = g.make_filter('sender', model.Person.display_name,
|
||||||
default_active=True, default_verb='contains')
|
default_active=True, default_verb='contains')
|
||||||
g.sorters['sender'] = g.make_sorter(model.Person.display_name)
|
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_active = True
|
||||||
g.filters['subject'].default_verb = 'contains'
|
g.filters['subject'].default_verb = 'contains'
|
||||||
g.subject.set(renderer=SubjectFieldRenderer)
|
|
||||||
|
|
||||||
g.recipients.set(label="To", renderer=TerseRecipientsFieldRenderer)
|
|
||||||
|
|
||||||
g.default_sortkey = 'sent'
|
g.default_sortkey = 'sent'
|
||||||
g.default_sortdir = 'desc'
|
g.default_sortdir = 'desc'
|
||||||
|
|
||||||
def configure_grid(self, g):
|
g.set_renderer('sent', self.render_sent)
|
||||||
g.configure(
|
g.set_renderer('sender', self.render_sender)
|
||||||
include=[
|
g.set_renderer('recipients', self.render_recipients)
|
||||||
g.subject,
|
|
||||||
g.sender,
|
|
||||||
g.recipients,
|
|
||||||
g.sent,
|
|
||||||
],
|
|
||||||
readonly=True)
|
|
||||||
|
|
||||||
def row_attrs(self, row, i):
|
g.set_link('subject')
|
||||||
recip = self.get_recipient(row)
|
|
||||||
if recip:
|
g.set_label('sender', "From")
|
||||||
return {'data-uuid': recip.uuid}
|
g.set_label('recipients', "To")
|
||||||
return {}
|
|
||||||
|
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):
|
def make_form(self, instance, **kwargs):
|
||||||
form = super(MessagesView, self).make_form(instance, **kwargs)
|
form = super(MessagesView, self).make_form(instance, **kwargs)
|
||||||
|
@ -398,9 +387,11 @@ class MessagesView(MasterView):
|
||||||
if uuids:
|
if uuids:
|
||||||
new_status = enum.MESSAGE_STATUS_INBOX if dest == 'inbox' else enum.MESSAGE_STATUS_ARCHIVE
|
new_status = enum.MESSAGE_STATUS_INBOX if dest == 'inbox' else enum.MESSAGE_STATUS_ARCHIVE
|
||||||
for uuid in uuids:
|
for uuid in uuids:
|
||||||
recip = Session.query(model.MessageRecipient).get(uuid) if uuid else None
|
recip = self.Session.query(model.MessageRecipient)\
|
||||||
if recip and recip.recipient is self.request.user:
|
.filter(model.MessageRecipient.message_uuid == uuid)\
|
||||||
if recip.status != new_status:
|
.filter(model.MessageRecipient.recipient_uuid == self.request.user.uuid)\
|
||||||
|
.first()
|
||||||
|
if recip and recip.status != new_status:
|
||||||
recip.status = new_status
|
recip.status = new_status
|
||||||
route = 'messages.{}'.format('archive' if dest == 'inbox' else 'inbox')
|
route = 'messages.{}'.format('archive' if dest == 'inbox' else 'inbox')
|
||||||
return self.redirect(self.request.route_url(route))
|
return self.redirect(self.request.route_url(route))
|
||||||
|
@ -480,8 +471,8 @@ class SentView(MessagesView):
|
||||||
return session.query(model.Message)\
|
return session.query(model.Message)\
|
||||||
.filter(model.Message.sender == self.request.user)
|
.filter(model.Message.sender == self.request.user)
|
||||||
|
|
||||||
def _preconfigure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
super(SentView, self)._preconfigure_grid(g)
|
super(SentView, self).configure_grid(g)
|
||||||
g.filters['sender'].default_active = False
|
g.filters['sender'].default_active = False
|
||||||
g.joiners['recipients'] = lambda q: q.join(model.MessageRecipient)\
|
g.joiners['recipients'] = lambda q: q.join(model.MessageRecipient)\
|
||||||
.join(model.User, model.User.uuid == model.MessageRecipient.recipient_uuid)\
|
.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 webhelpers2.html import HTML, tags
|
||||||
|
|
||||||
from tailbone import forms
|
from tailbone import forms
|
||||||
from tailbone.views import MasterView, AutocompleteView
|
from tailbone.views import MasterView2 as MasterView, AutocompleteView
|
||||||
|
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
|
|
||||||
|
@ -63,7 +63,17 @@ class PeopleView(MasterView):
|
||||||
route_prefix = 'people'
|
route_prefix = 'people'
|
||||||
has_versions = True
|
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_(
|
g.joiners['email'] = lambda q: q.outerjoin(model.PersonEmailAddress, sa.and_(
|
||||||
model.PersonEmailAddress.parent_uuid == model.Person.uuid,
|
model.PersonEmailAddress.parent_uuid == model.Person.uuid,
|
||||||
model.PersonEmailAddress.preference == 1))
|
model.PersonEmailAddress.preference == 1))
|
||||||
|
@ -71,13 +81,11 @@ class PeopleView(MasterView):
|
||||||
model.PersonPhoneNumber.parent_uuid == model.Person.uuid,
|
model.PersonPhoneNumber.parent_uuid == model.Person.uuid,
|
||||||
model.PersonPhoneNumber.preference == 1))
|
model.PersonPhoneNumber.preference == 1))
|
||||||
|
|
||||||
g.filters['email'] = g.make_filter('email', model.PersonEmailAddress.address,
|
g.filters['email'] = g.make_filter('email', model.PersonEmailAddress.address)
|
||||||
label="Email Address")
|
g.filters['phone'] = g.make_filter('phone', model.PersonPhoneNumber.number)
|
||||||
g.filters['phone'] = g.make_filter('phone', model.PersonPhoneNumber.number,
|
|
||||||
label="Phone Number")
|
|
||||||
|
|
||||||
g.joiners['customer_id'] = lambda q: q.outerjoin(model.CustomerPerson).outerjoin(model.Customer)
|
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_active = True
|
||||||
g.filters['first_name'].default_verb = 'contains'
|
g.filters['first_name'].default_verb = 'contains'
|
||||||
|
@ -90,16 +98,10 @@ class PeopleView(MasterView):
|
||||||
|
|
||||||
g.default_sortkey = 'display_name'
|
g.default_sortkey = 'display_name'
|
||||||
|
|
||||||
def configure_grid(self, g):
|
g.set_label('display_name', "Full Name")
|
||||||
g.configure(
|
g.set_label('phone', "Phone Number")
|
||||||
include=[
|
g.set_label('email', "Email Address")
|
||||||
g.display_name.label("Full Name"),
|
g.set_label('customer_id', "Customer ID")
|
||||||
g.first_name,
|
|
||||||
g.last_name,
|
|
||||||
g.phone.label("Phone Number"),
|
|
||||||
g.email.label("Email Address"),
|
|
||||||
],
|
|
||||||
readonly=True)
|
|
||||||
|
|
||||||
def get_instance(self):
|
def get_instance(self):
|
||||||
# TODO: I don't recall why this fallback check for a vendor contact
|
# 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
|
# Rattail -- Retail Software Framework
|
||||||
|
@ -30,7 +30,7 @@ import copy
|
||||||
|
|
||||||
import wtforms
|
import wtforms
|
||||||
|
|
||||||
from tailbone.views import MasterView
|
from tailbone.views import MasterView2 as MasterView
|
||||||
|
|
||||||
|
|
||||||
class PrincipalMasterView(MasterView):
|
class PrincipalMasterView(MasterView):
|
||||||
|
|
|
@ -44,11 +44,11 @@ import wtforms
|
||||||
import formalchemy as fa
|
import formalchemy as fa
|
||||||
from pyramid import httpexceptions
|
from pyramid import httpexceptions
|
||||||
from pyramid.renderers import render_to_response
|
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.db import Session
|
||||||
from tailbone.views import MasterView, AutocompleteView
|
from tailbone.views import MasterView2 as MasterView, AutocompleteView
|
||||||
from tailbone.progress import SessionProgress
|
from tailbone.progress import SessionProgress
|
||||||
|
|
||||||
|
|
||||||
|
@ -71,21 +71,6 @@ from tailbone.progress import SessionProgress
|
||||||
# return query
|
# 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):
|
class ProductsView(MasterView):
|
||||||
"""
|
"""
|
||||||
Master view for the Product class.
|
Master view for the Product class.
|
||||||
|
@ -93,6 +78,17 @@ class ProductsView(MasterView):
|
||||||
model_class = model.Product
|
model_class = model.Product
|
||||||
supports_mobile = True
|
supports_mobile = True
|
||||||
|
|
||||||
|
grid_columns = [
|
||||||
|
'upc',
|
||||||
|
'brand',
|
||||||
|
'description',
|
||||||
|
'size',
|
||||||
|
'subdepartment',
|
||||||
|
'vendor',
|
||||||
|
'regular_price',
|
||||||
|
'current_price',
|
||||||
|
]
|
||||||
|
|
||||||
# child_version_classes = [
|
# child_version_classes = [
|
||||||
# (model.ProductCode, 'product_uuid'),
|
# (model.ProductCode, 'product_uuid'),
|
||||||
# (model.ProductCost, 'product_uuid'),
|
# (model.ProductCost, 'product_uuid'),
|
||||||
|
@ -133,7 +129,9 @@ class ProductsView(MasterView):
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
def _preconfigure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
|
super(ProductsView, self).configure_grid(g)
|
||||||
|
|
||||||
def join_vendor(q):
|
def join_vendor(q):
|
||||||
return q.outerjoin(model.ProductCost,
|
return q.outerjoin(model.ProductCost,
|
||||||
sa.and_(
|
sa.and_(
|
||||||
|
@ -185,7 +183,6 @@ class ProductsView(MasterView):
|
||||||
|
|
||||||
g.filters['upc'].default_active = True
|
g.filters['upc'].default_active = True
|
||||||
g.filters['upc'].default_verb = 'equal'
|
g.filters['upc'].default_verb = 'equal'
|
||||||
g.filters['upc'].label = "UPC"
|
|
||||||
g.filters['description'].default_active = True
|
g.filters['description'].default_active = True
|
||||||
g.filters['description'].default_verb = 'contains'
|
g.filters['description'].default_verb = 'contains'
|
||||||
g.filters['brand'] = g.make_filter('brand', model.Brand.name,
|
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['subdepartment'] = g.make_filter('subdepartment', model.Subdepartment.name)
|
||||||
g.filters['report_code'] = g.make_filter('report_code', model.ReportCode.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['code'] = g.make_filter('code', model.ProductCode.code)
|
||||||
g.filters['vendor'] = g.make_filter('vendor', model.Vendor.name, label="Vendor (preferred)")
|
g.filters['vendor'] = g.make_filter('vendor', model.Vendor.name)
|
||||||
g.filters['vendor_any'] = g.make_filter('vendor_any', self.VendorAny.name, label="Vendor (any)")
|
g.filters['vendor_any'] = g.make_filter('vendor_any', self.VendorAny.name)
|
||||||
# factory=VendorAnyFilter, joiner=join_vendor_any)
|
# factory=VendorAnyFilter, joiner=join_vendor_any)
|
||||||
g.filters['vendor_code'] = g.make_filter('vendor_code', ProductCostCode.code)
|
g.filters['vendor_code'] = g.make_filter('vendor_code', ProductCostCode.code)
|
||||||
g.filters['vendor_code_any'] = g.make_filter('vendor_code_any', ProductCostCodeAny.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,
|
g.joiners['cost'] = lambda q: q.outerjoin(model.ProductCost,
|
||||||
sa.and_(
|
sa.and_(
|
||||||
model.ProductCost.product_uuid == model.Product.uuid,
|
model.ProductCost.product_uuid == model.Product.uuid,
|
||||||
model.ProductCost.preference == 1))
|
model.ProductCost.preference == 1))
|
||||||
g.sorters['cost'] = g.make_sorter(model.ProductCost.unit_cost)
|
g.sorters['cost'] = g.make_sorter(model.ProductCost.unit_cost)
|
||||||
g.filters['cost'] = g.make_filter('cost', 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'
|
g.default_sortkey = 'upc'
|
||||||
|
|
||||||
if self.print_labels and self.request.has_perm('products.print_labels'):
|
if self.print_labels and self.request.has_perm('products.print_labels'):
|
||||||
g.more_actions.append(grids.GridAction('print_label', icon='print'))
|
g.more_actions.append(grids.GridAction('print_label', icon='print'))
|
||||||
|
|
||||||
def configure_grid(self, g):
|
g.set_type('upc', 'gpc')
|
||||||
g.configure(
|
|
||||||
include=[
|
g.set_renderer('regular_price', self.render_price)
|
||||||
g.upc,
|
g.set_renderer('current_price', self.render_price)
|
||||||
g.brand,
|
g.set_renderer('cost', self.render_cost)
|
||||||
g.description,
|
|
||||||
g.size,
|
g.set_link('upc')
|
||||||
g.subdepartment,
|
g.set_link('description')
|
||||||
g.vendor,
|
|
||||||
g.regular_price,
|
g.set_label('upc', "UPC")
|
||||||
g.current_price,
|
g.set_label('vendor', "Vendor (preferred)")
|
||||||
],
|
g.set_label('vendor_any', "Vendor (any)")
|
||||||
readonly=True)
|
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):
|
def template_kwargs_index(self, **kwargs):
|
||||||
if self.print_labels:
|
if self.print_labels:
|
||||||
|
@ -250,19 +260,15 @@ class ProductsView(MasterView):
|
||||||
.all()
|
.all()
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
def row_attrs(self, row, i):
|
|
||||||
|
|
||||||
attrs = {'uuid': row.uuid}
|
|
||||||
|
|
||||||
|
def grid_extra_class(self, product, i):
|
||||||
classes = []
|
classes = []
|
||||||
if row.not_for_sale:
|
if product.not_for_sale:
|
||||||
classes.append('not-for-sale')
|
classes.append('not-for-sale')
|
||||||
if row.deleted:
|
if product.deleted:
|
||||||
classes.append('deleted')
|
classes.append('deleted')
|
||||||
if classes:
|
if classes:
|
||||||
attrs['class_'] = ' '.join(classes)
|
return ' '.join(classes)
|
||||||
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
def get_instance(self):
|
def get_instance(self):
|
||||||
key = self.request.matchdict['uuid']
|
key = self.request.matchdict['uuid']
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8; -*-
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
|
@ -29,4 +29,4 @@ from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
config.include('tailbone.views.purchases.core')
|
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 import forms
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.views import MasterView
|
from tailbone.views import MasterView2 as MasterView
|
||||||
|
|
||||||
|
|
||||||
class BatchesFieldRenderer(fa.FieldRenderer):
|
class BatchesFieldRenderer(fa.FieldRenderer):
|
||||||
|
@ -73,6 +73,32 @@ class PurchaseView(MasterView):
|
||||||
model_row_class = model.PurchaseItem
|
model_row_class = model.PurchaseItem
|
||||||
row_model_title = 'Purchase Item'
|
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):
|
def get_instance_title(self, purchase):
|
||||||
if purchase.status >= self.enum.PURCHASE_STATUS_COSTED:
|
if purchase.status >= self.enum.PURCHASE_STATUS_COSTED:
|
||||||
if purchase.invoice_date:
|
if purchase.invoice_date:
|
||||||
|
@ -90,7 +116,9 @@ class PurchaseView(MasterView):
|
||||||
return "{} (ordered)".format(purchase.vendor)
|
return "{} (ordered)".format(purchase.vendor)
|
||||||
return unicode(purchase)
|
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.joiners['store'] = lambda q: q.join(model.Store)
|
||||||
g.filters['store'] = g.make_filter('store', model.Store.name)
|
g.filters['store'] = g.make_filter('store', model.Store.name)
|
||||||
g.sorters['store'] = g.make_sorter(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_sortkey = 'date_ordered'
|
||||||
g.default_sortdir = 'desc'
|
g.default_sortdir = 'desc'
|
||||||
|
|
||||||
g.date_ordered.set(label="Ordered")
|
g.set_enum('status', self.enum.PURCHASE_STATUS)
|
||||||
g.date_received.set(label="Received")
|
|
||||||
g.invoice_number.set(label="Invoice No.")
|
|
||||||
g.status.set(renderer=forms.renderers.EnumFieldRenderer(self.enum.PURCHASE_STATUS))
|
|
||||||
|
|
||||||
def configure_grid(self, g):
|
g.set_label('date_ordered', "Ordered")
|
||||||
g.configure(
|
g.set_label('date_received', "Received")
|
||||||
include=[
|
g.set_label('invoice_number', "Invoice No.")
|
||||||
g.store,
|
|
||||||
g.vendor,
|
|
||||||
g.department,
|
|
||||||
g.buyer,
|
|
||||||
g.date_ordered,
|
|
||||||
g.date_received,
|
|
||||||
g.invoice_number,
|
|
||||||
g.status,
|
|
||||||
],
|
|
||||||
readonly=True)
|
|
||||||
|
|
||||||
def _preconfigure_fieldset(self, fs):
|
def _preconfigure_fieldset(self, fs):
|
||||||
fs.store.set(renderer=forms.renderers.StoreFieldRenderer)
|
fs.store.set(renderer=forms.renderers.StoreFieldRenderer)
|
||||||
|
@ -189,43 +204,36 @@ class PurchaseView(MasterView):
|
||||||
return Session.query(model.PurchaseItem)\
|
return Session.query(model.PurchaseItem)\
|
||||||
.filter(model.PurchaseItem.purchase == purchase)
|
.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):
|
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()
|
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:
|
if purchase.status == self.enum.PURCHASE_STATUS_ORDERED:
|
||||||
del g.cases_received
|
g.hide_column('cases_received')
|
||||||
del g.units_received
|
g.hide_column('units_received')
|
||||||
del g.invoice_total
|
g.hide_column('invoice_total')
|
||||||
elif purchase.status in (self.enum.PURCHASE_STATUS_RECEIVED,
|
elif purchase.status in (self.enum.PURCHASE_STATUS_RECEIVED,
|
||||||
self.enum.PURCHASE_STATUS_COSTED):
|
self.enum.PURCHASE_STATUS_COSTED):
|
||||||
del g.po_total
|
g.hide_column('po_total')
|
||||||
|
|
||||||
def _preconfigure_row_fieldset(self, fs):
|
def _preconfigure_row_fieldset(self, fs):
|
||||||
fs.vendor_code.set(label="Vendor Item Code")
|
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 rattail.db import model
|
||||||
|
|
||||||
from tailbone import forms
|
from tailbone import forms
|
||||||
from tailbone.views import MasterView
|
from tailbone.views import MasterView2 as MasterView
|
||||||
|
|
||||||
|
|
||||||
class PurchaseCreditView(MasterView):
|
class PurchaseCreditView(MasterView):
|
||||||
|
@ -42,7 +42,21 @@ class PurchaseCreditView(MasterView):
|
||||||
creatable = False
|
creatable = False
|
||||||
editable = 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.joiners['vendor'] = lambda q: q.outerjoin(model.Vendor)
|
||||||
g.sorters['vendor'] = g.make_sorter(model.Vendor.name)
|
g.sorters['vendor'] = g.make_sorter(model.Vendor.name)
|
||||||
|
@ -50,28 +64,15 @@ class PurchaseCreditView(MasterView):
|
||||||
g.default_sortkey = 'date_received'
|
g.default_sortkey = 'date_received'
|
||||||
g.default_sortdir = 'desc'
|
g.default_sortdir = 'desc'
|
||||||
|
|
||||||
g.upc.set(label="UPC")
|
g.set_type('cases_shorted', 'quantity')
|
||||||
g.brand_name.set(label="Brand")
|
g.set_type('units_shorted', 'quantity')
|
||||||
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")
|
|
||||||
|
|
||||||
def configure_grid(self, g):
|
g.set_label('upc', "UPC")
|
||||||
g.configure(
|
g.set_label('brand_name', "Brand")
|
||||||
include=[
|
g.set_label('cases_shorted', "Cases")
|
||||||
g.vendor,
|
g.set_label('units_shorted', "Units")
|
||||||
g.upc,
|
g.set_label('credit_type', "Type")
|
||||||
g.brand_name,
|
g.set_label('date_received', "Date")
|
||||||
g.description,
|
|
||||||
g.size,
|
|
||||||
g.cases_shorted,
|
|
||||||
g.units_shorted,
|
|
||||||
g.credit_type,
|
|
||||||
g.date_received,
|
|
||||||
g.status,
|
|
||||||
],
|
|
||||||
readonly=True)
|
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
|
|
|
@ -36,7 +36,7 @@ import formalchemy as fa
|
||||||
from pyramid import httpexceptions
|
from pyramid import httpexceptions
|
||||||
|
|
||||||
from tailbone import forms
|
from tailbone import forms
|
||||||
from tailbone.views.batch import BatchMasterView
|
from tailbone.views.batch import BatchMasterView2 as BatchMasterView
|
||||||
|
|
||||||
|
|
||||||
class PurchasingBatchView(BatchMasterView):
|
class PurchasingBatchView(BatchMasterView):
|
||||||
|
@ -47,6 +47,34 @@ class PurchasingBatchView(BatchMasterView):
|
||||||
model_row_class = model.PurchaseBatchRow
|
model_row_class = model.PurchaseBatchRow
|
||||||
default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler'
|
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
|
@property
|
||||||
def batch_mode(self):
|
def batch_mode(self):
|
||||||
raise NotImplementedError("Please define `batch_mode` for your purchasing batch view")
|
raise NotImplementedError("Please define `batch_mode` for your purchasing batch view")
|
||||||
|
@ -55,9 +83,8 @@ class PurchasingBatchView(BatchMasterView):
|
||||||
return session.query(model.PurchaseBatch)\
|
return session.query(model.PurchaseBatch)\
|
||||||
.filter(model.PurchaseBatch.mode == self.batch_mode)
|
.filter(model.PurchaseBatch.mode == self.batch_mode)
|
||||||
|
|
||||||
def _preconfigure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
super(PurchasingBatchView, self)._preconfigure_grid(g)
|
super(PurchasingBatchView, self).configure_grid(g)
|
||||||
del g.filters['mode']
|
|
||||||
|
|
||||||
g.joiners['vendor'] = lambda q: q.join(model.Vendor)
|
g.joiners['vendor'] = lambda q: q.join(model.Vendor)
|
||||||
g.filters['vendor'] = g.make_filter('vendor', model.Vendor.name,
|
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_active = True
|
||||||
g.filters['complete'].default_verb = 'is_true'
|
g.filters['complete'].default_verb = 'is_true'
|
||||||
|
|
||||||
g.date_ordered.set(label="Ordered")
|
g.set_label('date_ordered', "Ordered")
|
||||||
g.date_received.set(label="Received")
|
g.set_label('date_received', "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)
|
|
||||||
|
|
||||||
# def make_form(self, batch, **kwargs):
|
# def make_form(self, batch, **kwargs):
|
||||||
# if self.creating:
|
# if self.creating:
|
||||||
|
@ -299,65 +312,36 @@ class PurchasingBatchView(BatchMasterView):
|
||||||
# query = super(PurchasingBatchView, self).get_row_data(batch)
|
# query = super(PurchasingBatchView, self).get_row_data(batch)
|
||||||
# return query.options(orm.joinedload(model.PurchaseBatchRow.credits))
|
# return query.options(orm.joinedload(model.PurchaseBatchRow.credits))
|
||||||
|
|
||||||
def _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):
|
def configure_row_grid(self, g):
|
||||||
batch = self.get_instance()
|
super(PurchasingBatchView, self).configure_row_grid(g)
|
||||||
|
|
||||||
g.configure(
|
g.set_type('upc', 'gpc')
|
||||||
include=[
|
g.set_type('cases_ordered', 'quantity')
|
||||||
g.sequence,
|
g.set_type('units_ordered', 'quantity')
|
||||||
g.upc,
|
g.set_type('cases_received', 'quantity')
|
||||||
# g.item_id,
|
g.set_type('units_received', 'quantity')
|
||||||
g.brand_name,
|
g.set_type('po_total', 'currency')
|
||||||
g.description,
|
g.set_type('invoice_total', 'currency')
|
||||||
g.size,
|
g.set_type('credits', 'boolean')
|
||||||
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)
|
|
||||||
|
|
||||||
if batch.mode == self.enum.PURCHASE_BATCH_MODE_ORDERING:
|
g.set_label('upc', "UPC")
|
||||||
del g.cases_received
|
g.set_label('brand_name', "Brand")
|
||||||
del g.units_received
|
g.set_label('cases_ordered', "Cases Ord.")
|
||||||
del g.has_credits
|
g.set_label('units_ordered', "Units Ord.")
|
||||||
del g.invoice_total
|
g.set_label('cases_received', "Cases Rec.")
|
||||||
elif batch.mode in (self.enum.PURCHASE_BATCH_MODE_RECEIVING,
|
g.set_label('units_received', "Units Rec.")
|
||||||
self.enum.PURCHASE_BATCH_MODE_COSTING):
|
g.set_label('po_total', "Total")
|
||||||
del g.po_total
|
g.set_label('invoice_total', "Total")
|
||||||
|
g.set_label('credits', "Credits?")
|
||||||
|
|
||||||
def make_row_grid_tools(self, batch):
|
def make_row_grid_tools(self, batch):
|
||||||
return self.make_default_row_grid_tools(batch)
|
return self.make_default_row_grid_tools(batch)
|
||||||
|
|
||||||
def row_grid_row_attrs(self, row, i):
|
def row_grid_extra_class(self, row, i):
|
||||||
attrs = {}
|
|
||||||
if row.status_code == row.STATUS_PRODUCT_NOT_FOUND:
|
if row.status_code == row.STATUS_PRODUCT_NOT_FOUND:
|
||||||
attrs['class_'] = 'warning'
|
return 'warning'
|
||||||
elif row.status_code in (row.STATUS_INCOMPLETE,
|
if row.status_code in (row.STATUS_INCOMPLETE, row.STATUS_ORDERED_RECEIVED_DIFFER):
|
||||||
row.STATUS_ORDERED_RECEIVED_DIFFER):
|
return 'notice'
|
||||||
attrs['class_'] = 'notice'
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
def _preconfigure_row_fieldset(self, fs):
|
def _preconfigure_row_fieldset(self, fs):
|
||||||
super(PurchasingBatchView, self)._preconfigure_row_fieldset(fs)
|
super(PurchasingBatchView, self)._preconfigure_row_fieldset(fs)
|
||||||
|
|
|
@ -51,6 +51,23 @@ class OrderingBatchView(PurchasingBatchView):
|
||||||
model_title = "Ordering Batch"
|
model_title = "Ordering Batch"
|
||||||
model_title_plural = "Ordering Batches"
|
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 = [
|
order_form_header_columns = [
|
||||||
"UPC",
|
"UPC",
|
||||||
"Brand",
|
"Brand",
|
||||||
|
|
|
@ -39,7 +39,7 @@ import formalchemy as fa
|
||||||
import formencode as fe
|
import formencode as fe
|
||||||
from webhelpers2.html import tags
|
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
|
from tailbone.views.purchasing import PurchasingBatchView
|
||||||
|
|
||||||
|
|
||||||
|
@ -93,10 +93,36 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
mobile_creatable = True
|
mobile_creatable = True
|
||||||
mobile_rows_filterable = 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
|
@property
|
||||||
def batch_mode(self):
|
def batch_mode(self):
|
||||||
return self.enum.PURCHASE_BATCH_MODE_RECEIVING
|
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):
|
def make_mobile_row_filters(self):
|
||||||
"""
|
"""
|
||||||
Returns a set of filters for the mobile row grid.
|
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
|
kwargs['sms_transaction_number'] = batch.sms_transaction_number
|
||||||
return kwargs
|
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):
|
def configure_mobile_fieldset(self, fs):
|
||||||
fs.configure(include=[
|
fs.configure(include=[
|
||||||
fs.vendor.with_renderer(fa.TextFieldRenderer),
|
fs.vendor.with_renderer(fa.TextFieldRenderer),
|
||||||
|
@ -178,13 +200,9 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
else:
|
else:
|
||||||
del fs.complete
|
del fs.complete
|
||||||
|
|
||||||
def render_mobile_row_listitem(self, row, **kwargs):
|
def render_mobile_row_listitem(self, row, i):
|
||||||
if row is None:
|
|
||||||
return ''
|
|
||||||
description = row.product.full_description if row.product else row.description
|
description = row.product.full_description if row.product else row.description
|
||||||
title = "({}) {}".format(row.upc.pretty(), description)
|
return "({}) {}".format(row.upc.pretty(), description)
|
||||||
url = self.request.route_url('mobile.receiving.rows.view', uuid=row.uuid)
|
|
||||||
return tags.link_to(title, url)
|
|
||||||
|
|
||||||
# TODO: this view can create new rows, with only a GET query. that should
|
# 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
|
# probably be changed to require POST; for now we just require the "create
|
||||||
|
@ -344,20 +362,6 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
cls._defaults(config)
|
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):
|
class ValidBatchRow(forms.validators.ModelValidator):
|
||||||
model_class = model.PurchaseBatchRow
|
model_class = model.PurchaseBatchRow
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8; -*-
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
|
@ -26,10 +26,10 @@ Report Code Views
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
from tailbone.views import MasterView
|
|
||||||
|
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
|
|
||||||
|
from tailbone.views import MasterView2 as MasterView
|
||||||
|
|
||||||
|
|
||||||
class ReportCodesView(MasterView):
|
class ReportCodesView(MasterView):
|
||||||
"""
|
"""
|
||||||
|
@ -38,16 +38,16 @@ class ReportCodesView(MasterView):
|
||||||
model_class = model.ReportCode
|
model_class = model.ReportCode
|
||||||
model_title = "Report Code"
|
model_title = "Report Code"
|
||||||
|
|
||||||
|
grid_columns = [
|
||||||
|
'code',
|
||||||
|
'name',
|
||||||
|
]
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
|
super(ReportCodesView, self).configure_grid(g)
|
||||||
g.filters['name'].default_active = True
|
g.filters['name'].default_active = True
|
||||||
g.filters['name'].default_verb = 'contains'
|
g.filters['name'].default_verb = 'contains'
|
||||||
g.default_sortkey = 'code'
|
g.default_sortkey = 'code'
|
||||||
g.configure(
|
|
||||||
include=[
|
|
||||||
g.code,
|
|
||||||
g.name,
|
|
||||||
],
|
|
||||||
readonly=True)
|
|
||||||
|
|
||||||
def configure_fieldset(self, fs):
|
def configure_fieldset(self, fs):
|
||||||
fs.configure(
|
fs.configure(
|
||||||
|
|
|
@ -204,16 +204,13 @@ class ReportOutputView(ExportMasterView):
|
||||||
url_prefix = '/reports/generated'
|
url_prefix = '/reports/generated'
|
||||||
downloadable = True
|
downloadable = True
|
||||||
|
|
||||||
def configure_grid(self, g):
|
grid_columns = [
|
||||||
g.configure(
|
'id',
|
||||||
include=[
|
'report_name',
|
||||||
g.id,
|
'filename',
|
||||||
g.report_name,
|
'created',
|
||||||
g.filename,
|
'created_by',
|
||||||
g.created,
|
]
|
||||||
g.created_by,
|
|
||||||
],
|
|
||||||
readonly=True)
|
|
||||||
|
|
||||||
def _preconfigure_fieldset(self, fs):
|
def _preconfigure_fieldset(self, fs):
|
||||||
super(ReportOutputView, self)._preconfigure_fieldset(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
|
import formalchemy as fa
|
||||||
from formalchemy.fields import IntegerFieldRenderer
|
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.db import Session
|
||||||
|
from tailbone.newgrids import AlchemyGrid
|
||||||
from tailbone.views.principal import PrincipalMasterView
|
from tailbone.views.principal import PrincipalMasterView
|
||||||
|
|
||||||
|
|
||||||
|
@ -45,19 +46,17 @@ class RolesView(PrincipalMasterView):
|
||||||
"""
|
"""
|
||||||
model_class = model.Role
|
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_active = True
|
||||||
g.filters['name'].default_verb = 'contains'
|
g.filters['name'].default_verb = 'contains'
|
||||||
g.default_sortkey = 'name'
|
g.default_sortkey = 'name'
|
||||||
|
|
||||||
def configure_grid(self, g):
|
|
||||||
g.configure(
|
|
||||||
include=[
|
|
||||||
g.name,
|
|
||||||
g.session_timeout,
|
|
||||||
],
|
|
||||||
readonly=True)
|
|
||||||
|
|
||||||
def _preconfigure_fieldset(self, fs):
|
def _preconfigure_fieldset(self, fs):
|
||||||
fs.append(PermissionsField('permissions'))
|
fs.append(PermissionsField('permissions'))
|
||||||
permissions = self.request.registry.settings.get('tailbone_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
|
# for this one, for instance (no settings required), but there is
|
||||||
# plenty of room for improvement here.
|
# plenty of room for improvement here.
|
||||||
users = sorted(role.users, key=lambda u: u.username)
|
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=[
|
main_actions=[
|
||||||
grids.GridAction('view', icon='zoomin',
|
grids.GridAction('view', icon='zoomin',
|
||||||
url=lambda r, i: self.request.route_url('users.view', uuid=r.uuid)),
|
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
|
import formalchemy as fa
|
||||||
|
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.views import MasterView
|
from tailbone.views import MasterView2 as MasterView
|
||||||
|
|
||||||
|
|
||||||
def unique_name(value, field):
|
def unique_name(value, field):
|
||||||
|
@ -48,17 +48,16 @@ class SettingsView(MasterView):
|
||||||
"""
|
"""
|
||||||
model_class = model.Setting
|
model_class = model.Setting
|
||||||
feedback = re.compile(r'^rattail\.mail\.user_feedback\..*')
|
feedback = re.compile(r'^rattail\.mail\.user_feedback\..*')
|
||||||
|
grid_columns = [
|
||||||
|
'name',
|
||||||
|
'value',
|
||||||
|
]
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
g.filters['name'].default_active = True
|
g.filters['name'].default_active = True
|
||||||
g.filters['name'].default_verb = 'contains'
|
g.filters['name'].default_verb = 'contains'
|
||||||
g.default_sortkey = 'name'
|
g.default_sortkey = 'name'
|
||||||
g.configure(
|
g.set_link('name')
|
||||||
include=[
|
|
||||||
g.name,
|
|
||||||
g.value,
|
|
||||||
],
|
|
||||||
readonly=True)
|
|
||||||
|
|
||||||
def _preconfigure_fieldset(self, fs):
|
def _preconfigure_fieldset(self, fs):
|
||||||
fs.name.set(validate=unique_name)
|
fs.name.set(validate=unique_name)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8; -*-
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
|
@ -36,7 +36,7 @@ from rattail.time import localtime
|
||||||
import formalchemy
|
import formalchemy
|
||||||
|
|
||||||
from tailbone import forms
|
from tailbone import forms
|
||||||
from tailbone.views import MasterView
|
from tailbone.views import MasterView2 as MasterView
|
||||||
|
|
||||||
|
|
||||||
class ShiftLengthField(formalchemy.Field):
|
class ShiftLengthField(formalchemy.Field):
|
||||||
|
@ -53,6 +53,14 @@ class ShiftLengthField(formalchemy.Field):
|
||||||
return humanize.naturaldelta(shift.end_time - shift.start_time)
|
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):
|
class ScheduledShiftsView(MasterView):
|
||||||
"""
|
"""
|
||||||
Master view for employee scheduled shifts.
|
Master view for employee scheduled shifts.
|
||||||
|
@ -60,24 +68,25 @@ class ScheduledShiftsView(MasterView):
|
||||||
model_class = model.ScheduledShift
|
model_class = model.ScheduledShift
|
||||||
url_prefix = '/shifts/scheduled'
|
url_prefix = '/shifts/scheduled'
|
||||||
|
|
||||||
|
grid_columns = [
|
||||||
|
'employee',
|
||||||
|
'store',
|
||||||
|
'start_time',
|
||||||
|
'end_time',
|
||||||
|
'length',
|
||||||
|
]
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
g.joiners['employee'] = lambda q: q.join(model.Employee).join(model.Person)
|
g.joiners['employee'] = lambda q: q.join(model.Employee).join(model.Person)
|
||||||
g.filters['employee'] = g.make_filter('employee', model.Person.display_name,
|
g.filters['employee'] = g.make_filter('employee', model.Person.display_name,
|
||||||
default_active=True, default_verb='contains',
|
default_active=True, default_verb='contains')
|
||||||
label="Employee Name")
|
|
||||||
|
|
||||||
g.default_sortkey = 'start_time'
|
g.default_sortkey = 'start_time'
|
||||||
g.default_sortdir = 'desc'
|
g.default_sortdir = 'desc'
|
||||||
g.append(ShiftLengthField('length'))
|
|
||||||
g.configure(
|
g.set_renderer('length', render_shift_length)
|
||||||
include=[
|
|
||||||
g.employee,
|
g.set_label('employee', "Employee Name")
|
||||||
g.store,
|
|
||||||
g.start_time,
|
|
||||||
g.end_time,
|
|
||||||
g.length,
|
|
||||||
],
|
|
||||||
readonly=True)
|
|
||||||
|
|
||||||
def configure_fieldset(self, fs):
|
def configure_fieldset(self, fs):
|
||||||
fs.append(ShiftLengthField('length'))
|
fs.append(ShiftLengthField('length'))
|
||||||
|
@ -98,37 +107,37 @@ class WorkedShiftsView(MasterView):
|
||||||
model_class = model.WorkedShift
|
model_class = model.WorkedShift
|
||||||
url_prefix = '/shifts/worked'
|
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.joiners['employee'] = lambda q: q.join(model.Employee).join(model.Person)
|
||||||
g.filters['employee'] = g.make_filter('employee', model.Person.display_name,
|
g.filters['employee'] = g.make_filter('employee', model.Person.display_name)
|
||||||
label="Employee Name")
|
|
||||||
g.sorters['employee'] = g.make_sorter(model.Person.display_name)
|
g.sorters['employee'] = g.make_sorter(model.Person.display_name)
|
||||||
|
|
||||||
g.joiners['store'] = lambda q: q.join(model.Store)
|
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.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
|
# TODO: these sorters should be automatic once we fix the schema
|
||||||
g.sorters['start_time'] = g.make_sorter(model.WorkedShift.punch_in)
|
g.sorters['start_time'] = g.make_sorter(model.WorkedShift.punch_in)
|
||||||
g.sorters['end_time'] = g.make_sorter(model.WorkedShift.punch_out)
|
g.sorters['end_time'] = g.make_sorter(model.WorkedShift.punch_out)
|
||||||
g.default_sortkey = 'start_time'
|
g.default_sortkey = 'start_time'
|
||||||
g.default_sortdir = 'desc'
|
g.default_sortdir = 'desc'
|
||||||
|
|
||||||
g.append(ShiftLengthField('length'))
|
g.set_renderer('length', render_shift_length)
|
||||||
|
|
||||||
def configure_grid(self, g):
|
g.set_label('employee', "Employee Name")
|
||||||
g.configure(
|
g.set_label('store', "Store Name")
|
||||||
include=[
|
g.set_label('punch_in', "Start Time")
|
||||||
g.employee,
|
g.set_label('punch_out', "End Time")
|
||||||
g.store,
|
|
||||||
g.start_time,
|
|
||||||
g.end_time,
|
|
||||||
g.length,
|
|
||||||
],
|
|
||||||
readonly=True)
|
|
||||||
|
|
||||||
def get_instance_title(self, shift):
|
def get_instance_title(self, shift):
|
||||||
time = shift.start_time or shift.end_time
|
time = shift.start_time or shift.end_time
|
||||||
|
|
|
@ -30,7 +30,7 @@ import sqlalchemy as sa
|
||||||
|
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
|
|
||||||
from tailbone.views import MasterView
|
from tailbone.views import MasterView2 as MasterView
|
||||||
|
|
||||||
|
|
||||||
class StoresView(MasterView):
|
class StoresView(MasterView):
|
||||||
|
@ -39,6 +39,12 @@ class StoresView(MasterView):
|
||||||
"""
|
"""
|
||||||
model_class = model.Store
|
model_class = model.Store
|
||||||
has_versions = True
|
has_versions = True
|
||||||
|
grid_columns = [
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'phone',
|
||||||
|
'email',
|
||||||
|
]
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
|
|
||||||
|
@ -49,28 +55,21 @@ class StoresView(MasterView):
|
||||||
model.StorePhoneNumber.parent_uuid == model.Store.uuid,
|
model.StorePhoneNumber.parent_uuid == model.Store.uuid,
|
||||||
model.StorePhoneNumber.preference == 1))
|
model.StorePhoneNumber.preference == 1))
|
||||||
|
|
||||||
g.filters['email'] = g.make_filter('email', model.StoreEmailAddress.address,
|
g.filters['phone'] = g.make_filter('phone', model.StorePhoneNumber.number)
|
||||||
label="Email Address")
|
g.filters['email'] = g.make_filter('email', model.StoreEmailAddress.address)
|
||||||
g.filters['phone'] = g.make_filter('phone', model.StorePhoneNumber.number,
|
|
||||||
label="Phone Number")
|
|
||||||
|
|
||||||
g.filters['name'].default_active = True
|
g.filters['name'].default_active = True
|
||||||
g.filters['name'].default_verb = 'contains'
|
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.default_sortkey = 'id'
|
||||||
|
|
||||||
g.configure(
|
g.set_link('id')
|
||||||
include=[
|
g.set_link('name')
|
||||||
g.id.label("ID"),
|
|
||||||
g.name,
|
g.set_label('id', "ID")
|
||||||
g.phone.label("Phone Number"),
|
g.set_label('phone', "Phone Number")
|
||||||
g.email.label("Email Address"),
|
g.set_label('email', "Email Address")
|
||||||
],
|
|
||||||
readonly=True)
|
|
||||||
|
|
||||||
def configure_fieldset(self, fs):
|
def configure_fieldset(self, fs):
|
||||||
fs.configure(
|
fs.configure(
|
||||||
|
|
|
@ -29,7 +29,7 @@ from __future__ import unicode_literals, absolute_import
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
|
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.views import MasterView
|
from tailbone.views import MasterView2 as MasterView
|
||||||
|
|
||||||
|
|
||||||
class SubdepartmentsView(MasterView):
|
class SubdepartmentsView(MasterView):
|
||||||
|
@ -37,6 +37,13 @@ class SubdepartmentsView(MasterView):
|
||||||
Master view for the Subdepartment class.
|
Master view for the Subdepartment class.
|
||||||
"""
|
"""
|
||||||
model_class = model.Subdepartment
|
model_class = model.Subdepartment
|
||||||
|
|
||||||
|
grid_columns = [
|
||||||
|
'number',
|
||||||
|
'name',
|
||||||
|
'department',
|
||||||
|
]
|
||||||
|
|
||||||
mergeable = True
|
mergeable = True
|
||||||
merge_additive_fields = [
|
merge_additive_fields = [
|
||||||
'product_count',
|
'product_count',
|
||||||
|
@ -49,16 +56,10 @@ class SubdepartmentsView(MasterView):
|
||||||
]
|
]
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
|
super(SubdepartmentsView, self).configure_grid(g)
|
||||||
g.filters['name'].default_active = True
|
g.filters['name'].default_active = True
|
||||||
g.filters['name'].default_verb = 'contains'
|
g.filters['name'].default_verb = 'contains'
|
||||||
g.default_sortkey = 'name'
|
g.default_sortkey = 'name'
|
||||||
g.configure(
|
|
||||||
include=[
|
|
||||||
g.number,
|
|
||||||
g.name,
|
|
||||||
g.department,
|
|
||||||
],
|
|
||||||
readonly=True)
|
|
||||||
|
|
||||||
def configure_fieldset(self, fs):
|
def configure_fieldset(self, fs):
|
||||||
fs.configure(
|
fs.configure(
|
||||||
|
|
|
@ -26,8 +26,7 @@ Views with info about the underlying Rattail tables
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
from tailbone import newgrids as grids
|
from tailbone.views import MasterView2 as MasterView
|
||||||
from tailbone.views import MasterView
|
|
||||||
|
|
||||||
|
|
||||||
class TablesView(MasterView):
|
class TablesView(MasterView):
|
||||||
|
@ -41,10 +40,14 @@ class TablesView(MasterView):
|
||||||
editable = False
|
editable = False
|
||||||
deletable = False
|
deletable = False
|
||||||
viewable = False
|
viewable = False
|
||||||
grid_factory = grids.Grid
|
|
||||||
filterable = False
|
filterable = False
|
||||||
pageable = False
|
pageable = False
|
||||||
|
|
||||||
|
grid_columns = [
|
||||||
|
'name',
|
||||||
|
'row_count',
|
||||||
|
]
|
||||||
|
|
||||||
def get_data(self, **kwargs):
|
def get_data(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Fetch existing table names and estimate row counts via PG SQL
|
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]
|
return [dict(name=row[1], row_count=row[2]) for row in result]
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
g.columns = [
|
g.sorters['name'] = g.make_simple_sorter('name', foldcase=True)
|
||||||
grids.GridColumn('name'),
|
g.sorters['row_count'] = g.make_simple_sorter('row_count')
|
||||||
grids.GridColumn('row_count'),
|
|
||||||
]
|
|
||||||
|
|
||||||
g.sorters['name'] = g.make_sorter('name', foldcase=True)
|
|
||||||
g.sorters['row_count'] = g.make_sorter('row_count')
|
|
||||||
g.default_sortkey = 'name'
|
g.default_sortkey = 'name'
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -69,25 +69,27 @@ class TempmonClientView(MasterView):
|
||||||
route_prefix = 'tempmon.clients'
|
route_prefix = 'tempmon.clients'
|
||||||
url_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_active = True
|
||||||
g.filters['hostname'].default_verb = 'contains'
|
g.filters['hostname'].default_verb = 'contains'
|
||||||
g.filters['location'].default_active = True
|
g.filters['location'].default_active = True
|
||||||
g.filters['location'].default_verb = 'contains'
|
g.filters['location'].default_verb = 'contains'
|
||||||
g.default_sortkey = 'config_key'
|
g.default_sortkey = 'config_key'
|
||||||
g.config_key.set(label="Key")
|
|
||||||
|
|
||||||
def configure_grid(self, g):
|
g.set_type('enabled', 'boolean')
|
||||||
g.configure(
|
g.set_type('online', 'boolean')
|
||||||
include=[
|
|
||||||
g.config_key,
|
g.set_label('config_key', "Key")
|
||||||
g.hostname,
|
|
||||||
g.location,
|
|
||||||
g.delay,
|
|
||||||
g.enabled,
|
|
||||||
g.online,
|
|
||||||
],
|
|
||||||
readonly=True)
|
|
||||||
|
|
||||||
def _preconfigure_fieldset(self, fs):
|
def _preconfigure_fieldset(self, fs):
|
||||||
fs.config_key.set(validate=unique_config_key)
|
fs.config_key.set(validate=unique_config_key)
|
||||||
|
|
|
@ -33,7 +33,7 @@ from tailbone import views
|
||||||
from tailbone.db import TempmonSession
|
from tailbone.db import TempmonSession
|
||||||
|
|
||||||
|
|
||||||
class MasterView(views.MasterView):
|
class MasterView(views.MasterView2):
|
||||||
"""
|
"""
|
||||||
Base class for tempmon views.
|
Base class for tempmon views.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -55,26 +55,29 @@ class TempmonProbeView(MasterView):
|
||||||
route_prefix = 'tempmon.probes'
|
route_prefix = 'tempmon.probes'
|
||||||
url_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.joiners['client'] = lambda q: q.join(tempmon.Client)
|
||||||
g.sorters['client'] = g.make_sorter(tempmon.Client.config_key)
|
g.sorters['client'] = g.make_sorter(tempmon.Client.config_key)
|
||||||
g.default_sortkey = 'client'
|
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.set_enum('appliance_type', self.enum.TEMPMON_APPLIANCE_TYPE)
|
||||||
g.configure(
|
g.set_enum('status', self.enum.TEMPMON_PROBE_STATUS)
|
||||||
include=[
|
|
||||||
g.client,
|
g.set_type('enabled', 'boolean')
|
||||||
g.config_key,
|
|
||||||
g.appliance_type,
|
g.set_label('config_key', "Key")
|
||||||
g.description,
|
|
||||||
g.device_path,
|
|
||||||
g.enabled,
|
|
||||||
g.status,
|
|
||||||
],
|
|
||||||
readonly=True)
|
|
||||||
|
|
||||||
def _preconfigure_fieldset(self, fs):
|
def _preconfigure_fieldset(self, fs):
|
||||||
fs.config_key.set(validate=unique_config_key)
|
fs.config_key.set(validate=unique_config_key)
|
||||||
|
|
|
@ -47,39 +47,45 @@ class TempmonReadingView(MasterView):
|
||||||
creatable = False
|
creatable = False
|
||||||
editable = False
|
editable = False
|
||||||
|
|
||||||
|
grid_columns = [
|
||||||
|
'client_key',
|
||||||
|
'client_host',
|
||||||
|
'probe',
|
||||||
|
'taken',
|
||||||
|
'degrees_f',
|
||||||
|
]
|
||||||
|
|
||||||
def query(self, session):
|
def query(self, session):
|
||||||
return session.query(tempmon.Reading)\
|
return session.query(tempmon.Reading)\
|
||||||
.join(tempmon.Client)\
|
.join(tempmon.Client)\
|
||||||
.options(orm.joinedload(tempmon.Reading.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.sorters['client_key'] = g.make_sorter(tempmon.Client.config_key)
|
||||||
g.filters['client_key'] = g.make_filter('client_key', 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.sorters['client_host'] = g.make_sorter(tempmon.Client.hostname)
|
||||||
g.filters['client_host'] = g.make_filter('client_host', tempmon.Client.hostname)
|
g.filters['client_host'] = g.make_filter('client_host', tempmon.Client.hostname)
|
||||||
|
|
||||||
g.joiners['probe'] = lambda q: q.join(tempmon.Probe,
|
g.joiners['probe'] = lambda q: q.join(tempmon.Probe, tempmon.Probe.uuid == tempmon.Reading.probe_uuid)
|
||||||
tempmon.Probe.uuid == tempmon.Reading.probe_uuid)
|
|
||||||
g.sorters['probe'] = g.make_sorter(tempmon.Probe.description)
|
g.sorters['probe'] = g.make_sorter(tempmon.Probe.description)
|
||||||
g.filters['probe'] = g.make_filter('probe', tempmon.Probe.description)
|
g.filters['probe'] = g.make_filter('probe', tempmon.Probe.description)
|
||||||
|
|
||||||
g.default_sortkey = 'taken'
|
g.default_sortkey = 'taken'
|
||||||
g.default_sortdir = 'desc'
|
g.default_sortdir = 'desc'
|
||||||
|
|
||||||
def configure_grid(self, g):
|
g.set_type('taken', 'datetime')
|
||||||
g.configure(
|
|
||||||
include=[
|
g.set_renderer('client_key', self.render_client_key)
|
||||||
g.client_key,
|
g.set_renderer('client_host', self.render_client_host)
|
||||||
g.client_host,
|
|
||||||
g.probe,
|
def render_client_key(self, reading, column):
|
||||||
g.taken,
|
return reading.client.config_key
|
||||||
g.degrees_f,
|
|
||||||
],
|
def render_client_host(self, reading, column):
|
||||||
readonly=True)
|
return reading.client.hostname
|
||||||
|
|
||||||
def _preconfigure_fieldset(self, fs):
|
def _preconfigure_fieldset(self, fs):
|
||||||
fs.client.set(label="TempMon Client", renderer=ClientFieldRenderer)
|
fs.client.set(label="TempMon Client", renderer=ClientFieldRenderer)
|
||||||
|
|
|
@ -32,7 +32,7 @@ from rattail.time import localtime
|
||||||
|
|
||||||
from tailbone import forms
|
from tailbone import forms
|
||||||
from tailbone.db import TrainwreckSession
|
from tailbone.db import TrainwreckSession
|
||||||
from tailbone.views import MasterView
|
from tailbone.views import MasterView2 as MasterView
|
||||||
|
|
||||||
|
|
||||||
class TransactionView(MasterView):
|
class TransactionView(MasterView):
|
||||||
|
@ -49,11 +49,35 @@ class TransactionView(MasterView):
|
||||||
editable = False
|
editable = False
|
||||||
deletable = False
|
deletable = False
|
||||||
|
|
||||||
|
grid_columns = [
|
||||||
|
'start_time',
|
||||||
|
'system',
|
||||||
|
'terminal_id',
|
||||||
|
'receipt_number',
|
||||||
|
'customer_id',
|
||||||
|
'customer_name',
|
||||||
|
'total',
|
||||||
|
]
|
||||||
|
|
||||||
has_rows = True
|
has_rows = True
|
||||||
# model_row_class = trainwreck.TransactionItem
|
# model_row_class = trainwreck.TransactionItem
|
||||||
rows_default_pagesize = 100
|
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_active = True
|
||||||
g.filters['receipt_number'].default_verb = 'equal'
|
g.filters['receipt_number'].default_verb = 'equal'
|
||||||
g.filters['start_time'].default_active = True
|
g.filters['start_time'].default_active = True
|
||||||
|
@ -62,24 +86,11 @@ class TransactionView(MasterView):
|
||||||
g.default_sortkey = 'start_time'
|
g.default_sortkey = 'start_time'
|
||||||
g.default_sortdir = 'desc'
|
g.default_sortdir = 'desc'
|
||||||
|
|
||||||
g.system.set(renderer=forms.renderers.EnumFieldRenderer(self.enum.TRAINWRECK_SYSTEM))
|
g.set_enum('system', self.enum.TRAINWRECK_SYSTEM)
|
||||||
g.terminal_id.set(label="Terminal")
|
g.set_type('total', 'currency')
|
||||||
g.receipt_number.set(label="Receipt No.")
|
g.set_label('terminal_id', "Terminal")
|
||||||
g.customer_id.set(label="Customer ID")
|
g.set_label('receipt_number', "Receipt No.")
|
||||||
g.total.set(renderer=forms.renderers.CurrencyFieldRenderer)
|
g.set_label('customer_id', "Customer ID")
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
def _preconfigure_fieldset(self, fs):
|
def _preconfigure_fieldset(self, fs):
|
||||||
fs.system.set(renderer=forms.renderers.EnumFieldRenderer(self.enum.TRAINWRECK_SYSTEM))
|
fs.system.set(renderer=forms.renderers.EnumFieldRenderer(self.enum.TRAINWRECK_SYSTEM))
|
||||||
|
@ -117,32 +128,18 @@ class TransactionView(MasterView):
|
||||||
def get_parent(self, item):
|
def get_parent(self, item):
|
||||||
return item.transaction
|
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.default_sortkey = 'sequence'
|
||||||
|
|
||||||
g.item_id.set(label="Item ID")
|
g.set_type('unit_quantity', 'quantity')
|
||||||
g.department_number.set(label="Dept. No.")
|
g.set_type('subtotal', 'currency')
|
||||||
g.unit_quantity.set(renderer=forms.renderers.QuantityFieldRenderer)
|
g.set_type('discounted_subtotal', 'currency')
|
||||||
g.subtotal.set(renderer=forms.renderers.CurrencyFieldRenderer)
|
g.set_type('tax', 'currency')
|
||||||
g.discounted_subtotal.set(renderer=forms.renderers.CurrencyFieldRenderer)
|
g.set_type('total', 'currency')
|
||||||
g.tax.set(renderer=forms.renderers.CurrencyFieldRenderer)
|
|
||||||
g.total.set(renderer=forms.renderers.CurrencyFieldRenderer)
|
|
||||||
|
|
||||||
def configure_row_grid(self, g):
|
g.set_label('item_id', "Item ID")
|
||||||
g.configure(
|
g.set_label('department_number', "Dept. No.")
|
||||||
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)
|
|
||||||
|
|
||||||
def _preconfigure_row_fieldset(self, fs):
|
def _preconfigure_row_fieldset(self, fs):
|
||||||
fs.item_id.set(label="Item ID")
|
fs.item_id.set(label="Item ID")
|
||||||
|
|
|
@ -142,11 +142,18 @@ class UsersView(PrincipalMasterView):
|
||||||
'active',
|
'active',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
grid_columns = [
|
||||||
|
'username',
|
||||||
|
'person',
|
||||||
|
]
|
||||||
|
|
||||||
def query(self, session):
|
def query(self, session):
|
||||||
return session.query(model.User)\
|
return session.query(model.User)\
|
||||||
.options(orm.joinedload(model.User.person))
|
.options(orm.joinedload(model.User.person))
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
|
super(UsersView, self).configure_grid(g)
|
||||||
|
|
||||||
g.joiners['person'] = lambda q: q.outerjoin(model.Person)
|
g.joiners['person'] = lambda q: q.outerjoin(model.Person)
|
||||||
|
|
||||||
del g.filters['password']
|
del g.filters['password']
|
||||||
|
@ -155,7 +162,7 @@ class UsersView(PrincipalMasterView):
|
||||||
g.filters['username'].default_verb = 'contains'
|
g.filters['username'].default_verb = 'contains'
|
||||||
g.filters['active'].default_active = True
|
g.filters['active'].default_active = True
|
||||||
g.filters['active'].default_verb = 'is_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')
|
default_active=True, default_verb='contains')
|
||||||
g.filters['password'] = g.make_filter('password', model.User.password,
|
g.filters['password'] = g.make_filter('password', model.User.password,
|
||||||
verbs=['is_null', 'is_not_null'])
|
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.sorters['person'] = lambda q, d: q.order_by(getattr(model.Person.display_name, d)())
|
||||||
g.default_sortkey = 'username'
|
g.default_sortkey = 'username'
|
||||||
|
|
||||||
g.person.set(label="Person's Name")
|
g.set_label('person', "Person's Name")
|
||||||
g.configure(
|
|
||||||
include=[
|
|
||||||
g.username,
|
|
||||||
g.person,
|
|
||||||
],
|
|
||||||
readonly=True)
|
|
||||||
|
|
||||||
def _preconfigure_fieldset(self, fs):
|
def _preconfigure_fieldset(self, fs):
|
||||||
fs.username.set(renderer=forms.renderers.StrippedTextFieldRenderer, validate=unique_username)
|
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 import forms
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.views.batch import FileBatchMasterView
|
from tailbone.views.batch import FileBatchMasterView2 as FileBatchMasterView
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
@ -52,28 +52,40 @@ class VendorCatalogsView(FileBatchMasterView):
|
||||||
editable = False
|
editable = False
|
||||||
rows_bulk_deletable = True
|
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):
|
def get_parsers(self):
|
||||||
if not hasattr(self, 'parsers'):
|
if not hasattr(self, 'parsers'):
|
||||||
self.parsers = sorted(iter_catalog_parsers(), key=lambda p: p.display)
|
self.parsers = sorted(iter_catalog_parsers(), key=lambda p: p.display)
|
||||||
return self.parsers
|
return self.parsers
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
|
super(VendorCatalogsView, self).configure_grid(g)
|
||||||
g.joiners['vendor'] = lambda q: q.join(model.Vendor)
|
g.joiners['vendor'] = lambda q: q.join(model.Vendor)
|
||||||
g.filters['vendor'] = g.make_filter('vendor', model.Vendor.name,
|
g.filters['vendor'] = g.make_filter('vendor', model.Vendor.name,
|
||||||
default_active=True, default_verb='contains')
|
default_active=True, default_verb='contains')
|
||||||
g.sorters['vendor'] = g.make_sorter(model.Vendor.name)
|
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):
|
def get_instance_title(self, batch):
|
||||||
return unicode(batch.vendor)
|
return unicode(batch.vendor)
|
||||||
|
|
||||||
|
@ -115,28 +127,18 @@ class VendorCatalogsView(FileBatchMasterView):
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
def configure_row_grid(self, g):
|
def configure_row_grid(self, g):
|
||||||
g.configure(
|
super(VendorCatalogsView, self).configure_row_grid(g)
|
||||||
include=[
|
g.set_label('upc', "UPC")
|
||||||
g.sequence,
|
g.set_label('brand_name', "Brand")
|
||||||
g.upc.label("UPC"),
|
g.set_label('old_unit_cost', "Old Cost")
|
||||||
g.brand_name.label("Brand"),
|
g.set_label('unit_cost', "New Cost")
|
||||||
g.description,
|
g.set_label('unit_cost_diff', "Diff.")
|
||||||
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)
|
|
||||||
|
|
||||||
def row_grid_row_attrs(self, row, i):
|
def row_grid_extra_class(self, row, i):
|
||||||
attrs = {}
|
|
||||||
if row.status_code in (row.STATUS_NEW_COST, row.STATUS_UPDATE_COST):
|
|
||||||
attrs['class_'] = 'notice'
|
|
||||||
if row.status_code == row.STATUS_PRODUCT_NOT_FOUND:
|
if row.status_code == row.STATUS_PRODUCT_NOT_FOUND:
|
||||||
attrs['class_'] = 'warning'
|
return 'warning'
|
||||||
return attrs
|
if row.status_code in (row.STATUS_NEW_COST, row.STATUS_UPDATE_COST):
|
||||||
|
return 'notice'
|
||||||
|
|
||||||
def template_kwargs_create(self, **kwargs):
|
def template_kwargs_create(self, **kwargs):
|
||||||
parsers = self.get_parsers()
|
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 import forms
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.views import MasterView, AutocompleteView
|
from tailbone.views import MasterView2 as MasterView, AutocompleteView
|
||||||
|
|
||||||
|
|
||||||
class VendorsView(MasterView):
|
class VendorsView(MasterView):
|
||||||
|
@ -40,22 +40,24 @@ class VendorsView(MasterView):
|
||||||
model_class = model.Vendor
|
model_class = model.Vendor
|
||||||
has_versions = True
|
has_versions = True
|
||||||
|
|
||||||
|
grid_columns = [
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'phone',
|
||||||
|
'email',
|
||||||
|
'contact',
|
||||||
|
]
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
|
super(VendorsView, self).configure_grid(g)
|
||||||
|
|
||||||
g.filters['name'].default_active = True
|
g.filters['name'].default_active = True
|
||||||
g.filters['name'].default_verb = 'contains'
|
g.filters['name'].default_verb = 'contains'
|
||||||
g.filters['id'].label = "ID"
|
|
||||||
g.default_sortkey = 'name'
|
g.default_sortkey = 'name'
|
||||||
|
|
||||||
g.append(forms.AssociationProxyField('contact'))
|
g.set_label('id', "ID")
|
||||||
g.configure(
|
g.set_label('phone', "Phone Number")
|
||||||
include=[
|
g.set_label('email', "Email Address")
|
||||||
g.id.label("ID"),
|
|
||||||
g.name,
|
|
||||||
g.phone.label("Phone Number"),
|
|
||||||
g.email.label("Email Address"),
|
|
||||||
g.contact,
|
|
||||||
],
|
|
||||||
readonly=True)
|
|
||||||
|
|
||||||
def configure_fieldset(self, fs):
|
def configure_fieldset(self, fs):
|
||||||
fs.append(forms.AssociationProxyField('contact'))
|
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
|
# Rattail -- Retail Software Framework
|
||||||
|
@ -32,7 +32,7 @@ from rattail.vendors.invoices import iter_invoice_parsers, require_invoice_parse
|
||||||
import formalchemy
|
import formalchemy
|
||||||
|
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.views.batch import FileBatchMasterView
|
from tailbone.views.batch import FileBatchMasterView2 as FileBatchMasterView
|
||||||
|
|
||||||
|
|
||||||
class VendorInvoicesView(FileBatchMasterView):
|
class VendorInvoicesView(FileBatchMasterView):
|
||||||
|
@ -44,23 +44,36 @@ class VendorInvoicesView(FileBatchMasterView):
|
||||||
default_handler_spec = 'rattail.batch.vendorinvoice:VendorInvoiceHandler'
|
default_handler_spec = 'rattail.batch.vendorinvoice:VendorInvoiceHandler'
|
||||||
url_prefix = '/vendors/invoices'
|
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):
|
def get_instance_title(self, batch):
|
||||||
return unicode(batch.vendor)
|
return unicode(batch.vendor)
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
|
super(VendorInvoicesView, self).configure_grid(g)
|
||||||
g.joiners['vendor'] = lambda q: q.join(model.Vendor)
|
g.joiners['vendor'] = lambda q: q.join(model.Vendor)
|
||||||
g.filters['vendor'] = g.make_filter('vendor', model.Vendor.name,
|
g.filters['vendor'] = g.make_filter('vendor', model.Vendor.name,
|
||||||
default_active=True, default_verb='contains')
|
default_active=True, default_verb='contains')
|
||||||
g.sorters['vendor'] = g.make_sorter(model.Vendor.name)
|
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):
|
def configure_fieldset(self, fs):
|
||||||
fs.purchase_order_number.set(label=self.handler.po_number_title)
|
fs.purchase_order_number.set(label=self.handler.po_number_title)
|
||||||
|
@ -123,34 +136,21 @@ class VendorInvoicesView(FileBatchMasterView):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def configure_row_grid(self, g):
|
def configure_row_grid(self, g):
|
||||||
g.filters['upc'].label = "UPC"
|
super(VendorInvoicesView, self).configure_row_grid(g)
|
||||||
g.filters['brand_name'].label = "Brand"
|
g.set_label('upc', "UPC")
|
||||||
g.configure(
|
g.set_label('brand_name', "Brand")
|
||||||
include=[
|
g.set_label('shipped_cases', "Cases")
|
||||||
g.sequence,
|
g.set_label('shipped_units', "Units")
|
||||||
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)
|
|
||||||
|
|
||||||
def row_grid_row_attrs(self, row, i):
|
def row_grid_extra_class(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'
|
|
||||||
if row.status_code in (row.STATUS_NOT_IN_DB,
|
if row.status_code in (row.STATUS_NOT_IN_DB,
|
||||||
row.STATUS_COST_NOT_IN_DB,
|
row.STATUS_COST_NOT_IN_DB,
|
||||||
row.STATUS_NO_CASE_QUANTITY):
|
row.STATUS_NO_CASE_QUANTITY):
|
||||||
attrs['class_'] = 'warning'
|
return 'warning'
|
||||||
return attrs
|
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):
|
def includeme(config):
|
||||||
|
|
Loading…
Reference in a new issue