feat: add basic support for rows grid for master, batch views
This commit is contained in:
parent
5006c97b4b
commit
e3beb9953d
|
@ -5,5 +5,34 @@
|
||||||
|
|
||||||
<%def name="content_title()">${instance_title}</%def>
|
<%def name="content_title()">${instance_title}</%def>
|
||||||
|
|
||||||
|
<%def name="page_content()">
|
||||||
|
|
||||||
${parent.body()}
|
## render main form
|
||||||
|
${parent.page_content()}
|
||||||
|
|
||||||
|
## render row grid
|
||||||
|
% if master.has_rows:
|
||||||
|
<br />
|
||||||
|
<h4 class="block is-size-4">${master.get_rows_title() or ''}</h4>
|
||||||
|
${rows_grid.render_vue_tag()}
|
||||||
|
% endif
|
||||||
|
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="render_vue_templates()">
|
||||||
|
${parent.render_vue_templates()}
|
||||||
|
% if master.has_rows:
|
||||||
|
${self.render_vue_template_rows_grid()}
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="render_vue_template_rows_grid()">
|
||||||
|
${rows_grid.render_vue_template()}
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="make_vue_components()">
|
||||||
|
${parent.make_vue_components()}
|
||||||
|
% if master.has_rows:
|
||||||
|
${rows_grid.render_vue_finalize()}
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
|
@ -50,6 +50,10 @@ class BatchMasterView(MasterView):
|
||||||
|
|
||||||
sort_defaults = ('id', 'desc')
|
sort_defaults = ('id', 'desc')
|
||||||
|
|
||||||
|
has_rows = True
|
||||||
|
rows_title = "Batch Rows"
|
||||||
|
rows_sort_defaults = 'sequence'
|
||||||
|
|
||||||
def __init__(self, request, context=None):
|
def __init__(self, request, context=None):
|
||||||
super().__init__(request, context=context)
|
super().__init__(request, context=context)
|
||||||
self.batch_handler = self.get_batch_handler()
|
self.batch_handler = self.get_batch_handler()
|
||||||
|
@ -253,3 +257,33 @@ class BatchMasterView(MasterView):
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
session.close()
|
session.close()
|
||||||
|
|
||||||
|
##############################
|
||||||
|
# row methods
|
||||||
|
##############################
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_row_model_class(cls):
|
||||||
|
""" """
|
||||||
|
if hasattr(cls, 'row_model_class'):
|
||||||
|
return cls.row_model_class
|
||||||
|
|
||||||
|
Batch = cls.get_model_class()
|
||||||
|
return Batch.__row_class__
|
||||||
|
|
||||||
|
def get_row_grid_data(self, batch):
|
||||||
|
"""
|
||||||
|
Returns the base query for the batch
|
||||||
|
:attr:`~wuttjamaican:wuttjamaican.db.model.batch.BatchMixin.rows`
|
||||||
|
data.
|
||||||
|
"""
|
||||||
|
BatchRow = self.get_row_model_class()
|
||||||
|
query = self.Session.query(BatchRow)\
|
||||||
|
.filter(BatchRow.batch == batch)
|
||||||
|
return query
|
||||||
|
|
||||||
|
def configure_row_grid(self, g):
|
||||||
|
""" """
|
||||||
|
super().configure_row_grid(g)
|
||||||
|
|
||||||
|
g.set_label('sequence', "Seq.", column_only=True)
|
||||||
|
|
|
@ -73,12 +73,12 @@ class MasterView(View):
|
||||||
|
|
||||||
.. attribute:: model_class
|
.. attribute:: model_class
|
||||||
|
|
||||||
Optional reference to a data model class. While not strictly
|
Optional reference to a :term:`data model` class. While not
|
||||||
required, most views will set this to a SQLAlchemy mapped
|
strictly required, most views will set this to a SQLAlchemy
|
||||||
class,
|
mapped class,
|
||||||
e.g. :class:`~wuttjamaican:wuttjamaican.db.model.base.Person`.
|
e.g. :class:`~wuttjamaican:wuttjamaican.db.model.base.Person`.
|
||||||
|
|
||||||
Code should not access this directly but instead call
|
The base logic should not access this directly but instead call
|
||||||
:meth:`get_model_class()`.
|
:meth:`get_model_class()`.
|
||||||
|
|
||||||
.. attribute:: model_name
|
.. attribute:: model_name
|
||||||
|
@ -340,6 +340,38 @@ class MasterView(View):
|
||||||
Boolean indicating whether the master view supports
|
Boolean indicating whether the master view supports
|
||||||
"configuring" - i.e. it should have a :meth:`configure()` view.
|
"configuring" - i.e. it should have a :meth:`configure()` view.
|
||||||
Default value is ``False``.
|
Default value is ``False``.
|
||||||
|
|
||||||
|
**ROW FEATURES**
|
||||||
|
|
||||||
|
.. attribute:: has_rows
|
||||||
|
|
||||||
|
Whether the model has "rows" which should also be displayed
|
||||||
|
when viewing model records.
|
||||||
|
|
||||||
|
This the "master switch" for all row features; if this is turned
|
||||||
|
on then many other things kick in.
|
||||||
|
|
||||||
|
See also :attr:`row_model_class`.
|
||||||
|
|
||||||
|
.. attribute:: row_model_class
|
||||||
|
|
||||||
|
Reference to a :term:`data model` class for the rows.
|
||||||
|
|
||||||
|
The base logic should not access this directly but instead call
|
||||||
|
:meth:`get_row_model_class()`.
|
||||||
|
|
||||||
|
.. attribute:: rows_title
|
||||||
|
|
||||||
|
Display title for the rows grid.
|
||||||
|
|
||||||
|
The base logic should not access this directly but instead call
|
||||||
|
:meth:`get_rows_title()`.
|
||||||
|
|
||||||
|
.. attribute:: row_grid_columns
|
||||||
|
|
||||||
|
List of columns for the row grid.
|
||||||
|
|
||||||
|
This is optional; see also :meth:`get_row_grid_columns()`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
|
@ -368,6 +400,16 @@ class MasterView(View):
|
||||||
execute_progress_template = None
|
execute_progress_template = None
|
||||||
configurable = False
|
configurable = False
|
||||||
|
|
||||||
|
# row features
|
||||||
|
has_rows = False
|
||||||
|
rows_filterable = True
|
||||||
|
rows_filter_defaults = None
|
||||||
|
rows_sortable = True
|
||||||
|
rows_sort_on_backend = True
|
||||||
|
rows_sort_defaults = None
|
||||||
|
rows_paginated = True
|
||||||
|
rows_paginate_on_backend = True
|
||||||
|
|
||||||
# current action
|
# current action
|
||||||
listing = False
|
listing = False
|
||||||
creating = False
|
creating = False
|
||||||
|
@ -525,15 +567,40 @@ class MasterView(View):
|
||||||
|
|
||||||
* :meth:`make_model_form()`
|
* :meth:`make_model_form()`
|
||||||
* :meth:`configure_form()`
|
* :meth:`configure_form()`
|
||||||
|
* :meth:`make_row_model_grid()` - if :attr:`has_rows` is true
|
||||||
"""
|
"""
|
||||||
self.viewing = True
|
self.viewing = True
|
||||||
instance = self.get_instance()
|
obj = self.get_instance()
|
||||||
form = self.make_model_form(instance, readonly=True)
|
form = self.make_model_form(obj, readonly=True)
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'instance': instance,
|
'instance': obj,
|
||||||
'form': form,
|
'form': form,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.has_rows:
|
||||||
|
|
||||||
|
# always make the grid first. note that it already knows
|
||||||
|
# to "reset" its params when that is requested.
|
||||||
|
grid = self.make_row_model_grid(obj)
|
||||||
|
|
||||||
|
# but if user did request a "reset" then we want to
|
||||||
|
# redirect so the query string gets cleared out
|
||||||
|
if self.request.GET.get('reset-view'):
|
||||||
|
|
||||||
|
# nb. we want to preserve url hash if applicable
|
||||||
|
kw = {'_query': None,
|
||||||
|
'_anchor': self.request.GET.get('hash')}
|
||||||
|
return self.redirect(self.request.current_route_url(**kw))
|
||||||
|
|
||||||
|
# so-called 'partial' requests get just the grid data
|
||||||
|
if self.request.params.get('partial'):
|
||||||
|
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)
|
||||||
|
|
||||||
|
context['rows_grid'] = grid
|
||||||
|
|
||||||
return self.render_to_response('view', context)
|
return self.render_to_response('view', context)
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
|
@ -1907,8 +1974,8 @@ class MasterView(View):
|
||||||
|
|
||||||
This is called by :meth:`make_model_grid()`.
|
This is called by :meth:`make_model_grid()`.
|
||||||
|
|
||||||
There is no default logic here; subclass should override as
|
There is minimal default logic here; subclass should override
|
||||||
needed. The ``grid`` param will already be "complete" and
|
as needed. The ``grid`` param will already be "complete" and
|
||||||
ready to use as-is, but this method can further modify it
|
ready to use as-is, but this method can further modify it
|
||||||
based on request details etc.
|
based on request details etc.
|
||||||
"""
|
"""
|
||||||
|
@ -2241,6 +2308,182 @@ class MasterView(View):
|
||||||
session = session or self.Session()
|
session = session or self.Session()
|
||||||
session.add(obj)
|
session.add(obj)
|
||||||
|
|
||||||
|
##############################
|
||||||
|
# row methods
|
||||||
|
##############################
|
||||||
|
|
||||||
|
def get_rows_title(self):
|
||||||
|
"""
|
||||||
|
Returns the display title for model **rows** grid, if
|
||||||
|
applicable/desired. Only relevant if :attr:`has_rows` is
|
||||||
|
true.
|
||||||
|
|
||||||
|
There is no default here, but subclass may override by
|
||||||
|
assigning :attr:`rows_title`.
|
||||||
|
"""
|
||||||
|
if hasattr(self, 'rows_title'):
|
||||||
|
return self.rows_title
|
||||||
|
|
||||||
|
def make_row_model_grid(self, obj, **kwargs):
|
||||||
|
"""
|
||||||
|
Create and return a grid for a record's **rows** data, for use
|
||||||
|
in :meth:`view()`. Only applicable if :attr:`has_rows` is
|
||||||
|
true.
|
||||||
|
|
||||||
|
:param obj: Current model instance for which rows data is
|
||||||
|
being displayed.
|
||||||
|
|
||||||
|
:returns: :class:`~wuttaweb.grids.base.Grid` instance for the
|
||||||
|
rows data.
|
||||||
|
|
||||||
|
See also related methods, which are called by this one:
|
||||||
|
|
||||||
|
* :meth:`get_row_grid_key()`
|
||||||
|
* :meth:`get_row_grid_columns()`
|
||||||
|
* :meth:`get_row_grid_data()`
|
||||||
|
* :meth:`configure_row_grid()`
|
||||||
|
"""
|
||||||
|
if 'key' not in kwargs:
|
||||||
|
kwargs['key'] = self.get_row_grid_key()
|
||||||
|
|
||||||
|
if 'model_class' not in kwargs:
|
||||||
|
model_class = self.get_row_model_class()
|
||||||
|
if model_class:
|
||||||
|
kwargs['model_class'] = model_class
|
||||||
|
|
||||||
|
if 'columns' not in kwargs:
|
||||||
|
kwargs['columns'] = self.get_row_grid_columns()
|
||||||
|
|
||||||
|
if 'data' not in kwargs:
|
||||||
|
kwargs['data'] = self.get_row_grid_data(obj)
|
||||||
|
|
||||||
|
kwargs.setdefault('filterable', self.rows_filterable)
|
||||||
|
kwargs.setdefault('filter_defaults', self.rows_filter_defaults)
|
||||||
|
kwargs.setdefault('sortable', self.rows_sortable)
|
||||||
|
kwargs.setdefault('sort_multiple', not self.request.use_oruga)
|
||||||
|
kwargs.setdefault('sort_on_backend', self.rows_sort_on_backend)
|
||||||
|
kwargs.setdefault('sort_defaults', self.rows_sort_defaults)
|
||||||
|
kwargs.setdefault('paginated', self.rows_paginated)
|
||||||
|
kwargs.setdefault('paginate_on_backend', self.rows_paginate_on_backend)
|
||||||
|
|
||||||
|
grid = self.make_grid(**kwargs)
|
||||||
|
self.configure_row_grid(grid)
|
||||||
|
grid.load_settings()
|
||||||
|
return grid
|
||||||
|
|
||||||
|
def get_row_grid_key(self):
|
||||||
|
"""
|
||||||
|
Returns the (presumably) unique key to be used for the
|
||||||
|
**rows** grid in :meth:`view()`. Only relevant if
|
||||||
|
:attr:`has_rows` is true.
|
||||||
|
|
||||||
|
This is called from :meth:`make_row_model_grid()`; in the
|
||||||
|
resulting grid, this becomes
|
||||||
|
:attr:`~wuttaweb.grids.base.Grid.key`.
|
||||||
|
|
||||||
|
Whereas you can define :attr:`grid_key` for the main grid, the
|
||||||
|
row grid key is always generated dynamically. This
|
||||||
|
incorporates the current record key (whose rows are in the
|
||||||
|
grid) so that the rows grid for each record is unique.
|
||||||
|
"""
|
||||||
|
parts = [self.get_grid_key()]
|
||||||
|
for key in self.get_model_key():
|
||||||
|
parts.append(str(self.request.matchdict[key]))
|
||||||
|
return '.'.join(parts)
|
||||||
|
|
||||||
|
def get_row_grid_columns(self):
|
||||||
|
"""
|
||||||
|
Returns the default list of column names for the **rows**
|
||||||
|
grid, for use in :meth:`view()`. Only relevant if
|
||||||
|
:attr:`has_rows` is true.
|
||||||
|
|
||||||
|
This is called by :meth:`make_row_model_grid()`; in the
|
||||||
|
resulting grid, this becomes
|
||||||
|
:attr:`~wuttaweb.grids.base.Grid.columns`.
|
||||||
|
|
||||||
|
This method may return ``None``, in which case the grid may
|
||||||
|
(try to) generate its own default list.
|
||||||
|
|
||||||
|
Subclass may define :attr:`row_grid_columns` for simple cases,
|
||||||
|
or can override this method if needed.
|
||||||
|
|
||||||
|
Also note that :meth:`configure_row_grid()` may be used to
|
||||||
|
further modify the final column set, regardless of what this
|
||||||
|
method returns. So a common pattern is to declare all
|
||||||
|
"supported" columns by setting :attr:`row_grid_columns` but
|
||||||
|
then optionally remove or replace some of those within
|
||||||
|
:meth:`configure_row_grid()`.
|
||||||
|
"""
|
||||||
|
if hasattr(self, 'row_grid_columns'):
|
||||||
|
return self.row_grid_columns
|
||||||
|
|
||||||
|
def get_row_grid_data(self, obj):
|
||||||
|
"""
|
||||||
|
Returns the data for the **rows** grid, for use in
|
||||||
|
:meth:`view()`. Only relevant if :attr:`has_rows` is true.
|
||||||
|
|
||||||
|
This is called by :meth:`make_row_model_grid()`; in the
|
||||||
|
resulting grid, this becomes
|
||||||
|
:attr:`~wuttaweb.grids.base.Grid.data`.
|
||||||
|
|
||||||
|
Default logic not implemented; subclass must define this.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def configure_row_grid(self, grid):
|
||||||
|
"""
|
||||||
|
Configure the **rows** grid for use in :meth:`view()`. Only
|
||||||
|
relevant if :attr:`has_rows` is true.
|
||||||
|
|
||||||
|
This is called by :meth:`make_row_model_grid()`.
|
||||||
|
|
||||||
|
There is minimal default logic here; subclass should override
|
||||||
|
as needed. The ``grid`` param will already be "complete" and
|
||||||
|
ready to use as-is, but this method can further modify it
|
||||||
|
based on request details etc.
|
||||||
|
"""
|
||||||
|
grid.remove('uuid')
|
||||||
|
self.set_row_labels(grid)
|
||||||
|
|
||||||
|
def set_row_labels(self, obj):
|
||||||
|
"""
|
||||||
|
Set label overrides on a **row** form or grid, based on what
|
||||||
|
is defined by the view class and its parent class(es).
|
||||||
|
|
||||||
|
This is called automatically from
|
||||||
|
:meth:`configure_row_grid()` and
|
||||||
|
:meth:`configure_row_form()`.
|
||||||
|
|
||||||
|
This calls :meth:`collect_row_labels()` to find everything,
|
||||||
|
then it assigns the labels using one of (based on ``obj``
|
||||||
|
type):
|
||||||
|
|
||||||
|
* :func:`wuttaweb.forms.base.Form.set_label()`
|
||||||
|
* :func:`wuttaweb.grids.base.Grid.set_label()`
|
||||||
|
|
||||||
|
:param obj: Either a :class:`~wuttaweb.grids.base.Grid` or a
|
||||||
|
:class:`~wuttaweb.forms.base.Form` instance.
|
||||||
|
"""
|
||||||
|
labels = self.collect_row_labels()
|
||||||
|
for key, label in labels.items():
|
||||||
|
obj.set_label(key, label)
|
||||||
|
|
||||||
|
def collect_row_labels(self):
|
||||||
|
"""
|
||||||
|
Collect all **row** labels defined within the view class
|
||||||
|
hierarchy.
|
||||||
|
|
||||||
|
This is called by :meth:`set_row_labels()`.
|
||||||
|
|
||||||
|
:returns: Dict of all labels found.
|
||||||
|
"""
|
||||||
|
labels = {}
|
||||||
|
hierarchy = self.get_class_hierarchy()
|
||||||
|
for cls in hierarchy:
|
||||||
|
if hasattr(cls, 'row_labels'):
|
||||||
|
labels.update(cls.row_labels)
|
||||||
|
return labels
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
# class methods
|
# class methods
|
||||||
##############################
|
##############################
|
||||||
|
@ -2526,6 +2769,18 @@ class MasterView(View):
|
||||||
|
|
||||||
return cls.get_model_title_plural()
|
return cls.get_model_title_plural()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_row_model_class(cls):
|
||||||
|
"""
|
||||||
|
Returns the **row** model class for the view, if defined.
|
||||||
|
Only relevant if :attr:`has_rows` is true.
|
||||||
|
|
||||||
|
There is no default here, but a subclass may override by
|
||||||
|
assigning :attr:`row_model_class`.
|
||||||
|
"""
|
||||||
|
if hasattr(cls, 'row_model_class'):
|
||||||
|
return cls.row_model_class
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
# configuration
|
# configuration
|
||||||
##############################
|
##############################
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import datetime
|
import datetime
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
from sqlalchemy import orm
|
||||||
from pyramid.httpexceptions import HTTPFound
|
from pyramid.httpexceptions import HTTPFound
|
||||||
|
|
||||||
from wuttjamaican.db import model
|
from wuttjamaican.db import model
|
||||||
|
@ -19,12 +20,23 @@ class MockBatchRow(model.BatchRowMixin, model.Base):
|
||||||
__tablename__ = 'testing_batch_mock_row'
|
__tablename__ = 'testing_batch_mock_row'
|
||||||
__batch_class__ = MockBatch
|
__batch_class__ = MockBatch
|
||||||
|
|
||||||
|
MockBatch.__row_class__ = MockBatchRow
|
||||||
|
|
||||||
class MockBatchHandler(BatchHandler):
|
class MockBatchHandler(BatchHandler):
|
||||||
model_class = MockBatch
|
model_class = MockBatch
|
||||||
|
|
||||||
|
|
||||||
class TestBatchMasterView(WebTestCase):
|
class TestBatchMasterView(WebTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.setup_web()
|
||||||
|
|
||||||
|
# nb. create MockBatch, MockBatchRow
|
||||||
|
model.Base.metadata.create_all(bind=self.session.bind)
|
||||||
|
|
||||||
|
def make_view(self):
|
||||||
|
return mod.BatchMasterView(self.request)
|
||||||
|
|
||||||
def test_get_batch_handler(self):
|
def test_get_batch_handler(self):
|
||||||
self.assertRaises(NotImplementedError, mod.BatchMasterView, self.request)
|
self.assertRaises(NotImplementedError, mod.BatchMasterView, self.request)
|
||||||
|
|
||||||
|
@ -200,3 +212,78 @@ class TestBatchMasterView(WebTestCase):
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
# nb. should give up waiting after 1 second
|
# nb. should give up waiting after 1 second
|
||||||
self.assertRaises(RuntimeError, view.populate_thread, batch.uuid)
|
self.assertRaises(RuntimeError, view.populate_thread, batch.uuid)
|
||||||
|
|
||||||
|
def test_get_row_model_class(self):
|
||||||
|
handler = MockBatchHandler(self.config)
|
||||||
|
with patch.object(mod.BatchMasterView, 'get_batch_handler', return_value=handler):
|
||||||
|
view = self.make_view()
|
||||||
|
|
||||||
|
self.assertRaises(AttributeError, view.get_row_model_class)
|
||||||
|
|
||||||
|
# row class determined from batch class
|
||||||
|
with patch.object(mod.BatchMasterView, 'model_class', new=MockBatch, create=True):
|
||||||
|
cls = view.get_row_model_class()
|
||||||
|
self.assertIs(cls, MockBatchRow)
|
||||||
|
|
||||||
|
self.assertRaises(AttributeError, view.get_row_model_class)
|
||||||
|
|
||||||
|
# view may specify row class
|
||||||
|
with patch.object(mod.BatchMasterView, 'row_model_class', new=MockBatchRow, create=True):
|
||||||
|
cls = view.get_row_model_class()
|
||||||
|
self.assertIs(cls, MockBatchRow)
|
||||||
|
|
||||||
|
def test_get_row_grid_data(self):
|
||||||
|
handler = MockBatchHandler(self.config)
|
||||||
|
model = self.app.model
|
||||||
|
|
||||||
|
user = model.User(username='barney')
|
||||||
|
self.session.add(user)
|
||||||
|
|
||||||
|
batch = handler.make_batch(self.session, created_by=user)
|
||||||
|
self.session.add(batch)
|
||||||
|
row = handler.make_row()
|
||||||
|
handler.add_row(batch, row)
|
||||||
|
self.session.flush()
|
||||||
|
|
||||||
|
with patch.object(mod.BatchMasterView, 'get_batch_handler', return_value=handler):
|
||||||
|
|
||||||
|
view = self.make_view()
|
||||||
|
self.assertRaises(AttributeError, view.get_row_grid_data, batch)
|
||||||
|
|
||||||
|
Session = MagicMock(return_value=self.session)
|
||||||
|
Session.query.side_effect = lambda m: self.session.query(m)
|
||||||
|
with patch.multiple(mod.BatchMasterView, create=True,
|
||||||
|
Session=Session,
|
||||||
|
model_class=MockBatch):
|
||||||
|
|
||||||
|
view = self.make_view()
|
||||||
|
data = view.get_row_grid_data(batch)
|
||||||
|
self.assertIsInstance(data, orm.Query)
|
||||||
|
self.assertEqual(data.count(), 1)
|
||||||
|
|
||||||
|
def test_configure_row_grid(self):
|
||||||
|
handler = MockBatchHandler(self.config)
|
||||||
|
model = self.app.model
|
||||||
|
|
||||||
|
user = model.User(username='barney')
|
||||||
|
self.session.add(user)
|
||||||
|
|
||||||
|
batch = handler.make_batch(self.session, created_by=user)
|
||||||
|
self.session.add(batch)
|
||||||
|
row = handler.make_row()
|
||||||
|
handler.add_row(batch, row)
|
||||||
|
self.session.flush()
|
||||||
|
|
||||||
|
with patch.object(mod.BatchMasterView, 'get_batch_handler', return_value=handler):
|
||||||
|
|
||||||
|
Session = MagicMock(return_value=self.session)
|
||||||
|
Session.query.side_effect = lambda m: self.session.query(m)
|
||||||
|
with patch.multiple(mod.BatchMasterView, create=True,
|
||||||
|
Session=Session,
|
||||||
|
model_class=MockBatch):
|
||||||
|
|
||||||
|
with patch.object(self.request, 'matchdict', new={'uuid': batch.uuid}):
|
||||||
|
view = self.make_view()
|
||||||
|
grid = view.make_row_model_grid(batch)
|
||||||
|
self.assertIn('sequence', grid.labels)
|
||||||
|
self.assertEqual(grid.labels['sequence'], "Seq.")
|
||||||
|
|
|
@ -334,6 +334,16 @@ class TestMasterView(WebTestCase):
|
||||||
model_class=MyModel):
|
model_class=MyModel):
|
||||||
self.assertEqual(mod.MasterView.get_config_title(), "Dinosaurs")
|
self.assertEqual(mod.MasterView.get_config_title(), "Dinosaurs")
|
||||||
|
|
||||||
|
def test_get_row_model_class(self):
|
||||||
|
model = self.app.model
|
||||||
|
|
||||||
|
# no default
|
||||||
|
self.assertIsNone(mod.MasterView.get_row_model_class())
|
||||||
|
|
||||||
|
# class may specify
|
||||||
|
with patch.object(mod.MasterView, 'row_model_class', create=True, new=model.User):
|
||||||
|
self.assertIs(mod.MasterView.get_row_model_class(), model.User)
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
# support methods
|
# support methods
|
||||||
##############################
|
##############################
|
||||||
|
@ -1017,6 +1027,53 @@ class TestMasterView(WebTestCase):
|
||||||
with patch.object(view, 'get_instance', return_value=setting):
|
with patch.object(view, 'get_instance', return_value=setting):
|
||||||
response = view.view()
|
response = view.view()
|
||||||
|
|
||||||
|
def test_view_with_rows(self):
|
||||||
|
self.pyramid_config.include('wuttaweb.views.common')
|
||||||
|
self.pyramid_config.include('wuttaweb.views.auth')
|
||||||
|
self.pyramid_config.add_route('people', '/people/')
|
||||||
|
model = self.app.model
|
||||||
|
person = model.Person(full_name="Whitney Houston")
|
||||||
|
self.session.add(person)
|
||||||
|
user = model.User(username='whitney', person=person)
|
||||||
|
self.session.add(user)
|
||||||
|
self.session.commit()
|
||||||
|
|
||||||
|
get_row_grid_data = MagicMock()
|
||||||
|
with patch.multiple(mod.MasterView, create=True,
|
||||||
|
Session=MagicMock(return_value=self.session),
|
||||||
|
model_class=model.Person,
|
||||||
|
route_prefix='people',
|
||||||
|
has_rows=True,
|
||||||
|
row_model_class=model.User,
|
||||||
|
get_row_grid_data=get_row_grid_data):
|
||||||
|
with patch.object(self.request, 'matchdict', new={'uuid': person.uuid}):
|
||||||
|
view = self.make_view()
|
||||||
|
|
||||||
|
# just for coverage
|
||||||
|
get_row_grid_data.return_value = []
|
||||||
|
response = view.view()
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.content_type, 'text/html')
|
||||||
|
|
||||||
|
# now with data...
|
||||||
|
get_row_grid_data.return_value = [user]
|
||||||
|
response = view.view()
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.content_type, 'text/html')
|
||||||
|
|
||||||
|
# then once more as 'partial' - aka. data only
|
||||||
|
with patch.dict(self.request.GET, {'partial': 1}):
|
||||||
|
response = view.view()
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.content_type, 'application/json')
|
||||||
|
|
||||||
|
# redirects when view is reset
|
||||||
|
with patch.dict(self.request.GET, {'reset-view': '1', 'hash': 'foo'}):
|
||||||
|
# nb. mock current route
|
||||||
|
with patch.object(self.request, 'current_route_url'):
|
||||||
|
response = view.view()
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
def test_edit(self):
|
def test_edit(self):
|
||||||
self.pyramid_config.include('wuttaweb.views.common')
|
self.pyramid_config.include('wuttaweb.views.common')
|
||||||
self.pyramid_config.include('wuttaweb.views.auth')
|
self.pyramid_config.include('wuttaweb.views.auth')
|
||||||
|
@ -1501,3 +1558,103 @@ class TestMasterView(WebTestCase):
|
||||||
# should now have 0 settings
|
# should now have 0 settings
|
||||||
count = self.session.query(model.Setting).count()
|
count = self.session.query(model.Setting).count()
|
||||||
self.assertEqual(count, 0)
|
self.assertEqual(count, 0)
|
||||||
|
|
||||||
|
##############################
|
||||||
|
# row methods
|
||||||
|
##############################
|
||||||
|
|
||||||
|
def test_collect_row_labels(self):
|
||||||
|
|
||||||
|
# default labels
|
||||||
|
view = self.make_view()
|
||||||
|
labels = view.collect_row_labels()
|
||||||
|
self.assertEqual(labels, {})
|
||||||
|
|
||||||
|
# labels come from all classes; subclass wins
|
||||||
|
with patch.object(View, 'row_labels', create=True, new={'foo': "Foo", 'bar': "Bar"}):
|
||||||
|
with patch.object(mod.MasterView, 'row_labels', create=True, new={'foo': "FOO FIGHTERS"}):
|
||||||
|
view = self.make_view()
|
||||||
|
labels = view.collect_row_labels()
|
||||||
|
self.assertEqual(labels, {'foo': "FOO FIGHTERS", 'bar': "Bar"})
|
||||||
|
|
||||||
|
def test_set_row_labels(self):
|
||||||
|
model = self.app.model
|
||||||
|
person = model.Person(full_name="Fred Flintstone")
|
||||||
|
self.session.add(person)
|
||||||
|
|
||||||
|
with patch.multiple(mod.MasterView, create=True,
|
||||||
|
model_class=model.Person,
|
||||||
|
has_rows=True,
|
||||||
|
row_model_class=model.User):
|
||||||
|
|
||||||
|
# no labels by default
|
||||||
|
view = self.make_view()
|
||||||
|
grid = view.make_row_model_grid(person, key='person.users', data=[])
|
||||||
|
view.set_row_labels(grid)
|
||||||
|
self.assertEqual(grid.labels, {})
|
||||||
|
|
||||||
|
# labels come from all classes; subclass wins
|
||||||
|
with patch.object(View, 'row_labels', create=True, new={'username': "USERNAME"}):
|
||||||
|
with patch.object(mod.MasterView, 'row_labels', create=True, new={'username': "UserName"}):
|
||||||
|
view = self.make_view()
|
||||||
|
grid = view.make_row_model_grid(person, key='person.users', data=[])
|
||||||
|
view.set_row_labels(grid)
|
||||||
|
self.assertEqual(grid.labels, {'username': "UserName"})
|
||||||
|
|
||||||
|
def test_get_row_grid_data(self):
|
||||||
|
model = self.app.model
|
||||||
|
person = model.Person(full_name="Fred Flintstone")
|
||||||
|
self.session.add(person)
|
||||||
|
view = self.make_view()
|
||||||
|
self.assertRaises(NotImplementedError, view.get_row_grid_data, person)
|
||||||
|
|
||||||
|
def test_get_row_grid_columns(self):
|
||||||
|
|
||||||
|
# no default
|
||||||
|
view = self.make_view()
|
||||||
|
self.assertIsNone(view.get_row_grid_columns())
|
||||||
|
|
||||||
|
# class may specify
|
||||||
|
with patch.object(view, 'row_grid_columns', create=True, new=['foo', 'bar']):
|
||||||
|
self.assertEqual(view.get_row_grid_columns(), ['foo', 'bar'])
|
||||||
|
|
||||||
|
def test_get_row_grid_key(self):
|
||||||
|
view = self.make_view()
|
||||||
|
with patch.multiple(mod.MasterView, create=True,
|
||||||
|
model_key='id',
|
||||||
|
grid_key='widgets'):
|
||||||
|
|
||||||
|
self.request.matchdict = {'id': 42}
|
||||||
|
self.assertEqual(view.get_row_grid_key(), 'widgets.42')
|
||||||
|
|
||||||
|
def test_make_row_model_grid(self):
|
||||||
|
model = self.app.model
|
||||||
|
person = model.Person(full_name="Barney Rubble")
|
||||||
|
self.session.add(person)
|
||||||
|
self.session.commit()
|
||||||
|
|
||||||
|
self.request.matchdict = {'uuid': person.uuid}
|
||||||
|
with patch.multiple(mod.MasterView, create=True,
|
||||||
|
model_class=model.Person):
|
||||||
|
view = self.make_view()
|
||||||
|
|
||||||
|
# specify data
|
||||||
|
grid = view.make_row_model_grid(person, data=[])
|
||||||
|
self.assertIsNone(grid.model_class)
|
||||||
|
self.assertEqual(grid.data, [])
|
||||||
|
|
||||||
|
# fetch data
|
||||||
|
with patch.object(view, 'get_row_grid_data', return_value=[]):
|
||||||
|
grid = view.make_row_model_grid(person)
|
||||||
|
self.assertIsNone(grid.model_class)
|
||||||
|
self.assertEqual(grid.data, [])
|
||||||
|
|
||||||
|
def test_get_rows_title(self):
|
||||||
|
view = self.make_view()
|
||||||
|
|
||||||
|
# no default
|
||||||
|
self.assertIsNone(view.get_rows_title())
|
||||||
|
|
||||||
|
# class may specify
|
||||||
|
with patch.object(view, 'rows_title', create=True, new="Mock Rows"):
|
||||||
|
self.assertEqual(view.get_rows_title(), "Mock Rows")
|
||||||
|
|
Loading…
Reference in a new issue