feat: add basic Delete support for CRUD master view
This commit is contained in:
parent
1a8fc8dd44
commit
c46b42f76d
|
@ -118,9 +118,9 @@ class Form:
|
||||||
|
|
||||||
.. attribute:: schema
|
.. attribute:: schema
|
||||||
|
|
||||||
Colander-based schema object for the form. This is optional;
|
:class:`colander:colander.Schema` object for the form. This is
|
||||||
if not specified an attempt will be made to construct one
|
optional; if not specified an attempt will be made to construct
|
||||||
automatically.
|
one automatically.
|
||||||
|
|
||||||
See also :meth:`get_schema()`.
|
See also :meth:`get_schema()`.
|
||||||
|
|
||||||
|
@ -151,12 +151,13 @@ class Form:
|
||||||
|
|
||||||
.. attribute:: readonly_fields
|
.. attribute:: readonly_fields
|
||||||
|
|
||||||
Set of fields which should be readonly. Each will still be
|
A :class:`~python:set` of field names which should be readonly.
|
||||||
rendered but with static value text and no widget.
|
Each will still be rendered but with static value text and no
|
||||||
|
widget.
|
||||||
|
|
||||||
This is only applicable if :attr:`readonly` is ``False``.
|
This is only applicable if :attr:`readonly` is ``False``.
|
||||||
|
|
||||||
See also :meth:`set_readonly()`.
|
See also :meth:`set_readonly()` and :meth:`is_readonly()`.
|
||||||
|
|
||||||
.. attribute:: action_url
|
.. attribute:: action_url
|
||||||
|
|
||||||
|
@ -202,6 +203,21 @@ class Form:
|
||||||
|
|
||||||
String icon name for the form submit button. Default is ``'save'``.
|
String icon name for the form submit button. Default is ``'save'``.
|
||||||
|
|
||||||
|
.. attribute:: button_type_submit
|
||||||
|
|
||||||
|
Buefy type for the submit button. Default is ``'is-primary'``,
|
||||||
|
so for example:
|
||||||
|
|
||||||
|
.. code-block:: html
|
||||||
|
|
||||||
|
<b-button type="is-primary"
|
||||||
|
native-type="submit">
|
||||||
|
Save
|
||||||
|
</b-button>
|
||||||
|
|
||||||
|
See also the `Buefy docs
|
||||||
|
<https://buefy.org/documentation/button/#api-view>`_.
|
||||||
|
|
||||||
.. attribute:: show_button_reset
|
.. attribute:: show_button_reset
|
||||||
|
|
||||||
Flag indicating whether a Reset button should be shown.
|
Flag indicating whether a Reset button should be shown.
|
||||||
|
@ -249,6 +265,7 @@ class Form:
|
||||||
auto_disable_submit=True,
|
auto_disable_submit=True,
|
||||||
button_label_submit="Save",
|
button_label_submit="Save",
|
||||||
button_icon_submit='save',
|
button_icon_submit='save',
|
||||||
|
button_type_submit='is-primary',
|
||||||
show_button_reset=False,
|
show_button_reset=False,
|
||||||
show_button_cancel=True,
|
show_button_cancel=True,
|
||||||
button_label_cancel="Cancel",
|
button_label_cancel="Cancel",
|
||||||
|
@ -267,6 +284,7 @@ class Form:
|
||||||
self.auto_disable_submit = auto_disable_submit
|
self.auto_disable_submit = auto_disable_submit
|
||||||
self.button_label_submit = button_label_submit
|
self.button_label_submit = button_label_submit
|
||||||
self.button_icon_submit = button_icon_submit
|
self.button_icon_submit = button_icon_submit
|
||||||
|
self.button_type_submit = button_type_submit
|
||||||
self.show_button_reset = show_button_reset
|
self.show_button_reset = show_button_reset
|
||||||
self.show_button_cancel = show_button_cancel
|
self.show_button_cancel = show_button_cancel
|
||||||
self.button_label_cancel = button_label_cancel
|
self.button_label_cancel = button_label_cancel
|
||||||
|
@ -650,14 +668,14 @@ class Form:
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
"""
|
"""
|
||||||
Try to validate the form.
|
Try to validate the form, using data from the :attr:`request`.
|
||||||
|
|
||||||
This should work whether request data was submitted as classic
|
Uses :func:`~wuttaweb.util.get_form_data()` to retrieve the
|
||||||
POST data, or as JSON body.
|
form data from POST or JSON body.
|
||||||
|
|
||||||
If the form data is valid, this method returns the data dict.
|
If the form data is valid, the data dict is returned. This
|
||||||
This data dict is also then available on the form object via
|
data dict is also made available on the form object via the
|
||||||
the :attr:`validated` attribute.
|
:attr:`validated` attribute.
|
||||||
|
|
||||||
However if the data is not valid, ``False`` is returned, and
|
However if the data is not valid, ``False`` is returned, and
|
||||||
there will be no :attr:`validated` attribute. In that case
|
there will be no :attr:`validated` attribute. In that case
|
||||||
|
@ -665,6 +683,17 @@ class Form:
|
||||||
wrong for the user's sake. See also
|
wrong for the user's sake. See also
|
||||||
:meth:`get_field_errors()`.
|
:meth:`get_field_errors()`.
|
||||||
|
|
||||||
|
This uses :meth:`deform:deform.Field.validate()` under the
|
||||||
|
hood.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
Calling ``validate()`` on some forms will cause the
|
||||||
|
underlying Deform and Colander structures to mutate. In
|
||||||
|
particular, all :attr:`readonly_fields` will be *removed*
|
||||||
|
from the :attr:`schema` to ensure they are not involved in
|
||||||
|
the validation.
|
||||||
|
|
||||||
:returns: Data dict, or ``False``.
|
:returns: Data dict, or ``False``.
|
||||||
"""
|
"""
|
||||||
if hasattr(self, 'validated'):
|
if hasattr(self, 'validated'):
|
||||||
|
@ -673,9 +702,16 @@ class Form:
|
||||||
if self.request.method != 'POST':
|
if self.request.method != 'POST':
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# remove all readonly fields from deform / schema
|
||||||
dform = self.get_deform()
|
dform = self.get_deform()
|
||||||
controls = get_form_data(self.request).items()
|
if self.readonly_fields:
|
||||||
|
schema = self.get_schema()
|
||||||
|
for field in self.readonly_fields:
|
||||||
|
del schema[field]
|
||||||
|
dform.children.remove(dform[field])
|
||||||
|
|
||||||
|
# let deform do real validation
|
||||||
|
controls = get_form_data(self.request).items()
|
||||||
try:
|
try:
|
||||||
self.validated = dform.validate(controls)
|
self.validated = dform.validate(controls)
|
||||||
except deform.ValidationFailure:
|
except deform.ValidationFailure:
|
||||||
|
|
|
@ -331,6 +331,10 @@ class GridAction:
|
||||||
Name of icon to be shown for the action link.
|
Name of icon to be shown for the action link.
|
||||||
|
|
||||||
See also :meth:`render_icon()`.
|
See also :meth:`render_icon()`.
|
||||||
|
|
||||||
|
.. attribute:: link_class
|
||||||
|
|
||||||
|
Optional HTML class attribute for the action's ``<a>`` tag.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -340,6 +344,7 @@ class GridAction:
|
||||||
label=None,
|
label=None,
|
||||||
url=None,
|
url=None,
|
||||||
icon=None,
|
icon=None,
|
||||||
|
link_class=None,
|
||||||
):
|
):
|
||||||
self.request = request
|
self.request = request
|
||||||
self.config = self.request.wutta_config
|
self.config = self.request.wutta_config
|
||||||
|
@ -348,6 +353,16 @@ class GridAction:
|
||||||
self.url = url
|
self.url = url
|
||||||
self.label = label or self.app.make_title(key)
|
self.label = label or self.app.make_title(key)
|
||||||
self.icon = icon or key
|
self.icon = icon or key
|
||||||
|
self.link_class = link_class or ''
|
||||||
|
|
||||||
|
def render_icon_and_label(self):
|
||||||
|
"""
|
||||||
|
Render the HTML snippet for action link icon and label.
|
||||||
|
|
||||||
|
Default logic returns the output from :meth:`render_icon()`
|
||||||
|
and :meth:`render_label()`.
|
||||||
|
"""
|
||||||
|
return self.render_icon() + ' ' + self.render_label()
|
||||||
|
|
||||||
def render_icon(self):
|
def render_icon(self):
|
||||||
"""
|
"""
|
||||||
|
@ -359,6 +374,8 @@ class GridAction:
|
||||||
.. code-block:: html
|
.. code-block:: html
|
||||||
|
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
|
|
||||||
|
See also :meth:`render_icon_and_label()`.
|
||||||
"""
|
"""
|
||||||
if self.request.use_oruga:
|
if self.request.use_oruga:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
@ -370,6 +387,8 @@ class GridAction:
|
||||||
Render the label text for the action link.
|
Render the label text for the action link.
|
||||||
|
|
||||||
Default behavior is to return :attr:`label` as-is.
|
Default behavior is to return :attr:`label` as-is.
|
||||||
|
|
||||||
|
See also :meth:`render_icon_and_label()`.
|
||||||
"""
|
"""
|
||||||
return self.label
|
return self.label
|
||||||
|
|
||||||
|
|
|
@ -231,13 +231,10 @@
|
||||||
## TODO
|
## TODO
|
||||||
% if master and master.configurable and not master.configuring:
|
% if master and master.configurable and not master.configuring:
|
||||||
<div class="level-item">
|
<div class="level-item">
|
||||||
<b-button type="is-primary"
|
<wutta-button once type="is-primary"
|
||||||
tag="a"
|
tag="a" href="${url(f'{route_prefix}.configure')}"
|
||||||
href="${url(f'{route_prefix}.configure')}"
|
icon-left="cog"
|
||||||
icon-pack="fas"
|
label="Configure" />
|
||||||
icon-left="cog">
|
|
||||||
Configure
|
|
||||||
</b-button>
|
|
||||||
</div>
|
</div>
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
|
@ -374,11 +371,28 @@
|
||||||
tag="a" href="${master.get_action_url('edit', instance)}"
|
tag="a" href="${master.get_action_url('edit', instance)}"
|
||||||
icon-left="edit"
|
icon-left="edit"
|
||||||
label="Edit This" />
|
label="Edit This" />
|
||||||
|
<wutta-button once type="is-danger"
|
||||||
|
tag="a" href="${master.get_action_url('delete', instance)}"
|
||||||
|
icon-left="trash"
|
||||||
|
label="Delete This" />
|
||||||
% elif master.editing:
|
% elif master.editing:
|
||||||
<wutta-button once
|
<wutta-button once
|
||||||
tag="a" href="${master.get_action_url('view', instance)}"
|
tag="a" href="${master.get_action_url('view', instance)}"
|
||||||
icon-left="eye"
|
icon-left="eye"
|
||||||
label="View This" />
|
label="View This" />
|
||||||
|
<wutta-button once type="is-danger"
|
||||||
|
tag="a" href="${master.get_action_url('delete', instance)}"
|
||||||
|
icon-left="trash"
|
||||||
|
label="Delete This" />
|
||||||
|
% elif master.deleting:
|
||||||
|
<wutta-button once
|
||||||
|
tag="a" href="${master.get_action_url('view', instance)}"
|
||||||
|
icon-left="eye"
|
||||||
|
label="View This" />
|
||||||
|
<wutta-button once
|
||||||
|
tag="a" href="${master.get_action_url('edit', instance)}"
|
||||||
|
icon-left="edit"
|
||||||
|
label="Edit This" />
|
||||||
% endif
|
% endif
|
||||||
% endif
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
</b-button>
|
</b-button>
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
<b-button type="is-primary"
|
<b-button type="${form.button_type_submit}"
|
||||||
native-type="submit"
|
native-type="submit"
|
||||||
% if form.auto_disable_submit:
|
% if form.auto_disable_submit:
|
||||||
:disabled="formSubmitting"
|
:disabled="formSubmitting"
|
||||||
|
|
|
@ -24,9 +24,9 @@
|
||||||
label="Actions"
|
label="Actions"
|
||||||
v-slot="props">
|
v-slot="props">
|
||||||
% for action in grid.actions:
|
% for action in grid.actions:
|
||||||
<a :href="props.row._action_url_${action.key}">
|
<a :href="props.row._action_url_${action.key}"
|
||||||
${action.render_icon()}
|
class="${action.link_class}">
|
||||||
${action.render_label()}
|
${action.render_icon_and_label()}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
% endfor
|
% endfor
|
||||||
|
|
18
src/wuttaweb/templates/master/delete.mako
Normal file
18
src/wuttaweb/templates/master/delete.mako
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
## -*- coding: utf-8; -*-
|
||||||
|
<%inherit file="/master/form.mako" />
|
||||||
|
|
||||||
|
<%def name="title()">${index_title} » ${instance_title} » Delete</%def>
|
||||||
|
|
||||||
|
<%def name="content_title()">Delete: ${instance_title}</%def>
|
||||||
|
|
||||||
|
<%def name="page_content()">
|
||||||
|
<br />
|
||||||
|
<b-notification type="is-danger" :closable="false"
|
||||||
|
style="width: 50%;">
|
||||||
|
Really DELETE this ${model_title}?
|
||||||
|
</b-notification>
|
||||||
|
${parent.page_content()}
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
|
${parent.body()}
|
|
@ -180,6 +180,12 @@ class MasterView(View):
|
||||||
i.e. it should have an :meth:`edit()` view. Default value is
|
i.e. it should have an :meth:`edit()` view. Default value is
|
||||||
``True``.
|
``True``.
|
||||||
|
|
||||||
|
.. attribute:: deletable
|
||||||
|
|
||||||
|
Boolean indicating whether the view model supports "deleting" -
|
||||||
|
i.e. it should have a :meth:`delete()` view. Default value is
|
||||||
|
``True``.
|
||||||
|
|
||||||
.. attribute:: form_fields
|
.. attribute:: form_fields
|
||||||
|
|
||||||
List of columns for the model form.
|
List of columns for the model form.
|
||||||
|
@ -202,12 +208,14 @@ class MasterView(View):
|
||||||
has_grid = True
|
has_grid = True
|
||||||
viewable = True
|
viewable = True
|
||||||
editable = True
|
editable = True
|
||||||
|
deletable = True
|
||||||
configurable = False
|
configurable = False
|
||||||
|
|
||||||
# current action
|
# current action
|
||||||
listing = False
|
listing = False
|
||||||
viewing = False
|
viewing = False
|
||||||
editing = False
|
editing = False
|
||||||
|
deleting = False
|
||||||
configuring = False
|
configuring = False
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
|
@ -277,6 +285,11 @@ class MasterView(View):
|
||||||
actions.append(self.make_grid_action('edit', icon='edit',
|
actions.append(self.make_grid_action('edit', icon='edit',
|
||||||
url=self.get_action_url_edit))
|
url=self.get_action_url_edit))
|
||||||
|
|
||||||
|
if self.deletable:
|
||||||
|
actions.append(self.make_grid_action('delete', icon='trash',
|
||||||
|
url=self.get_action_url_delete,
|
||||||
|
link_class='has-text-danger'))
|
||||||
|
|
||||||
kwargs['actions'] = actions
|
kwargs['actions'] = actions
|
||||||
|
|
||||||
grid = self.make_grid(**kwargs)
|
grid = self.make_grid(**kwargs)
|
||||||
|
@ -389,12 +402,11 @@ class MasterView(View):
|
||||||
instance_title = self.get_instance_title(instance)
|
instance_title = self.get_instance_title(instance)
|
||||||
|
|
||||||
form = self.make_model_form(instance,
|
form = self.make_model_form(instance,
|
||||||
cancel_url_fallback=self.get_index_url())
|
cancel_url_fallback=self.get_action_url('view', instance))
|
||||||
|
|
||||||
if self.request.method == 'POST':
|
if form.validate():
|
||||||
if form.validate():
|
self.edit_save_form(form)
|
||||||
self.edit_save_form(form)
|
return self.redirect(self.get_action_url('view', instance))
|
||||||
return self.redirect(self.get_action_url('view', instance))
|
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'instance': instance,
|
'instance': instance,
|
||||||
|
@ -422,6 +434,83 @@ class MasterView(View):
|
||||||
self.persist(obj)
|
self.persist(obj)
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
##############################
|
||||||
|
# delete methods
|
||||||
|
##############################
|
||||||
|
|
||||||
|
def delete(self):
|
||||||
|
"""
|
||||||
|
View to delete an existing model instance.
|
||||||
|
|
||||||
|
This usually corresponds to a URL like ``/widgets/XXX/delete``
|
||||||
|
where ``XXX`` represents the key/ID for the record.
|
||||||
|
|
||||||
|
By default, this view is included only if :attr:`deletable` is
|
||||||
|
true.
|
||||||
|
|
||||||
|
The default "delete" view logic will show a "psuedo-readonly"
|
||||||
|
form with no fields editable, but with a submit button so user
|
||||||
|
must confirm, before deletion actually occurs.
|
||||||
|
|
||||||
|
Subclass normally should not override this method, but rather
|
||||||
|
one of the related methods which are called (in)directly by
|
||||||
|
this one:
|
||||||
|
|
||||||
|
* :meth:`make_model_form()`
|
||||||
|
* :meth:`configure_form()`
|
||||||
|
* :meth:`delete_save_form()`
|
||||||
|
* :meth:`delete_instance()`
|
||||||
|
"""
|
||||||
|
self.deleting = True
|
||||||
|
instance = self.get_instance()
|
||||||
|
instance_title = self.get_instance_title(instance)
|
||||||
|
|
||||||
|
# nb. this form proper is not readonly..
|
||||||
|
form = self.make_model_form(instance,
|
||||||
|
cancel_url_fallback=self.get_action_url('view', instance),
|
||||||
|
button_label_submit="DELETE Forever",
|
||||||
|
button_icon_submit='trash',
|
||||||
|
button_type_submit='is-danger')
|
||||||
|
# ..but *all* fields are readonly
|
||||||
|
form.readonly_fields = set(form.fields)
|
||||||
|
|
||||||
|
# nb. validate() often returns empty dict here
|
||||||
|
if form.validate() is not False:
|
||||||
|
self.delete_save_form(form)
|
||||||
|
return self.redirect(self.get_index_url())
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'instance': instance,
|
||||||
|
'instance_title': instance_title,
|
||||||
|
'form': form,
|
||||||
|
}
|
||||||
|
return self.render_to_response('delete', context)
|
||||||
|
|
||||||
|
def delete_save_form(self, form):
|
||||||
|
"""
|
||||||
|
Perform the delete operation(s) based on the given form data.
|
||||||
|
|
||||||
|
Default logic simply calls :meth:`delete_instance()` on the
|
||||||
|
form's :attr:`~wuttaweb.forms.base.Form.model_instance`.
|
||||||
|
|
||||||
|
This method is called by :meth:`delete()` after it has
|
||||||
|
validated the form.
|
||||||
|
"""
|
||||||
|
obj = form.model_instance
|
||||||
|
self.delete_instance(obj)
|
||||||
|
|
||||||
|
def delete_instance(self, obj):
|
||||||
|
"""
|
||||||
|
Delete the given model instance.
|
||||||
|
|
||||||
|
As of yet there is no default logic for this method; it will
|
||||||
|
raise ``NotImplementedError``. Subclass should override if
|
||||||
|
needed.
|
||||||
|
|
||||||
|
This method is called by :meth:`delete_save_form()`.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
# configure methods
|
# configure methods
|
||||||
##############################
|
##############################
|
||||||
|
@ -748,6 +837,7 @@ class MasterView(View):
|
||||||
'route_prefix': self.get_route_prefix(),
|
'route_prefix': self.get_route_prefix(),
|
||||||
'index_title': self.get_index_title(),
|
'index_title': self.get_index_title(),
|
||||||
'index_url': self.get_index_url(),
|
'index_url': self.get_index_url(),
|
||||||
|
'model_title': self.get_model_title(),
|
||||||
'config_title': self.get_config_title(),
|
'config_title': self.get_config_title(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -894,6 +984,17 @@ class MasterView(View):
|
||||||
"""
|
"""
|
||||||
return self.get_action_url('edit', obj)
|
return self.get_action_url('edit', obj)
|
||||||
|
|
||||||
|
def get_action_url_delete(self, obj, i):
|
||||||
|
"""
|
||||||
|
Returns the "delete" grid action URL for the given object.
|
||||||
|
|
||||||
|
Most typically this is like ``/widgets/XXX/delete`` where
|
||||||
|
``XXX`` represents the object's key/ID.
|
||||||
|
|
||||||
|
Calls :meth:`get_action_url()` under the hood.
|
||||||
|
"""
|
||||||
|
return self.get_action_url('delete', obj)
|
||||||
|
|
||||||
def make_model_form(self, model_instance=None, **kwargs):
|
def make_model_form(self, model_instance=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Create and return a :class:`~wuttaweb.forms.base.Form`
|
Create and return a :class:`~wuttaweb.forms.base.Form`
|
||||||
|
@ -1309,6 +1410,14 @@ class MasterView(View):
|
||||||
config.add_view(cls, attr='edit',
|
config.add_view(cls, attr='edit',
|
||||||
route_name=f'{route_prefix}.edit')
|
route_name=f'{route_prefix}.edit')
|
||||||
|
|
||||||
|
# delete
|
||||||
|
if cls.deletable:
|
||||||
|
instance_url_prefix = cls.get_instance_url_prefix()
|
||||||
|
config.add_route(f'{route_prefix}.delete',
|
||||||
|
f'{instance_url_prefix}/delete')
|
||||||
|
config.add_view(cls, attr='delete',
|
||||||
|
route_name=f'{route_prefix}.delete')
|
||||||
|
|
||||||
# configure
|
# configure
|
||||||
if cls.configurable:
|
if cls.configurable:
|
||||||
config.add_route(f'{route_prefix}.configure',
|
config.add_route(f'{route_prefix}.configure',
|
||||||
|
|
|
@ -197,36 +197,19 @@ class SettingView(MasterView):
|
||||||
""" """
|
""" """
|
||||||
return setting['name']
|
return setting['name']
|
||||||
|
|
||||||
def make_model_form(self, *args, **kwargs):
|
# TODO: master should handle this
|
||||||
""" """
|
|
||||||
# TODO: sheesh what a hack. hopefully not needed for long..
|
|
||||||
# here we ensure deform is created before introducing our
|
|
||||||
# `name` field, to keep it out of submit handling
|
|
||||||
|
|
||||||
if self.editing:
|
|
||||||
kwargs['fields'] = ['value']
|
|
||||||
|
|
||||||
form = super().make_model_form(*args, **kwargs)
|
|
||||||
|
|
||||||
if self.editing:
|
|
||||||
form.get_deform()
|
|
||||||
form.fields.insert_before('value', 'name')
|
|
||||||
|
|
||||||
return form
|
|
||||||
|
|
||||||
def configure_form(self, f):
|
|
||||||
""" """
|
|
||||||
super().configure_form(f)
|
|
||||||
|
|
||||||
if self.editing:
|
|
||||||
f.set_readonly('name')
|
|
||||||
|
|
||||||
def persist(self, setting, session=None):
|
def persist(self, setting, session=None):
|
||||||
""" """
|
""" """
|
||||||
name = self.get_instance(session=session)['name']
|
name = self.get_instance(session=session)['name']
|
||||||
session = session or Session()
|
session = session or Session()
|
||||||
self.app.save_setting(session, name, setting['value'])
|
self.app.save_setting(session, name, setting['value'])
|
||||||
|
|
||||||
|
# TODO: master should handle this
|
||||||
|
def delete_instance(self, obj, session=None):
|
||||||
|
""" """
|
||||||
|
session = session or Session()
|
||||||
|
self.app.delete_setting(session, obj['name'])
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs):
|
def defaults(config, **kwargs):
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
|
@ -347,7 +347,7 @@ class TestForm(TestCase):
|
||||||
data = form.validate()
|
data = form.validate()
|
||||||
self.assertEqual(data, {'foo': 'blarg', 'bar': 'baz'})
|
self.assertEqual(data, {'foo': 'blarg', 'bar': 'baz'})
|
||||||
|
|
||||||
# validating a second type updates form.validated
|
# validating a second time updates form.validated
|
||||||
self.request.POST = {'foo': 'BLARG', 'bar': 'BAZ'}
|
self.request.POST = {'foo': 'BLARG', 'bar': 'BAZ'}
|
||||||
data = form.validate()
|
data = form.validate()
|
||||||
self.assertEqual(data, {'foo': 'BLARG', 'bar': 'BAZ'})
|
self.assertEqual(data, {'foo': 'BLARG', 'bar': 'BAZ'})
|
||||||
|
@ -359,3 +359,17 @@ class TestForm(TestCase):
|
||||||
dform = form.get_deform()
|
dform = form.get_deform()
|
||||||
self.assertEqual(len(dform.error.children), 2)
|
self.assertEqual(len(dform.error.children), 2)
|
||||||
self.assertEqual(dform['foo'].errormsg, "Pstruct is not a string")
|
self.assertEqual(dform['foo'].errormsg, "Pstruct is not a string")
|
||||||
|
|
||||||
|
# when a form has readonly fields, validating it will *remove*
|
||||||
|
# those fields from deform/schema as well as final data dict
|
||||||
|
schema = self.make_schema()
|
||||||
|
form = self.make_form(schema=schema)
|
||||||
|
form.set_readonly('foo')
|
||||||
|
self.request.POST = {'foo': 'one', 'bar': 'two'}
|
||||||
|
data = form.validate()
|
||||||
|
self.assertEqual(data, {'bar': 'two'})
|
||||||
|
dform = form.get_deform()
|
||||||
|
self.assertNotIn('foo', schema)
|
||||||
|
self.assertNotIn('foo', dform)
|
||||||
|
self.assertIn('bar', schema)
|
||||||
|
self.assertIn('bar', dform)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# -*- coding: utf-8; -*-
|
# -*- coding: utf-8; -*-
|
||||||
|
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from pyramid import testing
|
from pyramid import testing
|
||||||
|
|
||||||
|
@ -151,6 +152,14 @@ class TestGridAction(TestCase):
|
||||||
label = action.render_label()
|
label = action.render_label()
|
||||||
self.assertEqual(label, "Bar")
|
self.assertEqual(label, "Bar")
|
||||||
|
|
||||||
|
def test_render_icon_and_label(self):
|
||||||
|
action = self.make_action('blarg')
|
||||||
|
with patch.multiple(action,
|
||||||
|
render_icon=lambda: 'ICON',
|
||||||
|
render_label=lambda: 'LABEL'):
|
||||||
|
html = action.render_icon_and_label()
|
||||||
|
self.assertEqual('ICON LABEL', html)
|
||||||
|
|
||||||
def test_get_url(self):
|
def test_get_url(self):
|
||||||
obj = {'foo': 'bar'}
|
obj = {'foo': 'bar'}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,8 @@ class TestMasterView(WebTestCase):
|
||||||
with patch.multiple(master.MasterView, create=True,
|
with patch.multiple(master.MasterView, create=True,
|
||||||
model_name='Widget',
|
model_name='Widget',
|
||||||
viewable=False,
|
viewable=False,
|
||||||
editable=False):
|
editable=False,
|
||||||
|
deletable=False):
|
||||||
master.MasterView.defaults(self.pyramid_config)
|
master.MasterView.defaults(self.pyramid_config)
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
|
@ -415,7 +416,7 @@ class TestMasterView(WebTestCase):
|
||||||
self.assertNotIn('Required', response.text)
|
self.assertNotIn('Required', response.text)
|
||||||
|
|
||||||
def persist(setting):
|
def persist(setting):
|
||||||
self.app.save_setting(self.session, setting['name'], setting['value'])
|
self.app.save_setting(self.session, 'foo.bar', setting['value'])
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
|
|
||||||
# post request to save settings
|
# post request to save settings
|
||||||
|
@ -427,15 +428,13 @@ class TestMasterView(WebTestCase):
|
||||||
with patch.object(view, 'persist', new=persist):
|
with patch.object(view, 'persist', new=persist):
|
||||||
response = view.edit()
|
response = view.edit()
|
||||||
# nb. should get redirect back to view page
|
# nb. should get redirect back to view page
|
||||||
self.assertIsInstance(response, HTTPFound)
|
self.assertEqual(response.status_code, 302)
|
||||||
# setting should be updated in DB
|
# setting should be updated in DB
|
||||||
self.assertEqual(self.app.get_setting(self.session, 'foo.bar'), 'froogle')
|
self.assertEqual(self.app.get_setting(self.session, 'foo.bar'), 'froogle')
|
||||||
|
|
||||||
# try another post with invalid data (name is required)
|
# try another post with invalid data (value is required)
|
||||||
self.request.method = 'POST'
|
self.request.method = 'POST'
|
||||||
self.request.POST = {
|
self.request.POST = {}
|
||||||
'value': 'gargoyle',
|
|
||||||
}
|
|
||||||
with patch.object(view, 'persist', new=persist):
|
with patch.object(view, 'persist', new=persist):
|
||||||
response = view.edit()
|
response = view.edit()
|
||||||
# nb. should get a form with errors
|
# nb. should get a form with errors
|
||||||
|
@ -444,6 +443,60 @@ class TestMasterView(WebTestCase):
|
||||||
# setting did not change in DB
|
# setting did not change in DB
|
||||||
self.assertEqual(self.app.get_setting(self.session, 'foo.bar'), 'froogle')
|
self.assertEqual(self.app.get_setting(self.session, 'foo.bar'), 'froogle')
|
||||||
|
|
||||||
|
def test_delete(self):
|
||||||
|
model = self.app.model
|
||||||
|
self.app.save_setting(self.session, 'foo.bar', 'frazzle')
|
||||||
|
self.session.commit()
|
||||||
|
self.assertEqual(self.session.query(model.Setting).count(), 1)
|
||||||
|
|
||||||
|
def get_instance():
|
||||||
|
setting = self.session.query(model.Setting).get('foo.bar')
|
||||||
|
return {
|
||||||
|
'name': setting.name,
|
||||||
|
'value': setting.value,
|
||||||
|
}
|
||||||
|
|
||||||
|
# sanity/coverage check using /settings/XXX/delete
|
||||||
|
self.request.matchdict = {'name': 'foo.bar'}
|
||||||
|
with patch.multiple(master.MasterView, create=True,
|
||||||
|
model_name='Setting',
|
||||||
|
model_key='name',
|
||||||
|
form_fields=['name', 'value']):
|
||||||
|
view = master.MasterView(self.request)
|
||||||
|
with patch.object(view, 'get_instance', new=get_instance):
|
||||||
|
|
||||||
|
# get the form page
|
||||||
|
response = view.delete()
|
||||||
|
self.assertIsInstance(response, Response)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn('frazzle', response.text)
|
||||||
|
|
||||||
|
def delete_instance(setting):
|
||||||
|
print(setting) # TODO
|
||||||
|
self.app.delete_setting(self.session, setting['name'])
|
||||||
|
|
||||||
|
# post request to save settings
|
||||||
|
self.request.method = 'POST'
|
||||||
|
self.request.POST = {}
|
||||||
|
with patch.object(view, 'delete_instance', new=delete_instance):
|
||||||
|
response = view.delete()
|
||||||
|
# nb. should get redirect back to view page
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
# setting should be gone from DB
|
||||||
|
self.assertEqual(self.session.query(model.Setting).count(), 0)
|
||||||
|
|
||||||
|
def test_delete_instance(self):
|
||||||
|
model = self.app.model
|
||||||
|
self.app.save_setting(self.session, 'foo.bar', 'frazzle')
|
||||||
|
self.session.commit()
|
||||||
|
setting = self.session.query(model.Setting).one()
|
||||||
|
|
||||||
|
with patch.multiple(master.MasterView, create=True,
|
||||||
|
model_class=model.Setting,
|
||||||
|
form_fields=['name', 'value']):
|
||||||
|
view = master.MasterView(self.request)
|
||||||
|
self.assertRaises(NotImplementedError, view.delete_instance, setting)
|
||||||
|
|
||||||
def test_configure(self):
|
def test_configure(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
|
||||||
|
|
|
@ -66,17 +66,6 @@ class TestSettingView(WebTestCase):
|
||||||
title = view.get_instance_title(setting)
|
title = view.get_instance_title(setting)
|
||||||
self.assertEqual(title, 'foo')
|
self.assertEqual(title, 'foo')
|
||||||
|
|
||||||
def test_make_model_form(self):
|
|
||||||
view = self.make_view()
|
|
||||||
view.editing = True
|
|
||||||
form = view.make_model_form()
|
|
||||||
self.assertEqual(form.fields, ['name', 'value'])
|
|
||||||
self.assertIn('name', form)
|
|
||||||
self.assertIn('value', form)
|
|
||||||
dform = form.get_deform()
|
|
||||||
self.assertNotIn('name', dform)
|
|
||||||
self.assertIn('value', dform)
|
|
||||||
|
|
||||||
def test_persist(self):
|
def test_persist(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
|
@ -92,3 +81,18 @@ class TestSettingView(WebTestCase):
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
self.assertEqual(self.session.query(model.Setting).count(), 1)
|
self.assertEqual(self.session.query(model.Setting).count(), 1)
|
||||||
self.assertEqual(self.app.get_setting(self.session, 'foo'), 'frazzle')
|
self.assertEqual(self.app.get_setting(self.session, 'foo'), 'frazzle')
|
||||||
|
|
||||||
|
def test_delete_instance(self):
|
||||||
|
model = self.app.model
|
||||||
|
view = self.make_view()
|
||||||
|
|
||||||
|
# setup
|
||||||
|
self.app.save_setting(self.session, 'foo', 'bar')
|
||||||
|
self.session.commit()
|
||||||
|
self.assertEqual(self.session.query(model.Setting).count(), 1)
|
||||||
|
|
||||||
|
# setting is deleted
|
||||||
|
self.request.matchdict = {'name': 'foo'}
|
||||||
|
view.delete_instance({'name': 'foo', 'value': 'frazzle'}, session=self.session)
|
||||||
|
self.session.commit()
|
||||||
|
self.assertEqual(self.session.query(model.Setting).count(), 0)
|
||||||
|
|
Loading…
Reference in a new issue