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
|
.. attribute:: vue_tagname
|
||||||
|
|
||||||
String name for Vue component tag. By default this is
|
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
|
.. attribute:: model_class
|
||||||
|
|
||||||
|
|
@ -841,8 +842,9 @@ class Grid: # pylint: disable=too-many-instance-attributes,too-many-public-meth
|
||||||
def set_tools(self, tools):
|
def set_tools(self, tools):
|
||||||
"""
|
"""
|
||||||
Set the :attr:`tools` attribute using the given tools collection.
|
Set the :attr:`tools` attribute using the given tools collection.
|
||||||
|
|
||||||
This will normalize the list/dict to desired internal format.
|
This will normalize the list/dict to desired internal format.
|
||||||
|
|
||||||
|
See also :meth:`add_tool()`.
|
||||||
"""
|
"""
|
||||||
if tools and isinstance(tools, list):
|
if tools and isinstance(tools, list):
|
||||||
if not any(isinstance(t, (tuple, list)) for t in tools):
|
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")
|
sort_defaults = ("id", "desc")
|
||||||
|
|
||||||
has_rows = True
|
has_rows = True
|
||||||
rows_title = "Batch Rows"
|
row_model_title = "Batch Row"
|
||||||
rows_sort_defaults = "sequence"
|
rows_sort_defaults = "sequence"
|
||||||
|
|
||||||
row_labels = {
|
row_labels = {
|
||||||
|
|
@ -221,6 +221,14 @@ class BatchMasterView(MasterView):
|
||||||
f.set_node("executed_by", UserRef(self.request))
|
f.set_node("executed_by", UserRef(self.request))
|
||||||
f.set_readonly("executed_by")
|
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):
|
def objectify(self, form, **kwargs):
|
||||||
"""
|
"""
|
||||||
We override the default logic here, to invoke
|
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
|
:param \\**kwargs: Additional kwargs will be passed as-is to
|
||||||
the ``make_batch()`` call.
|
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
|
# first get the "normal" objectified batch. this will have
|
||||||
# all attributes set correctly per the form data, but will
|
# 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
|
# finally let batch handler make the "real" batch
|
||||||
return self.batch_handler.make_batch(self.Session(), **kw)
|
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)
|
return super().objectify(form)
|
||||||
|
|
||||||
def redirect_after_create(self, obj):
|
def redirect_after_create(self, obj):
|
||||||
|
|
@ -381,11 +392,22 @@ class BatchMasterView(MasterView):
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_row_model_class(cls): # pylint: disable=empty-docstring
|
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
|
return cls.row_model_class
|
||||||
|
|
||||||
model_class = cls.get_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):
|
def get_row_grid_data(self, obj):
|
||||||
"""
|
"""
|
||||||
|
|
@ -403,13 +425,73 @@ class BatchMasterView(MasterView):
|
||||||
""" """
|
""" """
|
||||||
g = grid
|
g = grid
|
||||||
super().configure_row_grid(g)
|
super().configure_row_grid(g)
|
||||||
|
batch = self.get_instance()
|
||||||
|
|
||||||
|
g.remove("batch", "status_text")
|
||||||
|
|
||||||
|
# sequence
|
||||||
g.set_label("sequence", "Seq.", column_only=True)
|
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)
|
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
|
def render_row_status( # pylint: disable=empty-docstring,unused-argument
|
||||||
self, row, key, value
|
self, row, key, value
|
||||||
):
|
):
|
||||||
""" """
|
""" """
|
||||||
return row.STATUS.get(value, 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
|
.. attribute:: has_rows
|
||||||
|
|
||||||
Whether the model has "rows" which should also be displayed
|
Whether the model has "child rows" which should also be
|
||||||
when viewing model records.
|
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
|
This the "master switch" for all row features; if this is turned
|
||||||
on then many other things kick in.
|
on then many other things kick in.
|
||||||
|
|
@ -370,11 +372,37 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
||||||
|
|
||||||
.. attribute:: row_model_class
|
.. 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()`.
|
: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
|
.. attribute:: rows_title
|
||||||
|
|
||||||
Display title for the rows grid.
|
Display title for the rows grid.
|
||||||
|
|
@ -393,7 +421,8 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
||||||
.. attribute:: rows_viewable
|
.. attribute:: rows_viewable
|
||||||
|
|
||||||
Boolean indicating whether the row model supports "viewing" -
|
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
|
(For now) If you enable this, you must also override
|
||||||
:meth:`get_row_action_url_view()`.
|
:meth:`get_row_action_url_view()`.
|
||||||
|
|
@ -401,6 +430,18 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
||||||
.. note::
|
.. note::
|
||||||
This eventually will cause there to be a ``row_view`` route
|
This eventually will cause there to be a ``row_view`` route
|
||||||
to be configured as well.
|
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
|
# row features
|
||||||
has_rows = False
|
has_rows = False
|
||||||
|
row_model_class = None
|
||||||
rows_filterable = True
|
rows_filterable = True
|
||||||
rows_filter_defaults = None
|
rows_filter_defaults = None
|
||||||
rows_sortable = True
|
rows_sortable = True
|
||||||
|
|
@ -442,6 +484,7 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
||||||
rows_paginated = True
|
rows_paginated = True
|
||||||
rows_paginate_on_backend = True
|
rows_paginate_on_backend = True
|
||||||
rows_viewable = False
|
rows_viewable = False
|
||||||
|
rows_creatable = False
|
||||||
|
|
||||||
# current action
|
# current action
|
||||||
listing = False
|
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
|
When dealing with SQLAlchemy models it would return a proper
|
||||||
mapped instance, creating it if necessary.
|
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*
|
:param form: Reference to the *already validated*
|
||||||
:class:`~wuttaweb.forms.base.Form` object. See the form's
|
:class:`~wuttaweb.forms.base.Form` object. See the form's
|
||||||
:attr:`~wuttaweb.forms.base.Form.validated` attribute for
|
:attr:`~wuttaweb.forms.base.Form.validated` attribute for
|
||||||
the data.
|
the data.
|
||||||
|
|
||||||
See also :meth:`edit_save_form()` which calls this method.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# use ColanderAlchemy magic if possible
|
# use ColanderAlchemy magic if possible
|
||||||
|
|
@ -2939,7 +2985,16 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
||||||
"""
|
"""
|
||||||
if hasattr(self, "rows_title"):
|
if hasattr(self, "rows_title"):
|
||||||
return 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):
|
def make_row_model_grid(self, obj, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
@ -3125,6 +3180,148 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
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
|
# class methods
|
||||||
##############################
|
##############################
|
||||||
|
|
@ -3422,15 +3619,71 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_row_model_class(cls):
|
def get_row_model_class(cls):
|
||||||
"""
|
"""
|
||||||
Returns the **row** model class for the view, if defined.
|
Returns the "child row" model class for the view. Only
|
||||||
Only relevant if :attr:`has_rows` is true.
|
relevant if :attr:`has_rows` is true.
|
||||||
|
|
||||||
There is no default here, but a subclass may override by
|
Default logic returns the :attr:`row_model_class` reference.
|
||||||
assigning :attr:`row_model_class`.
|
|
||||||
|
:returns: Mapped class, or ``None``
|
||||||
"""
|
"""
|
||||||
if hasattr(cls, "row_model_class"):
|
return cls.row_model_class
|
||||||
return cls.row_model_class
|
|
||||||
return None
|
@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
|
# configuration
|
||||||
|
|
@ -3660,3 +3913,24 @@ class MasterView(View): # pylint: disable=too-many-public-methods
|
||||||
route_name=f"{route_prefix}.version",
|
route_name=f"{route_prefix}.version",
|
||||||
permission=f"{permission_prefix}.versions",
|
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)
|
form = view.make_model_form(model_instance=batch)
|
||||||
view.configure_form(form)
|
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):
|
def test_objectify(self):
|
||||||
handler = self.make_handler()
|
handler = self.make_handler()
|
||||||
with patch.multiple(mod.BatchMasterView, create=True, model_class=MockBatch):
|
with patch.multiple(mod.BatchMasterView, create=True, model_class=MockBatch):
|
||||||
|
|
@ -345,24 +356,33 @@ class TestBatchMasterView(WebTestCase):
|
||||||
):
|
):
|
||||||
view = self.make_view()
|
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
|
# row class determined from batch class
|
||||||
with patch.object(
|
with patch.object(mod.BatchMasterView, "model_class", new=MockBatch):
|
||||||
mod.BatchMasterView, "model_class", new=MockBatch, create=True
|
|
||||||
):
|
|
||||||
cls = view.get_row_model_class()
|
cls = view.get_row_model_class()
|
||||||
self.assertIs(cls, MockBatchRow)
|
self.assertIs(cls, MockBatchRow)
|
||||||
|
|
||||||
self.assertRaises(AttributeError, view.get_row_model_class)
|
self.assertIsNone(view.get_row_model_class())
|
||||||
|
|
||||||
# view may specify row class
|
# view may specify row class
|
||||||
with patch.object(
|
with patch.object(mod.BatchMasterView, "row_model_class", new=MockBatchRow):
|
||||||
mod.BatchMasterView, "row_model_class", new=MockBatchRow, create=True
|
|
||||||
):
|
|
||||||
cls = view.get_row_model_class()
|
cls = view.get_row_model_class()
|
||||||
self.assertIs(cls, MockBatchRow)
|
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):
|
def test_get_row_grid_data(self):
|
||||||
handler = self.make_handler()
|
handler = self.make_handler()
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
|
@ -395,6 +415,9 @@ class TestBatchMasterView(WebTestCase):
|
||||||
self.assertEqual(data.count(), 1)
|
self.assertEqual(data.count(), 1)
|
||||||
|
|
||||||
def test_configure_row_grid(self):
|
def test_configure_row_grid(self):
|
||||||
|
self.pyramid_config.add_route(
|
||||||
|
"mock_batches.create_row", "/batch/mock/{uuid}/new-row"
|
||||||
|
)
|
||||||
handler = self.make_handler()
|
handler = self.make_handler()
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
|
||||||
|
|
@ -410,18 +433,50 @@ class TestBatchMasterView(WebTestCase):
|
||||||
with patch.object(
|
with patch.object(
|
||||||
mod.BatchMasterView, "get_batch_handler", return_value=handler
|
mod.BatchMasterView, "get_batch_handler", return_value=handler
|
||||||
):
|
):
|
||||||
|
view = self.make_view()
|
||||||
|
|
||||||
Session = MagicMock(return_value=self.session)
|
with patch.object(
|
||||||
Session.query.side_effect = lambda m: self.session.query(m)
|
mod.BatchMasterView, "Session", return_value=self.session
|
||||||
with patch.multiple(
|
|
||||||
mod.BatchMasterView, create=True, Session=Session, model_class=MockBatch
|
|
||||||
):
|
):
|
||||||
|
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}):
|
# basic sanity check
|
||||||
view = self.make_view()
|
grid = view.make_row_model_grid(batch)
|
||||||
grid = view.make_row_model_grid(batch)
|
self.assertEqual(
|
||||||
self.assertIn("sequence", grid.labels)
|
grid.columns, ["sequence", "status_code", "modified"]
|
||||||
self.assertEqual(grid.labels["sequence"], "Seq.")
|
)
|
||||||
|
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):
|
def test_render_row_status(self):
|
||||||
with patch.object(mod.BatchMasterView, "get_batch_handler", return_value=None):
|
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"})
|
row = MagicMock(foo=1, STATUS={1: "bar"})
|
||||||
self.assertEqual(view.render_row_status(row, "foo", 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):
|
def test_defaults(self):
|
||||||
# nb. coverage only
|
# nb. coverage only
|
||||||
with patch.object(
|
with patch.object(mod.BatchMasterView, "model_class", new=MockBatch):
|
||||||
mod.BatchMasterView, "model_class", new=MockBatch, create=True
|
|
||||||
):
|
|
||||||
mod.BatchMasterView.defaults(self.pyramid_config)
|
mod.BatchMasterView.defaults(self.pyramid_config)
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,8 @@ class TestMasterView(WebTestCase):
|
||||||
downloadable=True,
|
downloadable=True,
|
||||||
executable=True,
|
executable=True,
|
||||||
configurable=True,
|
configurable=True,
|
||||||
|
has_rows=True,
|
||||||
|
rows_creatable=True,
|
||||||
):
|
):
|
||||||
mod.MasterView.defaults(self.pyramid_config)
|
mod.MasterView.defaults(self.pyramid_config)
|
||||||
|
|
||||||
|
|
@ -327,6 +329,68 @@ class TestMasterView(WebTestCase):
|
||||||
):
|
):
|
||||||
self.assertIs(mod.MasterView.get_row_model_class(), model.User)
|
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
|
# support methods
|
||||||
##############################
|
##############################
|
||||||
|
|
@ -1722,6 +1786,34 @@ class TestMasterView(WebTestCase):
|
||||||
# row methods
|
# 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):
|
def test_collect_row_labels(self):
|
||||||
|
|
||||||
# default labels
|
# default labels
|
||||||
|
|
@ -1833,15 +1925,110 @@ class TestMasterView(WebTestCase):
|
||||||
row = MagicMock()
|
row = MagicMock()
|
||||||
self.assertRaises(NotImplementedError, view.get_row_action_url_view, row, 0)
|
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()
|
view = self.make_view()
|
||||||
|
|
||||||
# no default
|
# no model class
|
||||||
self.assertIsNone(view.get_rows_title())
|
form = view.make_row_model_form()
|
||||||
|
self.assertIsNone(form.model_class)
|
||||||
|
|
||||||
# class may specify
|
# explicit model class + fields
|
||||||
with patch.object(view, "rows_title", create=True, new="Mock Rows"):
|
form = view.make_row_model_form(
|
||||||
self.assertEqual(view.get_rows_title(), "Mock Rows")
|
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):
|
class TestVersionedMasterView(VersionWebTestCase):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue