fix: add basic create_row() support, esp. for batch views
This commit is contained in:
parent
66eccc52a2
commit
10610d5809
6 changed files with 696 additions and 48 deletions
|
|
@ -84,7 +84,8 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
|
|||
.. attribute:: vue_tagname
|
||||
|
||||
String name for Vue component tag. By default this is
|
||||
``'wutta-grid'``. See also :meth:`render_vue_tag()`.
|
||||
``'wutta-grid'``. See also :meth:`render_vue_tag()`
|
||||
and :attr:`vue_component`.
|
||||
|
||||
.. attribute:: model_class
|
||||
|
||||
|
|
@ -841,8 +842,9 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
|
|||
def set_tools(self, tools):
|
||||
"""
|
||||
Set the :attr:`tools` attribute using the given tools collection.
|
||||
|
||||
This will normalize the list/dict to desired internal format.
|
||||
|
||||
See also :meth:`add_tool()`.
|
||||
"""
|
||||
if tools and isinstance(tools, list):
|
||||
if not any(isinstance(t, (tuple, list)) for t in tools):
|
||||
|
|
|
|||
4
src/wuttaweb/templates/master/create_row.mako
Normal file
4
src/wuttaweb/templates/master/create_row.mako
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/master/form.mako" />
|
||||
|
||||
<%def name="title()">New ${row_model_title}</%def>
|
||||
|
|
@ -61,7 +61,7 @@ class BatchMasterView(MasterView):
|
|||
sort_defaults = ("id", "desc")
|
||||
|
||||
has_rows = True
|
||||
rows_title = "Batch Rows"
|
||||
row_model_title = "Batch Row"
|
||||
rows_sort_defaults = "sequence"
|
||||
|
||||
row_labels = {
|
||||
|
|
@ -221,6 +221,14 @@ class BatchMasterView(MasterView):
|
|||
f.set_node("executed_by", UserRef(self.request))
|
||||
f.set_readonly("executed_by")
|
||||
|
||||
def is_editable(self, batch):
|
||||
"""
|
||||
This overrides the parent method
|
||||
:meth:`~wuttaweb.views.master.MasterView.is_editable()` to
|
||||
return ``False`` if the batch has already been executed.
|
||||
"""
|
||||
return not batch.executed
|
||||
|
||||
def objectify(self, form, **kwargs):
|
||||
"""
|
||||
We override the default logic here, to invoke
|
||||
|
|
@ -231,7 +239,10 @@ class BatchMasterView(MasterView):
|
|||
:param \\**kwargs: Additional kwargs will be passed as-is to
|
||||
the ``make_batch()`` call.
|
||||
"""
|
||||
if self.creating:
|
||||
model = self.app.model
|
||||
|
||||
# need special handling when creating new batch
|
||||
if self.creating and issubclass(form.model_class, model.BatchMixin):
|
||||
|
||||
# first get the "normal" objectified batch. this will have
|
||||
# all attributes set correctly per the form data, but will
|
||||
|
|
@ -255,7 +266,7 @@ class BatchMasterView(MasterView):
|
|||
# finally let batch handler make the "real" batch
|
||||
return self.batch_handler.make_batch(self.Session(), **kw)
|
||||
|
||||
# when not creating, normal logic is fine
|
||||
# otherwise normal logic is fine
|
||||
return super().objectify(form)
|
||||
|
||||
def redirect_after_create(self, obj):
|
||||
|
|
@ -381,11 +392,22 @@ class BatchMasterView(MasterView):
|
|||
@classmethod
|
||||
def get_row_model_class(cls): # pylint: disable=empty-docstring
|
||||
""" """
|
||||
if hasattr(cls, "row_model_class"):
|
||||
if cls.row_model_class:
|
||||
return cls.row_model_class
|
||||
|
||||
model_class = cls.get_model_class()
|
||||
return model_class.__row_class__
|
||||
if model_class and hasattr(model_class, "__row_class__"):
|
||||
return model_class.__row_class__
|
||||
|
||||
return None
|
||||
|
||||
def get_row_parent(self, row):
|
||||
"""
|
||||
This overrides the parent method
|
||||
:meth:`~wuttaweb.views.master.MasterView.get_row_parent()` to
|
||||
return the batch to which the given row belongs.
|
||||
"""
|
||||
return row.batch
|
||||
|
||||
def get_row_grid_data(self, obj):
|
||||
"""
|
||||
|
|
@ -403,13 +425,73 @@ class BatchMasterView(MasterView):
|
|||
""" """
|
||||
g = grid
|
||||
super().configure_row_grid(g)
|
||||
batch = self.get_instance()
|
||||
|
||||
g.remove("batch", "status_text")
|
||||
|
||||
# sequence
|
||||
g.set_label("sequence", "Seq.", column_only=True)
|
||||
if "sequence" in g.columns:
|
||||
i = g.columns.index("sequence")
|
||||
if i > 0:
|
||||
g.columns.remove("sequence")
|
||||
g.columns.insert(0, "sequence")
|
||||
|
||||
# status_code
|
||||
g.set_renderer("status_code", self.render_row_status)
|
||||
|
||||
# tool button - create row
|
||||
if not batch.executed and self.has_perm("create_row"):
|
||||
button = self.make_button(
|
||||
f"New {self.get_row_model_title()}",
|
||||
primary=True,
|
||||
icon_left="plus",
|
||||
url=self.get_action_url("create_row", batch),
|
||||
)
|
||||
g.add_tool(button, key="create_row")
|
||||
|
||||
def render_row_status( # pylint: disable=empty-docstring,unused-argument
|
||||
self, row, key, value
|
||||
):
|
||||
""" """
|
||||
return row.STATUS.get(value, value)
|
||||
|
||||
def configure_row_form(self, form):
|
||||
""" """
|
||||
f = form
|
||||
super().configure_row_form(f)
|
||||
|
||||
f.remove("batch", "status_text")
|
||||
|
||||
# sequence
|
||||
if self.creating:
|
||||
f.remove("sequence")
|
||||
else:
|
||||
f.set_readonly("sequence")
|
||||
|
||||
# status_code
|
||||
if self.creating:
|
||||
f.remove("status_code")
|
||||
else:
|
||||
f.set_readonly("status_code")
|
||||
|
||||
# modified
|
||||
if self.creating:
|
||||
f.remove("modified")
|
||||
else:
|
||||
f.set_readonly("modified")
|
||||
|
||||
def create_row_save_form(self, form):
|
||||
"""
|
||||
Override of the parent method
|
||||
:meth:`~wuttaweb.views.master.MasterView.create_row_save_form()`;
|
||||
this does basically the same thing except it also will call
|
||||
:meth:`~wuttjamaican:wuttjamaican.batch.BatchHandler.add_row()`
|
||||
on the batch handler.
|
||||
"""
|
||||
session = self.Session()
|
||||
batch = self.get_instance()
|
||||
row = self.objectify(form)
|
||||
self.batch_handler.add_row(batch, row)
|
||||
session.flush()
|
||||
return row
|
||||
|
|
|
|||
|
|
@ -360,8 +360,10 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
|||
|
||||
.. attribute:: has_rows
|
||||
|
||||
Whether the model has "rows" which should also be displayed
|
||||
when viewing model records.
|
||||
Whether the model has "child rows" which should also be
|
||||
displayed when viewing model records. For instance when
|
||||
viewing a :term:`batch` you want to see both the batch header
|
||||
as well as its row data.
|
||||
|
||||
This the "master switch" for all row features; if this is turned
|
||||
on then many other things kick in.
|
||||
|
|
@ -370,11 +372,37 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
|||
|
||||
.. attribute:: row_model_class
|
||||
|
||||
Reference to a :term:`data model` class for the rows.
|
||||
Reference to the :term:`data model` class for the child rows.
|
||||
|
||||
The base logic should not access this directly but instead call
|
||||
Subclass should define this if :attr:`has_rows` is true.
|
||||
|
||||
View logic should not access this directly but instead call
|
||||
:meth:`get_row_model_class()`.
|
||||
|
||||
.. attribute:: row_model_name
|
||||
|
||||
Optional override for the view's row model name,
|
||||
e.g. ``'WuttaWidget'``.
|
||||
|
||||
Code should not access this directly but instead call
|
||||
:meth:`get_row_model_name()`.
|
||||
|
||||
.. attribute:: row_model_title
|
||||
|
||||
Optional override for the view's "humanized" (singular) row
|
||||
model title, e.g. ``"Wutta Widget"``.
|
||||
|
||||
Code should not access this directly but instead call
|
||||
:meth:`get_row_model_title()`.
|
||||
|
||||
.. attribute:: row_model_title_plural
|
||||
|
||||
Optional override for the view's "humanized" (plural) row model
|
||||
title, e.g. ``"Wutta Widgets"``.
|
||||
|
||||
Code should not access this directly but instead call
|
||||
:meth:`get_row_model_title_plural()`.
|
||||
|
||||
.. attribute:: rows_title
|
||||
|
||||
Display title for the rows grid.
|
||||
|
|
@ -393,7 +421,8 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
|||
.. attribute:: rows_viewable
|
||||
|
||||
Boolean indicating whether the row model supports "viewing" -
|
||||
i.e. it should have a "View" action in the row grid.
|
||||
i.e. the row grid should have a "View" action. Default value
|
||||
is ``False``.
|
||||
|
||||
(For now) If you enable this, you must also override
|
||||
:meth:`get_row_action_url_view()`.
|
||||
|
|
@ -401,6 +430,18 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
|||
.. note::
|
||||
This eventually will cause there to be a ``row_view`` route
|
||||
to be configured as well.
|
||||
|
||||
.. attribute:: row_form_fields
|
||||
|
||||
List of fields for the row model form.
|
||||
|
||||
This is optional; see also :meth:`get_row_form_fields()`.
|
||||
|
||||
.. attribute:: rows_creatable
|
||||
|
||||
Boolean indicating whether the row model supports "creating" -
|
||||
i.e. a route should be defined for :meth:`create_row()`.
|
||||
Default value is ``False``.
|
||||
"""
|
||||
|
||||
##############################
|
||||
|
|
@ -434,6 +475,7 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
|||
|
||||
# row features
|
||||
has_rows = False
|
||||
row_model_class = None
|
||||
rows_filterable = True
|
||||
rows_filter_defaults = None
|
||||
rows_sortable = True
|
||||
|
|
@ -442,6 +484,7 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
|||
rows_paginated = True
|
||||
rows_paginate_on_backend = True
|
||||
rows_viewable = False
|
||||
rows_creatable = False
|
||||
|
||||
# current action
|
||||
listing = False
|
||||
|
|
@ -2828,12 +2871,15 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
|||
When dealing with SQLAlchemy models it would return a proper
|
||||
mapped instance, creating it if necessary.
|
||||
|
||||
This is called by various other form-saving methods:
|
||||
|
||||
* :meth:`edit_save_form()`
|
||||
* :meth:`create_row_save_form()`
|
||||
|
||||
:param form: Reference to the *already validated*
|
||||
:class:`~wuttaweb.forms.base.Form` object. See the form's
|
||||
:attr:`~wuttaweb.forms.base.Form.validated` attribute for
|
||||
the data.
|
||||
|
||||
See also :meth:`edit_save_form()` which calls this method.
|
||||
"""
|
||||
|
||||
# use ColanderAlchemy magic if possible
|
||||
|
|
@ -2939,7 +2985,16 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
|||
"""
|
||||
if hasattr(self, "rows_title"):
|
||||
return self.rows_title
|
||||
return None
|
||||
return self.get_row_model_title_plural()
|
||||
|
||||
def get_row_parent(self, row):
|
||||
"""
|
||||
This must return the parent object for the given child row.
|
||||
Only relevant if :attr:`has_rows` is true.
|
||||
|
||||
Default logic is not implemented; subclass must override.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def make_row_model_grid(self, obj, **kwargs):
|
||||
"""
|
||||
|
|
@ -3125,6 +3180,148 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
|||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def create_row(self):
|
||||
"""
|
||||
View to create a new "child row" record.
|
||||
|
||||
This usually corresponds to a URL like ``/widgets/XXX/new-row``.
|
||||
|
||||
By default, this view is included only if
|
||||
:attr:`rows_creatable` is true.
|
||||
|
||||
The default "create row" view logic will show a form with
|
||||
field widgets, allowing user to submit new values which are
|
||||
then persisted to the DB (assuming typical SQLAlchemy model).
|
||||
|
||||
Subclass normally should not override this method, but rather
|
||||
one of the related methods which are called (in)directly by
|
||||
this one:
|
||||
|
||||
* :meth:`make_row_model_form()`
|
||||
* :meth:`configure_row_form()`
|
||||
* :meth:`create_row_save_form()`
|
||||
* :meth:`redirect_after_create_row()`
|
||||
"""
|
||||
self.creating = True
|
||||
parent = self.get_instance()
|
||||
parent_url = self.get_action_url("view", parent)
|
||||
|
||||
form = self.make_row_model_form(cancel_url_fallback=parent_url)
|
||||
if form.validate():
|
||||
row = self.create_row_save_form(form)
|
||||
return self.redirect_after_create_row(row)
|
||||
|
||||
index_link = tags.link_to(self.get_index_title(), self.get_index_url())
|
||||
parent_link = tags.link_to(self.get_instance_title(parent), parent_url)
|
||||
index_title_rendered = HTML.literal("<span> »</span>").join(
|
||||
[index_link, parent_link]
|
||||
)
|
||||
|
||||
context = {
|
||||
"form": form,
|
||||
"index_title_rendered": index_title_rendered,
|
||||
"row_model_title": self.get_row_model_title(),
|
||||
}
|
||||
return self.render_to_response("create_row", context)
|
||||
|
||||
def create_row_save_form(self, form):
|
||||
"""
|
||||
This method converts the validated form data to a row model
|
||||
instance, and then saves the result to DB. It is called by
|
||||
:meth:`create_row()`.
|
||||
|
||||
:returns: The resulting row model instance, as produced by
|
||||
:meth:`objectify()`.
|
||||
"""
|
||||
row = self.objectify(form)
|
||||
session = self.Session()
|
||||
session.add(row)
|
||||
session.flush()
|
||||
return row
|
||||
|
||||
def redirect_after_create_row(self, row, **kwargs):
|
||||
"""
|
||||
Returns a redirect to the "view parent" page relative to the
|
||||
given newly-created row. Subclass may override as needed.
|
||||
|
||||
This is called by :meth:`create_row()`.
|
||||
"""
|
||||
parent = self.get_row_parent(row)
|
||||
return self.redirect(self.get_action_url("view", parent))
|
||||
|
||||
def make_row_model_form(self, model_instance=None, **kwargs):
|
||||
"""
|
||||
Create and return a form for the row model.
|
||||
|
||||
This is called by :meth:`create_row()`.
|
||||
|
||||
See also related methods, which are called by this one:
|
||||
|
||||
* :meth:`get_row_model_class()`
|
||||
* :meth:`get_row_form_fields()`
|
||||
* :meth:`~wuttaweb.views.base.View.make_form()`
|
||||
* :meth:`configure_row_form()`
|
||||
|
||||
:returns: :class:`~wuttaweb.forms.base.Form` instance
|
||||
"""
|
||||
if "model_class" not in kwargs:
|
||||
model_class = self.get_row_model_class()
|
||||
if model_class:
|
||||
kwargs["model_class"] = model_class
|
||||
|
||||
kwargs["model_instance"] = model_instance
|
||||
|
||||
if not kwargs.get("fields"):
|
||||
fields = self.get_row_form_fields()
|
||||
if fields:
|
||||
kwargs["fields"] = fields
|
||||
|
||||
form = self.make_form(**kwargs)
|
||||
self.configure_row_form(form)
|
||||
return form
|
||||
|
||||
def get_row_form_fields(self):
|
||||
"""
|
||||
Returns the initial list of field names for the row model
|
||||
form.
|
||||
|
||||
This is called by :meth:`make_row_model_form()`; in the
|
||||
resulting :class:`~wuttaweb.forms.base.Form` instance, this
|
||||
becomes :attr:`~wuttaweb.forms.base.Form.fields`.
|
||||
|
||||
This method may return ``None``, in which case the form may
|
||||
(try to) generate its own default list.
|
||||
|
||||
Subclass may define :attr:`row_form_fields` for simple cases,
|
||||
or can override this method if needed.
|
||||
|
||||
Note that :meth:`configure_row_form()` may be used to further
|
||||
modify the final field list, regardless of what this method
|
||||
returns. So a common pattern is to declare all "supported"
|
||||
fields by setting :attr:`row_form_fields` but then optionally
|
||||
remove or replace some in :meth:`configure_row_form()`.
|
||||
"""
|
||||
if hasattr(self, "row_form_fields"):
|
||||
return self.row_form_fields
|
||||
return None
|
||||
|
||||
def configure_row_form(self, form):
|
||||
"""
|
||||
Configure the row model form.
|
||||
|
||||
This is called by :meth:`make_row_model_form()` - for multiple
|
||||
CRUD views (create, view, edit, delete, possibly others).
|
||||
|
||||
The ``form`` param will already be "complete" and ready to use
|
||||
as-is, but this method can further modify it based on request
|
||||
details etc.
|
||||
|
||||
Subclass can override as needed, although be sure to invoke
|
||||
this parent method via ``super()`` if so.
|
||||
"""
|
||||
form.remove("uuid")
|
||||
self.set_row_labels(form)
|
||||
|
||||
##############################
|
||||
# class methods
|
||||
##############################
|
||||
|
|
@ -3422,15 +3619,71 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
|||
@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.
|
||||
Returns the "child row" model class for the view. Only
|
||||
relevant if :attr:`has_rows` is true.
|
||||
|
||||
There is no default here, but a subclass may override by
|
||||
assigning :attr:`row_model_class`.
|
||||
Default logic returns the :attr:`row_model_class` reference.
|
||||
|
||||
:returns: Mapped class, or ``None``
|
||||
"""
|
||||
if hasattr(cls, "row_model_class"):
|
||||
return cls.row_model_class
|
||||
return None
|
||||
return cls.row_model_class
|
||||
|
||||
@classmethod
|
||||
def get_row_model_name(cls):
|
||||
"""
|
||||
Returns the row model name for the view.
|
||||
|
||||
A model name should generally be in the format of a Python
|
||||
class name, e.g. ``'BatchRow'``. (Note this is *singular*,
|
||||
not plural.)
|
||||
|
||||
The default logic will call :meth:`get_row_model_class()` and
|
||||
return that class name as-is. Subclass may override by
|
||||
assigning :attr:`row_model_name`.
|
||||
"""
|
||||
if hasattr(cls, "row_model_name"):
|
||||
return cls.row_model_name
|
||||
|
||||
return cls.get_row_model_class().__name__
|
||||
|
||||
@classmethod
|
||||
def get_row_model_title(cls):
|
||||
"""
|
||||
Returns the "humanized" (singular) title for the row model.
|
||||
|
||||
The model title will be displayed to the user, so should have
|
||||
proper grammar and capitalization, e.g. ``"Batch Row"``.
|
||||
(Note this is *singular*, not plural.)
|
||||
|
||||
The default logic will call :meth:`get_row_model_name()` and
|
||||
use the result as-is. Subclass may override by assigning
|
||||
:attr:`row_model_title`.
|
||||
|
||||
See also :meth:`get_row_model_title_plural()`.
|
||||
"""
|
||||
if hasattr(cls, "row_model_title"):
|
||||
return cls.row_model_title
|
||||
|
||||
return cls.get_row_model_name()
|
||||
|
||||
@classmethod
|
||||
def get_row_model_title_plural(cls):
|
||||
"""
|
||||
Returns the "humanized" (plural) title for the row model.
|
||||
|
||||
The model title will be displayed to the user, so should have
|
||||
proper grammar and capitalization, e.g. ``"Batch Rows"``.
|
||||
(Note this is *plural*, not singular.)
|
||||
|
||||
The default logic will call :meth:`get_row_model_title()` and
|
||||
simply add a ``'s'`` to the end. Subclass may override by
|
||||
assigning :attr:`row_model_title_plural`.
|
||||
"""
|
||||
if hasattr(cls, "row_model_title_plural"):
|
||||
return cls.row_model_title_plural
|
||||
|
||||
row_model_title = cls.get_row_model_title()
|
||||
return f"{row_model_title}s"
|
||||
|
||||
##############################
|
||||
# configuration
|
||||
|
|
@ -3660,3 +3913,24 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
|||
route_name=f"{route_prefix}.version",
|
||||
permission=f"{permission_prefix}.versions",
|
||||
)
|
||||
|
||||
##############################
|
||||
# row-specific routes
|
||||
##############################
|
||||
|
||||
# create row
|
||||
if cls.has_rows and cls.rows_creatable:
|
||||
config.add_wutta_permission(
|
||||
permission_prefix,
|
||||
f"{permission_prefix}.create_row",
|
||||
f'Create new "rows" for {model_title}',
|
||||
)
|
||||
config.add_route(
|
||||
f"{route_prefix}.create_row", f"{instance_url_prefix}/new-row"
|
||||
)
|
||||
config.add_view(
|
||||
cls,
|
||||
attr="create_row",
|
||||
route_name=f"{route_prefix}.create_row",
|
||||
permission=f"{permission_prefix}.create_row",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -156,6 +156,17 @@ class TestBatchMasterView(WebTestCase):
|
|||
form = view.make_model_form(model_instance=batch)
|
||||
view.configure_form(form)
|
||||
|
||||
def test_is_editable(self):
|
||||
handler = self.make_handler()
|
||||
with patch.object(
|
||||
mod.BatchMasterView, "get_batch_handler", return_value=handler
|
||||
):
|
||||
view = self.make_view()
|
||||
batch = handler.make_batch(self.session)
|
||||
self.assertTrue(view.is_editable(batch))
|
||||
batch.executed = datetime.datetime.now()
|
||||
self.assertFalse(view.is_editable(batch))
|
||||
|
||||
def test_objectify(self):
|
||||
handler = self.make_handler()
|
||||
with patch.multiple(mod.BatchMasterView, create=True, model_class=MockBatch):
|
||||
|
|
@ -345,24 +356,33 @@ class TestBatchMasterView(WebTestCase):
|
|||
):
|
||||
view = self.make_view()
|
||||
|
||||
self.assertRaises(AttributeError, view.get_row_model_class)
|
||||
self.assertIsNone(view.get_row_model_class())
|
||||
|
||||
# row class determined from batch class
|
||||
with patch.object(
|
||||
mod.BatchMasterView, "model_class", new=MockBatch, create=True
|
||||
):
|
||||
with patch.object(mod.BatchMasterView, "model_class", new=MockBatch):
|
||||
cls = view.get_row_model_class()
|
||||
self.assertIs(cls, MockBatchRow)
|
||||
|
||||
self.assertRaises(AttributeError, view.get_row_model_class)
|
||||
self.assertIsNone(view.get_row_model_class())
|
||||
|
||||
# view may specify row class
|
||||
with patch.object(
|
||||
mod.BatchMasterView, "row_model_class", new=MockBatchRow, create=True
|
||||
):
|
||||
with patch.object(mod.BatchMasterView, "row_model_class", new=MockBatchRow):
|
||||
cls = view.get_row_model_class()
|
||||
self.assertIs(cls, MockBatchRow)
|
||||
|
||||
def test_get_row_parent(self):
|
||||
handler = self.make_handler()
|
||||
with patch.object(
|
||||
mod.BatchMasterView, "get_batch_handler", return_value=handler
|
||||
):
|
||||
view = self.make_view()
|
||||
batch = handler.make_batch(self.session)
|
||||
self.session.add(batch)
|
||||
row = handler.make_row()
|
||||
handler.add_row(batch, row)
|
||||
parent = view.get_row_parent(row)
|
||||
self.assertIs(parent, batch)
|
||||
|
||||
def test_get_row_grid_data(self):
|
||||
handler = self.make_handler()
|
||||
model = self.app.model
|
||||
|
|
@ -395,6 +415,9 @@ class TestBatchMasterView(WebTestCase):
|
|||
self.assertEqual(data.count(), 1)
|
||||
|
||||
def test_configure_row_grid(self):
|
||||
self.pyramid_config.add_route(
|
||||
"mock_batches.create_row", "/batch/mock/{uuid}/new-row"
|
||||
)
|
||||
handler = self.make_handler()
|
||||
model = self.app.model
|
||||
|
||||
|
|
@ -410,18 +433,50 @@ class TestBatchMasterView(WebTestCase):
|
|||
with patch.object(
|
||||
mod.BatchMasterView, "get_batch_handler", return_value=handler
|
||||
):
|
||||
view = self.make_view()
|
||||
|
||||
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(
|
||||
mod.BatchMasterView, "Session", return_value=self.session
|
||||
):
|
||||
with patch.multiple(
|
||||
mod.BatchMasterView,
|
||||
model_class=MockBatch,
|
||||
route_prefix="mock_batches",
|
||||
create=True,
|
||||
):
|
||||
with patch.object(
|
||||
self.request, "matchdict", new={"uuid": batch.uuid}
|
||||
):
|
||||
|
||||
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.")
|
||||
# basic sanity check
|
||||
grid = view.make_row_model_grid(batch)
|
||||
self.assertEqual(
|
||||
grid.columns, ["sequence", "status_code", "modified"]
|
||||
)
|
||||
self.assertIn("sequence", grid.labels)
|
||||
self.assertEqual(grid.labels["sequence"], "Seq.")
|
||||
self.assertEqual(grid.tools, {})
|
||||
|
||||
# missing 'sequence' column
|
||||
grid = view.make_row_model_grid(
|
||||
batch, columns=["status_code", "modified"]
|
||||
)
|
||||
self.assertEqual(grid.columns, ["status_code", "modified"])
|
||||
|
||||
# sequence column is made to be first if present
|
||||
grid = view.make_row_model_grid(
|
||||
batch, columns=["status_code", "modified", "sequence"]
|
||||
)
|
||||
self.assertEqual(
|
||||
grid.columns, ["sequence", "status_code", "modified"]
|
||||
)
|
||||
|
||||
# with "create row" button
|
||||
with patch.object(
|
||||
self.request, "is_root", new=True, create=True
|
||||
):
|
||||
grid = view.make_row_model_grid(batch)
|
||||
self.assertIn("create_row", grid.tools)
|
||||
|
||||
def test_render_row_status(self):
|
||||
with patch.object(mod.BatchMasterView, "get_batch_handler", return_value=None):
|
||||
|
|
@ -429,9 +484,53 @@ class TestBatchMasterView(WebTestCase):
|
|||
row = MagicMock(foo=1, STATUS={1: "bar"})
|
||||
self.assertEqual(view.render_row_status(row, "foo", 1), "bar")
|
||||
|
||||
def test_configure_row_form(self):
|
||||
handler = self.make_handler()
|
||||
|
||||
with patch.object(
|
||||
mod.BatchMasterView, "get_batch_handler", return_value=handler
|
||||
):
|
||||
view = self.make_view()
|
||||
|
||||
# some fields are readonly by default
|
||||
form = view.make_form(model_class=MockBatchRow)
|
||||
view.configure_row_form(form)
|
||||
self.assertIn("sequence", form.fields)
|
||||
self.assertTrue(form.is_readonly("sequence"))
|
||||
self.assertIn("status_code", form.fields)
|
||||
self.assertTrue(form.is_readonly("status_code"))
|
||||
self.assertIn("modified", form.fields)
|
||||
self.assertTrue(form.is_readonly("modified"))
|
||||
|
||||
# but those fields are removed when creating
|
||||
with patch.object(view, "creating", new=True):
|
||||
form = view.make_form(model_class=MockBatchRow)
|
||||
view.configure_row_form(form)
|
||||
self.assertNotIn("sequence", form.fields)
|
||||
self.assertNotIn("status_code", form.fields)
|
||||
self.assertNotIn("modified", form.fields)
|
||||
|
||||
def test_create_row_save_form(self):
|
||||
handler = self.make_handler()
|
||||
batch = MockBatch()
|
||||
row = MockBatchRow()
|
||||
|
||||
with patch.object(
|
||||
mod.BatchMasterView, "get_batch_handler", return_value=handler
|
||||
):
|
||||
with patch.object(
|
||||
mod.BatchMasterView, "Session", return_value=self.session
|
||||
):
|
||||
view = self.make_view()
|
||||
form = view.make_form(model_class=MockBatchRow)
|
||||
|
||||
with patch.object(view, "get_instance", return_value=batch):
|
||||
with patch.object(view, "objectify", return_value=row):
|
||||
with patch.object(handler, "add_row") as add_row:
|
||||
view.create_row_save_form(form)
|
||||
add_row.assert_called_once_with(batch, row)
|
||||
|
||||
def test_defaults(self):
|
||||
# nb. coverage only
|
||||
with patch.object(
|
||||
mod.BatchMasterView, "model_class", new=MockBatch, create=True
|
||||
):
|
||||
with patch.object(mod.BatchMasterView, "model_class", new=MockBatch):
|
||||
mod.BatchMasterView.defaults(self.pyramid_config)
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ class TestMasterView(WebTestCase):
|
|||
downloadable=True,
|
||||
executable=True,
|
||||
configurable=True,
|
||||
has_rows=True,
|
||||
rows_creatable=True,
|
||||
):
|
||||
mod.MasterView.defaults(self.pyramid_config)
|
||||
|
||||
|
|
@ -327,6 +329,68 @@ class TestMasterView(WebTestCase):
|
|||
):
|
||||
self.assertIs(mod.MasterView.get_row_model_class(), model.User)
|
||||
|
||||
def test_get_row_model_name(self):
|
||||
|
||||
# error by default (since no model class)
|
||||
self.assertRaises(AttributeError, mod.MasterView.get_row_model_name)
|
||||
|
||||
# may specify model name directly
|
||||
with patch.object(mod.MasterView, "row_model_name", new="Widget", create=True):
|
||||
self.assertEqual(mod.MasterView.get_row_model_name(), "Widget")
|
||||
|
||||
# or indirectly via model class
|
||||
MyModel = MagicMock(__name__="Blaster")
|
||||
with patch.object(mod.MasterView, "row_model_class", new=MyModel):
|
||||
self.assertEqual(mod.MasterView.get_row_model_name(), "Blaster")
|
||||
|
||||
def test_get_row_model_title(self):
|
||||
|
||||
# error by default (since no model class)
|
||||
self.assertRaises(AttributeError, mod.MasterView.get_row_model_title)
|
||||
|
||||
# may specify model title directly
|
||||
with patch.object(
|
||||
mod.MasterView, "row_model_title", new="Wutta Widget", create=True
|
||||
):
|
||||
self.assertEqual(mod.MasterView.get_row_model_title(), "Wutta Widget")
|
||||
|
||||
# or may specify model name
|
||||
with patch.object(mod.MasterView, "row_model_name", new="Blaster", create=True):
|
||||
self.assertEqual(mod.MasterView.get_row_model_title(), "Blaster")
|
||||
|
||||
# or may specify model class
|
||||
MyModel = MagicMock(__name__="Dinosaur")
|
||||
with patch.object(mod.MasterView, "row_model_class", new=MyModel):
|
||||
self.assertEqual(mod.MasterView.get_row_model_title(), "Dinosaur")
|
||||
|
||||
def test_get_row_model_title_plural(self):
|
||||
|
||||
# error by default (since no model class)
|
||||
self.assertRaises(AttributeError, mod.MasterView.get_row_model_title_plural)
|
||||
|
||||
# subclass may specify *plural* model title
|
||||
with patch.object(
|
||||
mod.MasterView, "row_model_title_plural", new="People", create=True
|
||||
):
|
||||
self.assertEqual(mod.MasterView.get_row_model_title_plural(), "People")
|
||||
|
||||
# or it may specify *singular* model title
|
||||
with patch.object(
|
||||
mod.MasterView, "row_model_title", new="Wutta Widget", create=True
|
||||
):
|
||||
self.assertEqual(
|
||||
mod.MasterView.get_row_model_title_plural(), "Wutta Widgets"
|
||||
)
|
||||
|
||||
# or it may specify model name
|
||||
with patch.object(mod.MasterView, "row_model_name", new="Blaster", create=True):
|
||||
self.assertEqual(mod.MasterView.get_row_model_title_plural(), "Blasters")
|
||||
|
||||
# or it may specify model class
|
||||
MyModel = MagicMock(__name__="Dinosaur")
|
||||
with patch.object(mod.MasterView, "row_model_class", new=MyModel, create=True):
|
||||
self.assertEqual(mod.MasterView.get_row_model_title_plural(), "Dinosaurs")
|
||||
|
||||
##############################
|
||||
# support methods
|
||||
##############################
|
||||
|
|
@ -1722,6 +1786,34 @@ class TestMasterView(WebTestCase):
|
|||
# row methods
|
||||
##############################
|
||||
|
||||
def test_get_rows_title(self):
|
||||
model = self.app.model
|
||||
|
||||
with patch.object(mod.MasterView, "row_model_class", new=model.User):
|
||||
view = self.make_view()
|
||||
|
||||
# default based on row model class
|
||||
self.assertEqual(view.get_rows_title(), "Users")
|
||||
|
||||
# explicit override
|
||||
with patch.object(view, "rows_title", create=True, new="Mock Rows"):
|
||||
self.assertEqual(view.get_rows_title(), "Mock Rows")
|
||||
|
||||
def test_get_row_parent(self):
|
||||
model = self.app.model
|
||||
view = self.make_view()
|
||||
|
||||
person = model.Person(full_name="Fred Flintstone")
|
||||
self.session.add(person)
|
||||
user = model.User(username="fred", person=person)
|
||||
self.session.add(user)
|
||||
self.session.commit()
|
||||
|
||||
with patch.multiple(
|
||||
mod.MasterView, model_class=model.Person, row_model_class=model.User
|
||||
):
|
||||
self.assertRaises(NotImplementedError, view.get_row_parent, user)
|
||||
|
||||
def test_collect_row_labels(self):
|
||||
|
||||
# default labels
|
||||
|
|
@ -1833,15 +1925,110 @@ class TestMasterView(WebTestCase):
|
|||
row = MagicMock()
|
||||
self.assertRaises(NotImplementedError, view.get_row_action_url_view, row, 0)
|
||||
|
||||
def test_get_rows_title(self):
|
||||
def test_make_row_model_form(self):
|
||||
model = self.app.model
|
||||
view = self.make_view()
|
||||
|
||||
# no default
|
||||
self.assertIsNone(view.get_rows_title())
|
||||
# no model class
|
||||
form = view.make_row_model_form()
|
||||
self.assertIsNone(form.model_class)
|
||||
|
||||
# class may specify
|
||||
with patch.object(view, "rows_title", create=True, new="Mock Rows"):
|
||||
self.assertEqual(view.get_rows_title(), "Mock Rows")
|
||||
# explicit model class + fields
|
||||
form = view.make_row_model_form(
|
||||
model_class=model.User, fields=["username", "active"]
|
||||
)
|
||||
self.assertIs(form.model_class, model.User)
|
||||
self.assertEqual(form.fields, ["username", "active"])
|
||||
|
||||
# implicit model + fields
|
||||
with patch.multiple(
|
||||
mod.MasterView,
|
||||
create=True,
|
||||
row_model_class=model.User,
|
||||
row_form_fields=["username", "person"],
|
||||
):
|
||||
form = view.make_row_model_form()
|
||||
self.assertIs(form.model_class, model.User)
|
||||
self.assertEqual(form.fields, ["username", "person"])
|
||||
|
||||
def test_configure_row_form(self):
|
||||
model = self.app.model
|
||||
view = self.make_view()
|
||||
|
||||
# uuid field is pruned
|
||||
with patch.object(mod.MasterView, "row_model_class", new=model.User):
|
||||
form = view.make_form(model_class=model.User, fields=["uuid", "username"])
|
||||
self.assertIn("uuid", form.fields)
|
||||
view.configure_row_form(form)
|
||||
self.assertNotIn("uuid", form.fields)
|
||||
|
||||
def test_create_row(self):
|
||||
self.pyramid_config.add_route("home", "/")
|
||||
self.pyramid_config.add_route("login", "/auth/login")
|
||||
self.pyramid_config.add_route("people", "/people/")
|
||||
self.pyramid_config.add_route("people.view", "/people/{uuid}")
|
||||
model = self.app.model
|
||||
|
||||
person = model.Person(
|
||||
first_name="Fred", last_name="Flintstone", full_name="Fred Flintstone"
|
||||
)
|
||||
self.session.add(person)
|
||||
user = model.User(username="fred", person=person)
|
||||
self.session.add(user)
|
||||
self.session.commit()
|
||||
|
||||
with patch.multiple(
|
||||
mod.MasterView,
|
||||
create=True,
|
||||
model_class=model.Person,
|
||||
row_model_class=model.User,
|
||||
row_form_fields=["person_uuid", "username"],
|
||||
route_prefix="people",
|
||||
):
|
||||
with patch.object(mod.MasterView, "Session", return_value=self.session):
|
||||
with patch.object(self.request, "matchdict", {"uuid": person.uuid}):
|
||||
with patch.object(
|
||||
mod.MasterView, "get_row_parent", return_value=person
|
||||
):
|
||||
view = self.make_view()
|
||||
|
||||
# get the form page
|
||||
response = view.create_row()
|
||||
self.assertIsInstance(response, Response)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# nb. no error
|
||||
self.assertNotIn("Required", response.text)
|
||||
|
||||
self.assertEqual(len(person.users), 1)
|
||||
|
||||
# post request to add user
|
||||
with patch.multiple(
|
||||
self.request,
|
||||
method="POST",
|
||||
POST={
|
||||
"person_uuid": person.uuid.hex,
|
||||
"username": "freddie2",
|
||||
},
|
||||
):
|
||||
response = view.create_row()
|
||||
# nb. should get redirect back to view page
|
||||
self.assertEqual(response.status_code, 302)
|
||||
# user should now be in DB
|
||||
self.session.refresh(person)
|
||||
self.assertEqual(len(person.users), 2)
|
||||
|
||||
# try another post with invalid data (username is required)
|
||||
with patch.multiple(
|
||||
self.request,
|
||||
method="POST",
|
||||
POST={"person_uuid": person.uuid.hex, "username": ""},
|
||||
):
|
||||
response = view.create_row()
|
||||
# nb. should get a form with errors
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn("Required", response.text)
|
||||
self.session.refresh(person)
|
||||
self.assertEqual(len(person.users), 2)
|
||||
|
||||
|
||||
class TestVersionedMasterView(VersionWebTestCase):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue