feat: add per-row css class support for grids
This commit is contained in:
parent
f6fb6957e3
commit
e332975ce9
|
@ -313,7 +313,7 @@ class Form:
|
||||||
self.set_fields(fields or self.get_fields())
|
self.set_fields(fields or self.get_fields())
|
||||||
|
|
||||||
# nb. this tracks grid JSON data for inclusion in page template
|
# nb. this tracks grid JSON data for inclusion in page template
|
||||||
self.grid_vue_data = OrderedDict()
|
self.grid_vue_context = OrderedDict()
|
||||||
|
|
||||||
def __contains__(self, name):
|
def __contains__(self, name):
|
||||||
"""
|
"""
|
||||||
|
@ -826,16 +826,16 @@ class Form:
|
||||||
output = render(template, context)
|
output = render(template, context)
|
||||||
return HTML.literal(output)
|
return HTML.literal(output)
|
||||||
|
|
||||||
def add_grid_vue_data(self, grid):
|
def add_grid_vue_context(self, grid):
|
||||||
""" """
|
""" """
|
||||||
if not grid.key:
|
if not grid.key:
|
||||||
raise ValueError("grid must have a key!")
|
raise ValueError("grid must have a key!")
|
||||||
|
|
||||||
if grid.key in self.grid_vue_data:
|
if grid.key in self.grid_vue_context:
|
||||||
log.warning("grid data with key '%s' already registered, "
|
log.warning("grid data with key '%s' already registered, "
|
||||||
"but will be replaced", grid.key)
|
"but will be replaced", grid.key)
|
||||||
|
|
||||||
self.grid_vue_data[grid.key] = grid.get_vue_data()
|
self.grid_vue_context[grid.key] = grid.get_vue_context()
|
||||||
|
|
||||||
def render_vue_field(
|
def render_vue_field(
|
||||||
self,
|
self,
|
||||||
|
|
|
@ -27,6 +27,7 @@ Base grid classes
|
||||||
import functools
|
import functools
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import warnings
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
@ -116,6 +117,26 @@ class Grid:
|
||||||
|
|
||||||
See also :meth:`set_renderer()`.
|
See also :meth:`set_renderer()`.
|
||||||
|
|
||||||
|
.. attribute:: row_class
|
||||||
|
|
||||||
|
This represents the CSS ``class`` attribute for a row within
|
||||||
|
the grid. Default is ``None``.
|
||||||
|
|
||||||
|
This can be a simple string, in which case the same class is
|
||||||
|
applied to all rows.
|
||||||
|
|
||||||
|
Or it can be a callable, which can then return different
|
||||||
|
class(es) depending on each row. The callable must take three
|
||||||
|
args: ``(obj, data, i)`` - for example::
|
||||||
|
|
||||||
|
def my_row_class(obj, data, i):
|
||||||
|
if obj.archived:
|
||||||
|
return 'poser-archived'
|
||||||
|
|
||||||
|
grid = Grid(request, key='foo', row_class=my_row_class)
|
||||||
|
|
||||||
|
See :meth:`get_row_class()` for more info.
|
||||||
|
|
||||||
.. attribute:: actions
|
.. attribute:: actions
|
||||||
|
|
||||||
List of :class:`GridAction` instances represenging action links
|
List of :class:`GridAction` instances represenging action links
|
||||||
|
@ -330,6 +351,7 @@ class Grid:
|
||||||
data=None,
|
data=None,
|
||||||
labels={},
|
labels={},
|
||||||
renderers={},
|
renderers={},
|
||||||
|
row_class=None,
|
||||||
actions=[],
|
actions=[],
|
||||||
linked_columns=[],
|
linked_columns=[],
|
||||||
sortable=False,
|
sortable=False,
|
||||||
|
@ -355,6 +377,7 @@ class Grid:
|
||||||
self.data = data
|
self.data = data
|
||||||
self.labels = labels or {}
|
self.labels = labels or {}
|
||||||
self.renderers = renderers or {}
|
self.renderers = renderers or {}
|
||||||
|
self.row_class = row_class
|
||||||
self.actions = actions or []
|
self.actions = actions or []
|
||||||
self.linked_columns = linked_columns or []
|
self.linked_columns = linked_columns or []
|
||||||
self.joiners = joiners or {}
|
self.joiners = joiners or {}
|
||||||
|
@ -530,8 +553,9 @@ class Grid:
|
||||||
Depending on the nature of grid data, sometimes a cell's
|
Depending on the nature of grid data, sometimes a cell's
|
||||||
"as-is" value will be undesirable for display purposes.
|
"as-is" value will be undesirable for display purposes.
|
||||||
|
|
||||||
The logic in :meth:`get_vue_data()` will first "convert" all
|
The logic in :meth:`get_vue_context()` will first "convert"
|
||||||
grid data as necessary so that it is at least JSON-compatible.
|
all grid data as necessary so that it is at least
|
||||||
|
JSON-compatible.
|
||||||
|
|
||||||
But then it also will invoke a renderer override (if defined)
|
But then it also will invoke a renderer override (if defined)
|
||||||
to obtain the "final" cell value.
|
to obtain the "final" cell value.
|
||||||
|
@ -1670,7 +1694,7 @@ class Grid:
|
||||||
|
|
||||||
.. code-block:: html
|
.. code-block:: html
|
||||||
|
|
||||||
<b-table :data="gridData['mykey']">
|
<b-table :data="gridContext['mykey'].data">
|
||||||
<!-- columns etc. -->
|
<!-- columns etc. -->
|
||||||
</b-table>
|
</b-table>
|
||||||
|
|
||||||
|
@ -1689,10 +1713,10 @@ class Grid:
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
The above example shows ``gridData['mykey']`` as the Vue
|
The above example shows ``gridContext['mykey'].data`` as
|
||||||
data reference. This should "just work" if you provide the
|
the Vue data reference. This should "just work" if you
|
||||||
correct ``form`` arg and the grid is contained directly by
|
provide the correct ``form`` arg and the grid is contained
|
||||||
that form's Vue component.
|
directly by that form's Vue component.
|
||||||
|
|
||||||
However, this may not account for all use cases. For now
|
However, this may not account for all use cases. For now
|
||||||
we wait and see what comes up, but know the dust may not
|
we wait and see what comes up, but know the dust may not
|
||||||
|
@ -1701,7 +1725,7 @@ class Grid:
|
||||||
|
|
||||||
# nb. must register data for inclusion on page template
|
# nb. must register data for inclusion on page template
|
||||||
if form:
|
if form:
|
||||||
form.add_grid_vue_data(self)
|
form.add_grid_vue_context(self)
|
||||||
|
|
||||||
# otherwise logic is the same, just different template
|
# otherwise logic is the same, just different template
|
||||||
return self.render_vue_template(template=template, **context)
|
return self.render_vue_template(template=template, **context)
|
||||||
|
@ -1809,7 +1833,7 @@ class Grid:
|
||||||
in its `Table docs
|
in its `Table docs
|
||||||
<https://buefy.org/documentation/table/#api-view>`_.
|
<https://buefy.org/documentation/table/#api-view>`_.
|
||||||
|
|
||||||
See also :meth:`get_vue_data()`.
|
See also :meth:`get_vue_context()`.
|
||||||
"""
|
"""
|
||||||
if not self.columns:
|
if not self.columns:
|
||||||
raise ValueError(f"you must define columns for the grid! key = {self.key}")
|
raise ValueError(f"you must define columns for the grid! key = {self.key}")
|
||||||
|
@ -1869,54 +1893,46 @@ class Grid:
|
||||||
})
|
})
|
||||||
return filters
|
return filters
|
||||||
|
|
||||||
def get_vue_data(self):
|
def get_vue_context(self):
|
||||||
"""
|
"""
|
||||||
Returns a list of Vue-compatible data records.
|
Returns a dict of context for the grid, for use with the Vue
|
||||||
|
component. This contains the following keys:
|
||||||
|
|
||||||
This calls :meth:`get_visible_data()` but then may modify the
|
* ``data`` - list of Vue-compatible data records
|
||||||
result, e.g. to add URLs for :attr:`actions` etc.
|
* ``row_classes`` - dict of per-row CSS classes
|
||||||
|
|
||||||
Importantly, this also ensures each value in the dict is
|
This first calls :meth:`get_visible_data()` to get the
|
||||||
JSON-serializable, using
|
original data set. Each record is converted to a dict.
|
||||||
:func:`~wuttaweb.util.make_json_safe()`.
|
|
||||||
|
|
||||||
:returns: List of data record dicts for use with Vue table
|
Then it calls :func:`~wuttaweb.util.make_json_safe()` to
|
||||||
component. May be the full set of data, or just the
|
ensure each record can be serialized to JSON.
|
||||||
current page, per :attr:`paginate_on_backend`.
|
|
||||||
|
Then it invokes any :attr:`renderers` which are defined, to
|
||||||
|
obtain the "final" values for each record.
|
||||||
|
|
||||||
|
Then it adds a URL key/value for each of the :attr:`actions`
|
||||||
|
defined, to each record.
|
||||||
|
|
||||||
|
Then it calls :meth:`get_row_class()` for each record. If a
|
||||||
|
value is returned, it is added to the ``row_classes`` dict.
|
||||||
|
Note that this dict is keyed by "zero-based row sequence as
|
||||||
|
string" - the Vue component expects that.
|
||||||
|
|
||||||
|
:returns: Dict of grid data/CSS context as described above.
|
||||||
"""
|
"""
|
||||||
original_data = self.get_visible_data()
|
original_data = self.get_visible_data()
|
||||||
|
|
||||||
# TODO: at some point i thought it was useful to wrangle the
|
# loop thru data
|
||||||
# columns here, but now i can't seem to figure out why..?
|
|
||||||
|
|
||||||
# # determine which columns are relevant for data set
|
|
||||||
# columns = None
|
|
||||||
# if not columns:
|
|
||||||
# columns = self.get_columns()
|
|
||||||
# if not columns:
|
|
||||||
# raise ValueError("cannot determine columns for the grid")
|
|
||||||
# columns = set(columns)
|
|
||||||
# if self.model_class:
|
|
||||||
# mapper = sa.inspect(self.model_class)
|
|
||||||
# for column in mapper.primary_key:
|
|
||||||
# columns.add(column.key)
|
|
||||||
|
|
||||||
# # prune data fields for which no column is defined
|
|
||||||
# for i, record in enumerate(original_data):
|
|
||||||
# original_data[i]= dict([(key, record[key])
|
|
||||||
# for key in columns])
|
|
||||||
|
|
||||||
# we have action(s), so add URL(s) for each record in data
|
|
||||||
data = []
|
data = []
|
||||||
for i, record in enumerate(original_data):
|
row_classes = {}
|
||||||
|
for i, record in enumerate(original_data, 1):
|
||||||
original_record = record
|
original_record = record
|
||||||
|
|
||||||
|
# convert record to new dict
|
||||||
record = dict(record)
|
record = dict(record)
|
||||||
|
|
||||||
# convert data if needed, for json compat
|
# make all values safe for json
|
||||||
record = make_json_safe(record,
|
record = make_json_safe(record, warn=False)
|
||||||
# TODO: is this a good idea?
|
|
||||||
warn=False)
|
|
||||||
|
|
||||||
# customize value rendering where applicable
|
# customize value rendering where applicable
|
||||||
for key in self.renderers:
|
for key in self.renderers:
|
||||||
|
@ -1931,9 +1947,48 @@ class Grid:
|
||||||
if url:
|
if url:
|
||||||
record[key] = url
|
record[key] = url
|
||||||
|
|
||||||
|
# set row css class if applicable
|
||||||
|
css_class = self.get_row_class(original_record, record, i)
|
||||||
|
if css_class:
|
||||||
|
# nb. use *string* zero-based index, for js compat
|
||||||
|
row_classes[str(i-1)] = css_class
|
||||||
|
|
||||||
data.append(record)
|
data.append(record)
|
||||||
|
|
||||||
return data
|
return {
|
||||||
|
'data': data,
|
||||||
|
'row_classes': row_classes,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_vue_data(self):
|
||||||
|
""" """
|
||||||
|
warnings.warn("grid.get_vue_data() is deprecated; "
|
||||||
|
"please use grid.get_vue_context() instead",
|
||||||
|
DeprecationWarning, stacklevel=2)
|
||||||
|
return self.get_vue_context()['data']
|
||||||
|
|
||||||
|
def get_row_class(self, obj, data, i):
|
||||||
|
"""
|
||||||
|
Returns the row CSS ``class`` attribute for the given record.
|
||||||
|
This method is called by :meth:`get_vue_context()`.
|
||||||
|
|
||||||
|
This will inspect/invoke :attr:`row_class` and return the
|
||||||
|
value obtained from there.
|
||||||
|
|
||||||
|
:param obj: Reference to the original model instance.
|
||||||
|
|
||||||
|
:param data: Dict of record data for the instance; part of the
|
||||||
|
Vue grid data set in/from :meth:`get_vue_context()`.
|
||||||
|
|
||||||
|
:param i: One-based sequence for this object/record (row)
|
||||||
|
within the grid.
|
||||||
|
|
||||||
|
:returns: String of CSS class name(s), or ``None``.
|
||||||
|
"""
|
||||||
|
if self.row_class:
|
||||||
|
if callable(self.row_class):
|
||||||
|
return self.row_class(obj, data, i)
|
||||||
|
return self.row_class
|
||||||
|
|
||||||
def get_vue_pager_stats(self):
|
def get_vue_pager_stats(self):
|
||||||
"""
|
"""
|
||||||
|
@ -2086,7 +2141,7 @@ class GridAction:
|
||||||
:param obj: Model instance of whatever type the parent grid is
|
:param obj: Model instance of whatever type the parent grid is
|
||||||
setup to use.
|
setup to use.
|
||||||
|
|
||||||
:param i: Zero-based sequence for the object, within the
|
:param i: One-based sequence for the object's row within the
|
||||||
parent grid.
|
parent grid.
|
||||||
|
|
||||||
See also :attr:`url`.
|
See also :attr:`url`.
|
||||||
|
|
|
@ -69,9 +69,9 @@
|
||||||
|
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
% if form.grid_vue_data:
|
% if form.grid_vue_context:
|
||||||
gridData: {
|
gridContext: {
|
||||||
% for key, data in form.grid_vue_data.items():
|
% for key, data in form.grid_vue_context.items():
|
||||||
'${key}': ${json.dumps(data)|n},
|
'${key}': ${json.dumps(data)|n},
|
||||||
% endfor
|
% endfor
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
## -*- coding: utf-8; -*-
|
## -*- coding: utf-8; -*-
|
||||||
<${b}-table :data="gridData['${grid.key}']">
|
<${b}-table :data="gridContext['${grid.key}'].data">
|
||||||
|
|
||||||
% for column in grid.get_vue_columns():
|
% for column in grid.get_vue_columns():
|
||||||
<${b}-table-column field="${column['field']}"
|
<${b}-table-column field="${column['field']}"
|
||||||
|
|
|
@ -93,6 +93,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<${b}-table :data="data"
|
<${b}-table :data="data"
|
||||||
|
:row-class="getRowClass"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
narrowed
|
narrowed
|
||||||
hoverable
|
hoverable
|
||||||
|
@ -227,10 +228,12 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
let ${grid.vue_component}CurrentData = ${json.dumps(grid.get_vue_data())|n}
|
const ${grid.vue_component}Context = ${json.dumps(grid.get_vue_context())|n}
|
||||||
|
let ${grid.vue_component}CurrentData = ${grid.vue_component}Context.data
|
||||||
|
|
||||||
const ${grid.vue_component}Data = {
|
const ${grid.vue_component}Data = {
|
||||||
data: ${grid.vue_component}CurrentData,
|
data: ${grid.vue_component}CurrentData,
|
||||||
|
rowClasses: ${grid.vue_component}Context.row_classes,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
|
||||||
## nb. this tracks whether grid.fetchFirstData() happened
|
## nb. this tracks whether grid.fetchFirstData() happened
|
||||||
|
@ -399,6 +402,11 @@
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getRowClass(row, i) {
|
||||||
|
// nb. use *string* index
|
||||||
|
return this.rowClasses[i.toString()]
|
||||||
|
},
|
||||||
|
|
||||||
renderNumber(value) {
|
renderNumber(value) {
|
||||||
if (value != undefined) {
|
if (value != undefined) {
|
||||||
return value.toLocaleString('en')
|
return value.toLocaleString('en')
|
||||||
|
@ -457,6 +465,7 @@
|
||||||
if (!response.data.error) {
|
if (!response.data.error) {
|
||||||
${grid.vue_component}CurrentData = response.data.data
|
${grid.vue_component}CurrentData = response.data.data
|
||||||
this.data = ${grid.vue_component}CurrentData
|
this.data = ${grid.vue_component}CurrentData
|
||||||
|
this.rowClasses = response.data.row_classes || {}
|
||||||
% if grid.paginated and grid.paginate_on_backend:
|
% if grid.paginated and grid.paginate_on_backend:
|
||||||
this.pagerStats = response.data.pager_stats
|
this.pagerStats = response.data.pager_stats
|
||||||
% endif
|
% endif
|
||||||
|
|
|
@ -181,6 +181,16 @@ class MasterView(View):
|
||||||
|
|
||||||
This is optional; see also :meth:`get_grid_columns()`.
|
This is optional; see also :meth:`get_grid_columns()`.
|
||||||
|
|
||||||
|
.. method:: grid_row_class(obj, data, i)
|
||||||
|
|
||||||
|
This method is *not* defined on the ``MasterView`` base class;
|
||||||
|
however if a subclass defines it then it will be automatically
|
||||||
|
used to provide :attr:`~wuttaweb.grids.base.Grid.row_class` for
|
||||||
|
the main :meth:`index()` grid.
|
||||||
|
|
||||||
|
For more info see
|
||||||
|
:meth:`~wuttaweb.grids.base.Grid.get_row_class()`.
|
||||||
|
|
||||||
.. attribute:: filterable
|
.. attribute:: filterable
|
||||||
|
|
||||||
Boolean indicating whether the grid for the :meth:`index()`
|
Boolean indicating whether the grid for the :meth:`index()`
|
||||||
|
@ -360,7 +370,7 @@ class MasterView(View):
|
||||||
if self.request.GET.get('partial'):
|
if self.request.GET.get('partial'):
|
||||||
|
|
||||||
# so-called 'partial' requests get just data, no html
|
# so-called 'partial' requests get just data, no html
|
||||||
context = {'data': grid.get_vue_data()}
|
context = grid.get_vue_context()
|
||||||
if grid.paginated and grid.paginate_on_backend:
|
if grid.paginated and grid.paginate_on_backend:
|
||||||
context['pager_stats'] = grid.get_vue_pager_stats()
|
context['pager_stats'] = grid.get_vue_pager_stats()
|
||||||
return self.json_response(context)
|
return self.json_response(context)
|
||||||
|
@ -1240,6 +1250,8 @@ class MasterView(View):
|
||||||
|
|
||||||
kwargs['actions'] = actions
|
kwargs['actions'] = actions
|
||||||
|
|
||||||
|
if hasattr(self, 'grid_row_class'):
|
||||||
|
kwargs.setdefault('row_class', self.grid_row_class)
|
||||||
kwargs.setdefault('filterable', self.filterable)
|
kwargs.setdefault('filterable', self.filterable)
|
||||||
kwargs.setdefault('filter_defaults', self.filter_defaults)
|
kwargs.setdefault('filter_defaults', self.filter_defaults)
|
||||||
kwargs.setdefault('sortable', self.sortable)
|
kwargs.setdefault('sortable', self.sortable)
|
||||||
|
|
|
@ -57,19 +57,24 @@ class UserView(MasterView):
|
||||||
|
|
||||||
filter_defaults = {
|
filter_defaults = {
|
||||||
'username': {'active': True},
|
'username': {'active': True},
|
||||||
|
'active': {'active': True, 'verb': 'is_true'},
|
||||||
}
|
}
|
||||||
sort_defaults = 'username'
|
sort_defaults = 'username'
|
||||||
|
|
||||||
# TODO: master should handle this, possibly via configure_form()
|
|
||||||
def get_query(self, session=None):
|
def get_query(self, session=None):
|
||||||
""" """
|
""" """
|
||||||
model = self.app.model
|
|
||||||
query = super().get_query(session=session)
|
query = super().get_query(session=session)
|
||||||
return query.order_by(model.User.username)
|
|
||||||
|
# nb. always join Person
|
||||||
|
model = self.app.model
|
||||||
|
query = query.outerjoin(model.Person)
|
||||||
|
|
||||||
|
return query
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
""" """
|
""" """
|
||||||
super().configure_grid(g)
|
super().configure_grid(g)
|
||||||
|
model = self.app.model
|
||||||
|
|
||||||
# never show these
|
# never show these
|
||||||
g.remove('person_uuid',
|
g.remove('person_uuid',
|
||||||
|
@ -81,6 +86,14 @@ class UserView(MasterView):
|
||||||
|
|
||||||
# person
|
# person
|
||||||
g.set_link('person')
|
g.set_link('person')
|
||||||
|
g.set_sorter('person', model.Person.full_name)
|
||||||
|
g.set_filter('person', model.Person.full_name,
|
||||||
|
label="Person Full Name")
|
||||||
|
|
||||||
|
def grid_row_class(self, user, data, i):
|
||||||
|
""" """
|
||||||
|
if not user.active:
|
||||||
|
return 'has-background-warning'
|
||||||
|
|
||||||
def configure_form(self, f):
|
def configure_form(self, f):
|
||||||
""" """
|
""" """
|
||||||
|
|
|
@ -406,28 +406,34 @@ class TestForm(TestCase):
|
||||||
self.assertIn('<script type="text/x-template" id="wutta-form-template">', html)
|
self.assertIn('<script type="text/x-template" id="wutta-form-template">', html)
|
||||||
self.assertNotIn('@submit', html)
|
self.assertNotIn('@submit', html)
|
||||||
|
|
||||||
def test_add_grid_vue_data(self):
|
def test_add_grid_vue_context(self):
|
||||||
form = self.make_form()
|
form = self.make_form()
|
||||||
|
|
||||||
# grid must have key
|
# grid must have key
|
||||||
grid = Grid(self.request)
|
grid = Grid(self.request)
|
||||||
self.assertRaises(ValueError, form.add_grid_vue_data, grid)
|
self.assertRaises(ValueError, form.add_grid_vue_context, grid)
|
||||||
|
|
||||||
# otherwise it works
|
# otherwise it works
|
||||||
grid = Grid(self.request, key='foo')
|
grid = Grid(self.request, key='foo')
|
||||||
self.assertEqual(len(form.grid_vue_data), 0)
|
self.assertEqual(len(form.grid_vue_context), 0)
|
||||||
form.add_grid_vue_data(grid)
|
form.add_grid_vue_context(grid)
|
||||||
self.assertEqual(len(form.grid_vue_data), 1)
|
self.assertEqual(len(form.grid_vue_context), 1)
|
||||||
self.assertIn('foo', form.grid_vue_data)
|
self.assertIn('foo', form.grid_vue_context)
|
||||||
self.assertEqual(form.grid_vue_data['foo'], [])
|
self.assertEqual(form.grid_vue_context['foo'], {
|
||||||
|
'data': [],
|
||||||
|
'row_classes': {},
|
||||||
|
})
|
||||||
|
|
||||||
# calling again with same key will replace data
|
# calling again with same key will replace data
|
||||||
records = [{'foo': 1}, {'foo': 2}]
|
records = [{'foo': 1}, {'foo': 2}]
|
||||||
grid = Grid(self.request, key='foo', columns=['foo'], data=records)
|
grid = Grid(self.request, key='foo', columns=['foo'], data=records)
|
||||||
form.add_grid_vue_data(grid)
|
form.add_grid_vue_context(grid)
|
||||||
self.assertEqual(len(form.grid_vue_data), 1)
|
self.assertEqual(len(form.grid_vue_context), 1)
|
||||||
self.assertIn('foo', form.grid_vue_data)
|
self.assertIn('foo', form.grid_vue_context)
|
||||||
self.assertEqual(form.grid_vue_data['foo'], records)
|
self.assertEqual(form.grid_vue_context['foo'], {
|
||||||
|
'data': records,
|
||||||
|
'row_classes': {},
|
||||||
|
})
|
||||||
|
|
||||||
def test_render_vue_finalize(self):
|
def test_render_vue_finalize(self):
|
||||||
form = self.make_form()
|
form = self.make_form()
|
||||||
|
|
|
@ -1286,10 +1286,10 @@ class TestGrid(WebTestCase):
|
||||||
|
|
||||||
# form will register grid data
|
# form will register grid data
|
||||||
form = Form(self.request)
|
form = Form(self.request)
|
||||||
self.assertEqual(len(form.grid_vue_data), 0)
|
self.assertEqual(len(form.grid_vue_context), 0)
|
||||||
html = grid.render_table_element(form)
|
html = grid.render_table_element(form)
|
||||||
self.assertEqual(len(form.grid_vue_data), 1)
|
self.assertEqual(len(form.grid_vue_context), 1)
|
||||||
self.assertIn('foobar', form.grid_vue_data)
|
self.assertIn('foobar', form.grid_vue_context)
|
||||||
|
|
||||||
def test_render_vue_finalize(self):
|
def test_render_vue_finalize(self):
|
||||||
grid = self.make_grid()
|
grid = self.make_grid()
|
||||||
|
@ -1337,6 +1337,40 @@ class TestGrid(WebTestCase):
|
||||||
filters = grid.get_vue_filters()
|
filters = grid.get_vue_filters()
|
||||||
self.assertEqual(len(filters), 2)
|
self.assertEqual(len(filters), 2)
|
||||||
|
|
||||||
|
def test_get_vue_context(self):
|
||||||
|
|
||||||
|
# empty if no columns defined
|
||||||
|
grid = self.make_grid()
|
||||||
|
context = grid.get_vue_context()
|
||||||
|
self.assertEqual(context, {'data': [], 'row_classes': {}})
|
||||||
|
|
||||||
|
# typical data is a list
|
||||||
|
mydata = [
|
||||||
|
{'foo': 'bar'},
|
||||||
|
]
|
||||||
|
grid = self.make_grid(columns=['foo'], data=mydata)
|
||||||
|
context = grid.get_vue_context()
|
||||||
|
self.assertEqual(context, {'data': [{'foo': 'bar'}], 'row_classes': {}})
|
||||||
|
|
||||||
|
# if grid has actions, that list may be supplemented
|
||||||
|
grid.actions.append(mod.GridAction(self.request, 'view', url='/blarg'))
|
||||||
|
context = grid.get_vue_context()
|
||||||
|
self.assertIsNot(context['data'], mydata)
|
||||||
|
self.assertEqual(context, {'data': [{'foo': 'bar', '_action_url_view': '/blarg'}],
|
||||||
|
'row_classes': {}})
|
||||||
|
|
||||||
|
# can override value rendering
|
||||||
|
grid.set_renderer('foo', lambda record, key, value: "blah blah")
|
||||||
|
context = grid.get_vue_context()
|
||||||
|
self.assertEqual(context, {'data': [{'foo': 'blah blah', '_action_url_view': '/blarg'}],
|
||||||
|
'row_classes': {}})
|
||||||
|
|
||||||
|
# can set row class
|
||||||
|
grid.row_class = 'whatever'
|
||||||
|
context = grid.get_vue_context()
|
||||||
|
self.assertEqual(context, {'data': [{'foo': 'blah blah', '_action_url_view': '/blarg'}],
|
||||||
|
'row_classes': {'0': 'whatever'}})
|
||||||
|
|
||||||
def test_get_vue_data(self):
|
def test_get_vue_data(self):
|
||||||
|
|
||||||
# empty if no columns defined
|
# empty if no columns defined
|
||||||
|
@ -1358,11 +1392,35 @@ class TestGrid(WebTestCase):
|
||||||
self.assertIsNot(data, mydata)
|
self.assertIsNot(data, mydata)
|
||||||
self.assertEqual(data, [{'foo': 'bar', '_action_url_view': '/blarg'}])
|
self.assertEqual(data, [{'foo': 'bar', '_action_url_view': '/blarg'}])
|
||||||
|
|
||||||
# also can override value rendering
|
# can override value rendering
|
||||||
grid.set_renderer('foo', lambda record, key, value: "blah blah")
|
grid.set_renderer('foo', lambda record, key, value: "blah blah")
|
||||||
data = grid.get_vue_data()
|
data = grid.get_vue_data()
|
||||||
self.assertEqual(data, [{'foo': 'blah blah', '_action_url_view': '/blarg'}])
|
self.assertEqual(data, [{'foo': 'blah blah', '_action_url_view': '/blarg'}])
|
||||||
|
|
||||||
|
def test_get_row_class(self):
|
||||||
|
model = self.app.model
|
||||||
|
user = model.User(username='barney', active=True)
|
||||||
|
self.session.add(user)
|
||||||
|
self.session.commit()
|
||||||
|
data = dict(user)
|
||||||
|
|
||||||
|
# null by default
|
||||||
|
grid = self.make_grid()
|
||||||
|
self.assertIsNone(grid.get_row_class(user, data, 1))
|
||||||
|
|
||||||
|
# can use static class
|
||||||
|
grid.row_class = 'foo'
|
||||||
|
self.assertEqual(grid.get_row_class(user, data, 1), 'foo')
|
||||||
|
|
||||||
|
# can use callable
|
||||||
|
def status(u, d, i):
|
||||||
|
if not u.active:
|
||||||
|
return 'inactive'
|
||||||
|
grid.row_class = status
|
||||||
|
self.assertIsNone(grid.get_row_class(user, data, 1))
|
||||||
|
user.active = False
|
||||||
|
self.assertEqual(grid.get_row_class(user, data, 1), 'inactive')
|
||||||
|
|
||||||
def test_get_vue_pager_stats(self):
|
def test_get_vue_pager_stats(self):
|
||||||
data = [
|
data = [
|
||||||
{'foo': 1, 'bar': 1},
|
{'foo': 1, 'bar': 1},
|
||||||
|
|
|
@ -487,6 +487,20 @@ class TestMasterView(WebTestCase):
|
||||||
grid = view.make_model_grid(session=self.session)
|
grid = view.make_model_grid(session=self.session)
|
||||||
self.assertIs(grid.model_class, model.Setting)
|
self.assertIs(grid.model_class, model.Setting)
|
||||||
|
|
||||||
|
# no row class by default
|
||||||
|
with patch.multiple(mod.MasterView, create=True,
|
||||||
|
model_class=model.Setting):
|
||||||
|
grid = view.make_model_grid(session=self.session)
|
||||||
|
self.assertIsNone(grid.row_class)
|
||||||
|
|
||||||
|
# can specify row class
|
||||||
|
get_row_class = MagicMock()
|
||||||
|
with patch.multiple(mod.MasterView, create=True,
|
||||||
|
model_class=model.Setting,
|
||||||
|
grid_row_class=get_row_class):
|
||||||
|
grid = view.make_model_grid(session=self.session)
|
||||||
|
self.assertIs(grid.row_class, get_row_class)
|
||||||
|
|
||||||
# no actions by default
|
# no actions by default
|
||||||
with patch.multiple(mod.MasterView, create=True,
|
with patch.multiple(mod.MasterView, create=True,
|
||||||
model_class=model.Setting):
|
model_class=model.Setting):
|
||||||
|
|
|
@ -31,6 +31,17 @@ class TestUserView(WebTestCase):
|
||||||
view.configure_grid(grid)
|
view.configure_grid(grid)
|
||||||
self.assertTrue(grid.is_linked('person'))
|
self.assertTrue(grid.is_linked('person'))
|
||||||
|
|
||||||
|
def test_grid_row_class(self):
|
||||||
|
model = self.app.model
|
||||||
|
user = model.User(username='barney', active=True)
|
||||||
|
data = dict(user)
|
||||||
|
view = self.make_view()
|
||||||
|
|
||||||
|
self.assertIsNone(view.grid_row_class(user, data, 1))
|
||||||
|
|
||||||
|
user.active = False
|
||||||
|
self.assertEqual(view.grid_row_class(user, data, 1), 'has-background-warning')
|
||||||
|
|
||||||
def test_configure_form(self):
|
def test_configure_form(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
barney = model.User(username='barney')
|
barney = model.User(username='barney')
|
||||||
|
|
Loading…
Reference in a new issue