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())
|
||||
|
||||
# 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):
|
||||
"""
|
||||
|
@ -826,16 +826,16 @@ class Form:
|
|||
output = render(template, context)
|
||||
return HTML.literal(output)
|
||||
|
||||
def add_grid_vue_data(self, grid):
|
||||
def add_grid_vue_context(self, grid):
|
||||
""" """
|
||||
if not grid.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, "
|
||||
"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(
|
||||
self,
|
||||
|
|
|
@ -27,6 +27,7 @@ Base grid classes
|
|||
import functools
|
||||
import json
|
||||
import logging
|
||||
import warnings
|
||||
from collections import namedtuple
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
@ -116,6 +117,26 @@ class Grid:
|
|||
|
||||
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
|
||||
|
||||
List of :class:`GridAction` instances represenging action links
|
||||
|
@ -330,6 +351,7 @@ class Grid:
|
|||
data=None,
|
||||
labels={},
|
||||
renderers={},
|
||||
row_class=None,
|
||||
actions=[],
|
||||
linked_columns=[],
|
||||
sortable=False,
|
||||
|
@ -355,6 +377,7 @@ class Grid:
|
|||
self.data = data
|
||||
self.labels = labels or {}
|
||||
self.renderers = renderers or {}
|
||||
self.row_class = row_class
|
||||
self.actions = actions or []
|
||||
self.linked_columns = linked_columns or []
|
||||
self.joiners = joiners or {}
|
||||
|
@ -530,8 +553,9 @@ class Grid:
|
|||
Depending on the nature of grid data, sometimes a cell's
|
||||
"as-is" value will be undesirable for display purposes.
|
||||
|
||||
The logic in :meth:`get_vue_data()` will first "convert" all
|
||||
grid data as necessary so that it is at least JSON-compatible.
|
||||
The logic in :meth:`get_vue_context()` will first "convert"
|
||||
all grid data as necessary so that it is at least
|
||||
JSON-compatible.
|
||||
|
||||
But then it also will invoke a renderer override (if defined)
|
||||
to obtain the "final" cell value.
|
||||
|
@ -1670,7 +1694,7 @@ class Grid:
|
|||
|
||||
.. code-block:: html
|
||||
|
||||
<b-table :data="gridData['mykey']">
|
||||
<b-table :data="gridContext['mykey'].data">
|
||||
<!-- columns etc. -->
|
||||
</b-table>
|
||||
|
||||
|
@ -1689,10 +1713,10 @@ class Grid:
|
|||
|
||||
.. note::
|
||||
|
||||
The above example shows ``gridData['mykey']`` as the Vue
|
||||
data reference. This should "just work" if you provide the
|
||||
correct ``form`` arg and the grid is contained directly by
|
||||
that form's Vue component.
|
||||
The above example shows ``gridContext['mykey'].data`` as
|
||||
the Vue data reference. This should "just work" if you
|
||||
provide the correct ``form`` arg and the grid is contained
|
||||
directly by that form's Vue component.
|
||||
|
||||
However, this may not account for all use cases. For now
|
||||
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
|
||||
if form:
|
||||
form.add_grid_vue_data(self)
|
||||
form.add_grid_vue_context(self)
|
||||
|
||||
# otherwise logic is the same, just different template
|
||||
return self.render_vue_template(template=template, **context)
|
||||
|
@ -1809,7 +1833,7 @@ class Grid:
|
|||
in its `Table docs
|
||||
<https://buefy.org/documentation/table/#api-view>`_.
|
||||
|
||||
See also :meth:`get_vue_data()`.
|
||||
See also :meth:`get_vue_context()`.
|
||||
"""
|
||||
if not self.columns:
|
||||
raise ValueError(f"you must define columns for the grid! key = {self.key}")
|
||||
|
@ -1869,54 +1893,46 @@ class Grid:
|
|||
})
|
||||
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
|
||||
result, e.g. to add URLs for :attr:`actions` etc.
|
||||
* ``data`` - list of Vue-compatible data records
|
||||
* ``row_classes`` - dict of per-row CSS classes
|
||||
|
||||
Importantly, this also ensures each value in the dict is
|
||||
JSON-serializable, using
|
||||
:func:`~wuttaweb.util.make_json_safe()`.
|
||||
This first calls :meth:`get_visible_data()` to get the
|
||||
original data set. Each record is converted to a dict.
|
||||
|
||||
:returns: List of data record dicts for use with Vue table
|
||||
component. May be the full set of data, or just the
|
||||
current page, per :attr:`paginate_on_backend`.
|
||||
Then it calls :func:`~wuttaweb.util.make_json_safe()` to
|
||||
ensure each record can be serialized to JSON.
|
||||
|
||||
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()
|
||||
|
||||
# TODO: at some point i thought it was useful to wrangle the
|
||||
# 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
|
||||
# loop thru data
|
||||
data = []
|
||||
for i, record in enumerate(original_data):
|
||||
row_classes = {}
|
||||
for i, record in enumerate(original_data, 1):
|
||||
original_record = record
|
||||
|
||||
# convert record to new dict
|
||||
record = dict(record)
|
||||
|
||||
# convert data if needed, for json compat
|
||||
record = make_json_safe(record,
|
||||
# TODO: is this a good idea?
|
||||
warn=False)
|
||||
# make all values safe for json
|
||||
record = make_json_safe(record, warn=False)
|
||||
|
||||
# customize value rendering where applicable
|
||||
for key in self.renderers:
|
||||
|
@ -1931,9 +1947,48 @@ class Grid:
|
|||
if 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)
|
||||
|
||||
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):
|
||||
"""
|
||||
|
@ -2086,7 +2141,7 @@ class GridAction:
|
|||
:param obj: Model instance of whatever type the parent grid is
|
||||
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.
|
||||
|
||||
See also :attr:`url`.
|
||||
|
|
|
@ -69,9 +69,9 @@
|
|||
|
||||
% endif
|
||||
|
||||
% if form.grid_vue_data:
|
||||
gridData: {
|
||||
% for key, data in form.grid_vue_data.items():
|
||||
% if form.grid_vue_context:
|
||||
gridContext: {
|
||||
% for key, data in form.grid_vue_context.items():
|
||||
'${key}': ${json.dumps(data)|n},
|
||||
% endfor
|
||||
},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<${b}-table :data="gridData['${grid.key}']">
|
||||
<${b}-table :data="gridContext['${grid.key}'].data">
|
||||
|
||||
% for column in grid.get_vue_columns():
|
||||
<${b}-table-column field="${column['field']}"
|
||||
|
|
|
@ -93,6 +93,7 @@
|
|||
</div>
|
||||
|
||||
<${b}-table :data="data"
|
||||
:row-class="getRowClass"
|
||||
:loading="loading"
|
||||
narrowed
|
||||
hoverable
|
||||
|
@ -227,10 +228,12 @@
|
|||
|
||||
<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 = {
|
||||
data: ${grid.vue_component}CurrentData,
|
||||
rowClasses: ${grid.vue_component}Context.row_classes,
|
||||
loading: false,
|
||||
|
||||
## 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) {
|
||||
if (value != undefined) {
|
||||
return value.toLocaleString('en')
|
||||
|
@ -457,6 +465,7 @@
|
|||
if (!response.data.error) {
|
||||
${grid.vue_component}CurrentData = response.data.data
|
||||
this.data = ${grid.vue_component}CurrentData
|
||||
this.rowClasses = response.data.row_classes || {}
|
||||
% if grid.paginated and grid.paginate_on_backend:
|
||||
this.pagerStats = response.data.pager_stats
|
||||
% endif
|
||||
|
|
|
@ -181,6 +181,16 @@ class MasterView(View):
|
|||
|
||||
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
|
||||
|
||||
Boolean indicating whether the grid for the :meth:`index()`
|
||||
|
@ -360,7 +370,7 @@ class MasterView(View):
|
|||
if self.request.GET.get('partial'):
|
||||
|
||||
# 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:
|
||||
context['pager_stats'] = grid.get_vue_pager_stats()
|
||||
return self.json_response(context)
|
||||
|
@ -1240,6 +1250,8 @@ class MasterView(View):
|
|||
|
||||
kwargs['actions'] = actions
|
||||
|
||||
if hasattr(self, 'grid_row_class'):
|
||||
kwargs.setdefault('row_class', self.grid_row_class)
|
||||
kwargs.setdefault('filterable', self.filterable)
|
||||
kwargs.setdefault('filter_defaults', self.filter_defaults)
|
||||
kwargs.setdefault('sortable', self.sortable)
|
||||
|
|
|
@ -57,19 +57,24 @@ class UserView(MasterView):
|
|||
|
||||
filter_defaults = {
|
||||
'username': {'active': True},
|
||||
'active': {'active': True, 'verb': 'is_true'},
|
||||
}
|
||||
sort_defaults = 'username'
|
||||
|
||||
# TODO: master should handle this, possibly via configure_form()
|
||||
def get_query(self, session=None):
|
||||
""" """
|
||||
model = self.app.model
|
||||
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):
|
||||
""" """
|
||||
super().configure_grid(g)
|
||||
model = self.app.model
|
||||
|
||||
# never show these
|
||||
g.remove('person_uuid',
|
||||
|
@ -81,6 +86,14 @@ class UserView(MasterView):
|
|||
|
||||
# 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):
|
||||
""" """
|
||||
|
|
|
@ -406,28 +406,34 @@ class TestForm(TestCase):
|
|||
self.assertIn('<script type="text/x-template" id="wutta-form-template">', html)
|
||||
self.assertNotIn('@submit', html)
|
||||
|
||||
def test_add_grid_vue_data(self):
|
||||
def test_add_grid_vue_context(self):
|
||||
form = self.make_form()
|
||||
|
||||
# grid must have key
|
||||
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
|
||||
grid = Grid(self.request, key='foo')
|
||||
self.assertEqual(len(form.grid_vue_data), 0)
|
||||
form.add_grid_vue_data(grid)
|
||||
self.assertEqual(len(form.grid_vue_data), 1)
|
||||
self.assertIn('foo', form.grid_vue_data)
|
||||
self.assertEqual(form.grid_vue_data['foo'], [])
|
||||
self.assertEqual(len(form.grid_vue_context), 0)
|
||||
form.add_grid_vue_context(grid)
|
||||
self.assertEqual(len(form.grid_vue_context), 1)
|
||||
self.assertIn('foo', form.grid_vue_context)
|
||||
self.assertEqual(form.grid_vue_context['foo'], {
|
||||
'data': [],
|
||||
'row_classes': {},
|
||||
})
|
||||
|
||||
# calling again with same key will replace data
|
||||
records = [{'foo': 1}, {'foo': 2}]
|
||||
grid = Grid(self.request, key='foo', columns=['foo'], data=records)
|
||||
form.add_grid_vue_data(grid)
|
||||
self.assertEqual(len(form.grid_vue_data), 1)
|
||||
self.assertIn('foo', form.grid_vue_data)
|
||||
self.assertEqual(form.grid_vue_data['foo'], records)
|
||||
form.add_grid_vue_context(grid)
|
||||
self.assertEqual(len(form.grid_vue_context), 1)
|
||||
self.assertIn('foo', form.grid_vue_context)
|
||||
self.assertEqual(form.grid_vue_context['foo'], {
|
||||
'data': records,
|
||||
'row_classes': {},
|
||||
})
|
||||
|
||||
def test_render_vue_finalize(self):
|
||||
form = self.make_form()
|
||||
|
|
|
@ -1286,10 +1286,10 @@ class TestGrid(WebTestCase):
|
|||
|
||||
# form will register grid data
|
||||
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)
|
||||
self.assertEqual(len(form.grid_vue_data), 1)
|
||||
self.assertIn('foobar', form.grid_vue_data)
|
||||
self.assertEqual(len(form.grid_vue_context), 1)
|
||||
self.assertIn('foobar', form.grid_vue_context)
|
||||
|
||||
def test_render_vue_finalize(self):
|
||||
grid = self.make_grid()
|
||||
|
@ -1337,6 +1337,40 @@ class TestGrid(WebTestCase):
|
|||
filters = grid.get_vue_filters()
|
||||
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):
|
||||
|
||||
# empty if no columns defined
|
||||
|
@ -1358,11 +1392,35 @@ class TestGrid(WebTestCase):
|
|||
self.assertIsNot(data, mydata)
|
||||
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")
|
||||
data = grid.get_vue_data()
|
||||
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):
|
||||
data = [
|
||||
{'foo': 1, 'bar': 1},
|
||||
|
|
|
@ -487,6 +487,20 @@ class TestMasterView(WebTestCase):
|
|||
grid = view.make_model_grid(session=self.session)
|
||||
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
|
||||
with patch.multiple(mod.MasterView, create=True,
|
||||
model_class=model.Setting):
|
||||
|
|
|
@ -31,6 +31,17 @@ class TestUserView(WebTestCase):
|
|||
view.configure_grid(grid)
|
||||
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):
|
||||
model = self.app.model
|
||||
barney = model.User(username='barney')
|
||||
|
|
Loading…
Reference in a new issue