feat: add basic "delete results" grid tool
this is done synchronously with no progress indicator yet
This commit is contained in:
parent
6650ee698e
commit
6fa8b0aeaa
|
@ -28,7 +28,7 @@ import functools
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import warnings
|
import warnings
|
||||||
from collections import namedtuple
|
from collections import namedtuple, OrderedDict
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from sqlalchemy import orm
|
from sqlalchemy import orm
|
||||||
|
@ -339,6 +339,16 @@ class Grid:
|
||||||
sorting.
|
sorting.
|
||||||
|
|
||||||
See :meth:`set_joiner()` for more info.
|
See :meth:`set_joiner()` for more info.
|
||||||
|
|
||||||
|
.. attribute:: tools
|
||||||
|
|
||||||
|
Dict of "tool" elements for the grid. Tools are usually buttons
|
||||||
|
(e.g. "Delete Results"), shown on top right of the grid.
|
||||||
|
|
||||||
|
The keys for this dict are somewhat arbitrary, defined by the
|
||||||
|
caller. Values should be HTML literal elements.
|
||||||
|
|
||||||
|
See also :meth:`add_tool()` and :meth:`set_tools()`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -369,6 +379,7 @@ class Grid:
|
||||||
filters=None,
|
filters=None,
|
||||||
filter_defaults=None,
|
filter_defaults=None,
|
||||||
joiners=None,
|
joiners=None,
|
||||||
|
tools=None,
|
||||||
):
|
):
|
||||||
self.request = request
|
self.request = request
|
||||||
self.vue_tagname = vue_tagname
|
self.vue_tagname = vue_tagname
|
||||||
|
@ -386,6 +397,7 @@ class Grid:
|
||||||
self.app = self.config.get_app()
|
self.app = self.config.get_app()
|
||||||
|
|
||||||
self.set_columns(columns or self.get_columns())
|
self.set_columns(columns or self.get_columns())
|
||||||
|
self.set_tools(tools)
|
||||||
|
|
||||||
# sorting
|
# sorting
|
||||||
self.sortable = sortable
|
self.sortable = sortable
|
||||||
|
@ -658,6 +670,33 @@ class Grid:
|
||||||
"""
|
"""
|
||||||
self.actions.append(GridAction(self.request, key, **kwargs))
|
self.actions.append(GridAction(self.request, key, **kwargs))
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
if tools and isinstance(tools, list):
|
||||||
|
if not any([isinstance(t, (tuple, list)) for t in tools]):
|
||||||
|
tools = [(self.app.make_uuid(), t) for t in tools]
|
||||||
|
self.tools = OrderedDict(tools or [])
|
||||||
|
|
||||||
|
def add_tool(self, html, key=None):
|
||||||
|
"""
|
||||||
|
Add a new HTML snippet to the :attr:`tools` dict.
|
||||||
|
|
||||||
|
:param html: HTML literal for the tool element.
|
||||||
|
|
||||||
|
:param key: Optional key to use when adding to the
|
||||||
|
:attr:`tools` dict. If not specified, a random string is
|
||||||
|
generated.
|
||||||
|
|
||||||
|
See also :meth:`set_tools()`.
|
||||||
|
"""
|
||||||
|
if not key:
|
||||||
|
key = self.app.make_uuid()
|
||||||
|
self.tools[key] = html
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
# joining methods
|
# joining methods
|
||||||
##############################
|
##############################
|
||||||
|
|
|
@ -90,6 +90,19 @@
|
||||||
</form>
|
</form>
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
|
<div style="display: flex; flex-direction: column; justify-content: space-between;">
|
||||||
|
|
||||||
|
## nb. this is needed to force tools to bottom
|
||||||
|
## TODO: should we put a context menu here?
|
||||||
|
<div></div>
|
||||||
|
|
||||||
|
<div class="wutta-grid-tools-wrapper">
|
||||||
|
% for html in grid.tools.values():
|
||||||
|
${html}
|
||||||
|
% endfor
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<${b}-table :data="data"
|
<${b}-table :data="data"
|
||||||
|
@ -290,6 +303,14 @@
|
||||||
template: '#${grid.vue_tagname}-template',
|
template: '#${grid.vue_tagname}-template',
|
||||||
computed: {
|
computed: {
|
||||||
|
|
||||||
|
recordCount() {
|
||||||
|
% if grid.paginated:
|
||||||
|
return this.pagerStats.item_count
|
||||||
|
% else:
|
||||||
|
return this.data.length
|
||||||
|
% endif
|
||||||
|
},
|
||||||
|
|
||||||
directLink() {
|
directLink() {
|
||||||
const params = new URLSearchParams(this.getAllParams())
|
const params = new URLSearchParams(this.getAllParams())
|
||||||
return `${request.path_url}?${'$'}{params}`
|
return `${request.path_url}?${'$'}{params}`
|
||||||
|
|
|
@ -23,6 +23,41 @@
|
||||||
% endif
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
<%def name="modify_vue_vars()">
|
||||||
|
${parent.modify_vue_vars()}
|
||||||
|
% if master.deletable_bulk and master.has_perm('delete_bulk'):
|
||||||
|
<script>
|
||||||
|
|
||||||
|
${grid.vue_component}Data.deleteResultsSubmitting = false
|
||||||
|
|
||||||
|
${grid.vue_component}.computed.deleteResultsDisabled = function() {
|
||||||
|
if (this.deleteResultsSubmitting) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (!this.recordCount) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
${grid.vue_component}.methods.deleteResultsSubmit = function() {
|
||||||
|
|
||||||
|
## TODO: should give a better dialog here
|
||||||
|
const msg = "You are about to delete "
|
||||||
|
+ this.recordCount.toLocaleString('en')
|
||||||
|
+ " records.\n\nAre you sure?"
|
||||||
|
if (!confirm(msg)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.deleteResultsSubmitting = true
|
||||||
|
this.$refs.deleteResultsForm.submit()
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
<%def name="make_vue_components()">
|
<%def name="make_vue_components()">
|
||||||
${parent.make_vue_components()}
|
${parent.make_vue_components()}
|
||||||
% if grid is not Undefined:
|
% if grid is not Undefined:
|
||||||
|
|
|
@ -154,6 +154,7 @@ class CommonView(View):
|
||||||
'settings.view',
|
'settings.view',
|
||||||
'settings.edit',
|
'settings.edit',
|
||||||
'settings.delete',
|
'settings.delete',
|
||||||
|
'settings.delete_bulk',
|
||||||
'upgrades.list',
|
'upgrades.list',
|
||||||
'upgrades.create',
|
'upgrades.create',
|
||||||
'upgrades.view',
|
'upgrades.view',
|
||||||
|
|
|
@ -24,6 +24,8 @@
|
||||||
Base Logic for Master Views
|
Base Logic for Master Views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from sqlalchemy import orm
|
from sqlalchemy import orm
|
||||||
|
|
||||||
|
@ -31,11 +33,14 @@ from pyramid.renderers import render_to_response
|
||||||
from webhelpers2.html import HTML
|
from webhelpers2.html import HTML
|
||||||
|
|
||||||
from wuttaweb.views import View
|
from wuttaweb.views import View
|
||||||
from wuttaweb.util import get_form_data, get_model_fields
|
from wuttaweb.util import get_form_data, get_model_fields, render_csrf_token
|
||||||
from wuttaweb.db import Session
|
from wuttaweb.db import Session
|
||||||
from wuttjamaican.util import get_class_hierarchy
|
from wuttjamaican.util import get_class_hierarchy
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class MasterView(View):
|
class MasterView(View):
|
||||||
"""
|
"""
|
||||||
Base class for "master" views.
|
Base class for "master" views.
|
||||||
|
@ -284,6 +289,12 @@ class MasterView(View):
|
||||||
|
|
||||||
See also :meth:`is_deletable()`.
|
See also :meth:`is_deletable()`.
|
||||||
|
|
||||||
|
.. attribute:: deletable_bulk
|
||||||
|
|
||||||
|
Boolean indicating whether the view model supports "bulk
|
||||||
|
deleting" - i.e. it should have a :meth:`delete_bulk()` view.
|
||||||
|
Default value is ``False``.
|
||||||
|
|
||||||
.. attribute:: form_fields
|
.. attribute:: form_fields
|
||||||
|
|
||||||
List of fields for the model form.
|
List of fields for the model form.
|
||||||
|
@ -321,6 +332,7 @@ class MasterView(View):
|
||||||
viewable = True
|
viewable = True
|
||||||
editable = True
|
editable = True
|
||||||
deletable = True
|
deletable = True
|
||||||
|
deletable_bulk = False
|
||||||
has_autocomplete = False
|
has_autocomplete = False
|
||||||
configurable = False
|
configurable = False
|
||||||
|
|
||||||
|
@ -622,6 +634,76 @@ class MasterView(View):
|
||||||
session = self.app.get_session(obj)
|
session = self.app.get_session(obj)
|
||||||
session.delete(obj)
|
session.delete(obj)
|
||||||
|
|
||||||
|
def delete_bulk(self, session=None):
|
||||||
|
"""
|
||||||
|
View to delete all records in the current :meth:`index()` grid
|
||||||
|
data set, i.e. those matching current query.
|
||||||
|
|
||||||
|
This usually corresponds to a URL like
|
||||||
|
``/widgets/delete-bulk``.
|
||||||
|
|
||||||
|
By default, this view is included only if
|
||||||
|
:attr:`deletable_bulk` is true.
|
||||||
|
|
||||||
|
This view requires POST method. When it is finished deleting,
|
||||||
|
user is redirected back to :meth:`index()` view.
|
||||||
|
|
||||||
|
Subclass normally should not override this method, but rather
|
||||||
|
one of the related methods which are called (in)directly by
|
||||||
|
this one:
|
||||||
|
|
||||||
|
* :meth:`delete_bulk_data()`
|
||||||
|
"""
|
||||||
|
# get current data set from grid
|
||||||
|
# nb. this must *not* be paginated, we need it all
|
||||||
|
grid = self.make_model_grid(paginated=False)
|
||||||
|
data = grid.get_visible_data()
|
||||||
|
|
||||||
|
# delete it all and go back to listing
|
||||||
|
self.delete_bulk_data(data, session=session)
|
||||||
|
return self.redirect(self.get_index_url())
|
||||||
|
|
||||||
|
def delete_bulk_data(self, data, session=None):
|
||||||
|
"""
|
||||||
|
This method performs the actual bulk deletion, for the given
|
||||||
|
data set.
|
||||||
|
|
||||||
|
Default logic will call :meth:`is_deletable()` for every data
|
||||||
|
record, and if that returns true then it calls
|
||||||
|
:meth:`delete_instance()`.
|
||||||
|
|
||||||
|
As of now there is no progress indicator or async; caller must
|
||||||
|
simply wait until delete is finished.
|
||||||
|
"""
|
||||||
|
session = session or self.Session()
|
||||||
|
|
||||||
|
for obj in data:
|
||||||
|
if self.is_deletable(obj):
|
||||||
|
self.delete_instance(obj)
|
||||||
|
|
||||||
|
def delete_bulk_make_button(self):
|
||||||
|
""" """
|
||||||
|
route_prefix = self.get_route_prefix()
|
||||||
|
|
||||||
|
label = HTML.literal(
|
||||||
|
'{{ deleteResultsSubmitting ? "Working, please wait..." : "Delete Results" }}')
|
||||||
|
button = self.make_button(label,
|
||||||
|
variant='is-danger',
|
||||||
|
icon_left='trash',
|
||||||
|
**{'@click': 'deleteResultsSubmit()',
|
||||||
|
':disabled': 'deleteResultsDisabled'})
|
||||||
|
|
||||||
|
form = HTML.tag('form',
|
||||||
|
method='post',
|
||||||
|
action=self.request.route_url(f'{route_prefix}.delete_bulk'),
|
||||||
|
ref='deleteResultsForm',
|
||||||
|
class_='control',
|
||||||
|
c=[
|
||||||
|
render_csrf_token(self.request),
|
||||||
|
button,
|
||||||
|
])
|
||||||
|
return form
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
# autocomplete methods
|
# autocomplete methods
|
||||||
##############################
|
##############################
|
||||||
|
@ -1168,6 +1250,64 @@ class MasterView(View):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def make_button(
|
||||||
|
self,
|
||||||
|
label,
|
||||||
|
variant=None,
|
||||||
|
primary=False,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Make and return a HTML ``<b-button>`` literal.
|
||||||
|
|
||||||
|
:param label: Text label for the button.
|
||||||
|
|
||||||
|
:param variant: This is the "Buefy type" (or "Oruga variant")
|
||||||
|
for the button. Buefy and Oruga represent this differently
|
||||||
|
but this logic expects the Buefy format
|
||||||
|
(e.g. ``is-danger``) and *not* the Oruga format
|
||||||
|
(e.g. ``danger``), despite the param name matching Oruga's
|
||||||
|
terminology.
|
||||||
|
|
||||||
|
:param type: This param is not advertised in the method
|
||||||
|
signature, but if caller specifies ``type`` instead of
|
||||||
|
``variant`` it should work the same.
|
||||||
|
|
||||||
|
:param primary: If neither ``variant`` nor ``type`` are
|
||||||
|
specified, this flag may be used to automatically set the
|
||||||
|
Buefy type to ``is-primary``.
|
||||||
|
|
||||||
|
This is the preferred method where applicable, since it
|
||||||
|
avoids the Buefy vs. Oruga confusion, and the
|
||||||
|
implementation can change in the future.
|
||||||
|
|
||||||
|
:param \**kwargs: All remaining kwargs are passed to the
|
||||||
|
underlying ``HTML.tag()`` call, so will be rendered as
|
||||||
|
attributes on the button tag.
|
||||||
|
|
||||||
|
:returns: HTML literal for the button element. Will be something
|
||||||
|
along the lines of:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
<b-button type="is-primary"
|
||||||
|
icon-pack="fas"
|
||||||
|
icon-left="hand-pointer">
|
||||||
|
Click Me
|
||||||
|
</b-button>
|
||||||
|
"""
|
||||||
|
btn_kw = kwargs
|
||||||
|
btn_kw.setdefault('c', label)
|
||||||
|
btn_kw.setdefault('icon_pack', 'fas')
|
||||||
|
|
||||||
|
if 'type' not in btn_kw:
|
||||||
|
if variant:
|
||||||
|
btn_kw['type'] = variant
|
||||||
|
elif primary:
|
||||||
|
btn_kw['type'] = 'is-primary'
|
||||||
|
|
||||||
|
return HTML.tag('b-button', **btn_kw)
|
||||||
|
|
||||||
def render_to_response(self, template, context):
|
def render_to_response(self, template, context):
|
||||||
"""
|
"""
|
||||||
Locate and render an appropriate template, with the given
|
Locate and render an appropriate template, with the given
|
||||||
|
@ -1378,6 +1518,14 @@ class MasterView(View):
|
||||||
|
|
||||||
kwargs['actions'] = actions
|
kwargs['actions'] = actions
|
||||||
|
|
||||||
|
if 'tools' not in kwargs:
|
||||||
|
tools = []
|
||||||
|
|
||||||
|
if self.deletable_bulk and self.has_perm('delete_bulk'):
|
||||||
|
tools.append(('delete-results', self.delete_bulk_make_button()))
|
||||||
|
|
||||||
|
kwargs['tools'] = tools
|
||||||
|
|
||||||
if hasattr(self, 'grid_row_class'):
|
if hasattr(self, 'grid_row_class'):
|
||||||
kwargs.setdefault('row_class', self.grid_row_class)
|
kwargs.setdefault('row_class', self.grid_row_class)
|
||||||
kwargs.setdefault('filterable', self.filterable)
|
kwargs.setdefault('filterable', self.filterable)
|
||||||
|
@ -2084,17 +2232,6 @@ class MasterView(View):
|
||||||
f'{permission_prefix}.create',
|
f'{permission_prefix}.create',
|
||||||
f"Create new {model_title}")
|
f"Create new {model_title}")
|
||||||
|
|
||||||
# view
|
|
||||||
if cls.viewable:
|
|
||||||
instance_url_prefix = cls.get_instance_url_prefix()
|
|
||||||
config.add_route(f'{route_prefix}.view', instance_url_prefix)
|
|
||||||
config.add_view(cls, attr='view',
|
|
||||||
route_name=f'{route_prefix}.view',
|
|
||||||
permission=f'{permission_prefix}.view')
|
|
||||||
config.add_wutta_permission(permission_prefix,
|
|
||||||
f'{permission_prefix}.view',
|
|
||||||
f"View {model_title}")
|
|
||||||
|
|
||||||
# edit
|
# edit
|
||||||
if cls.editable:
|
if cls.editable:
|
||||||
instance_url_prefix = cls.get_instance_url_prefix()
|
instance_url_prefix = cls.get_instance_url_prefix()
|
||||||
|
@ -2119,6 +2256,18 @@ class MasterView(View):
|
||||||
f'{permission_prefix}.delete',
|
f'{permission_prefix}.delete',
|
||||||
f"Delete {model_title}")
|
f"Delete {model_title}")
|
||||||
|
|
||||||
|
# bulk delete
|
||||||
|
if cls.deletable_bulk:
|
||||||
|
config.add_route(f'{route_prefix}.delete_bulk',
|
||||||
|
f'{url_prefix}/delete-bulk',
|
||||||
|
request_method='POST')
|
||||||
|
config.add_view(cls, attr='delete_bulk',
|
||||||
|
route_name=f'{route_prefix}.delete_bulk',
|
||||||
|
permission=f'{permission_prefix}.delete_bulk')
|
||||||
|
config.add_wutta_permission(permission_prefix,
|
||||||
|
f'{permission_prefix}.delete_bulk',
|
||||||
|
f"Delete {model_title_plural} in bulk")
|
||||||
|
|
||||||
# autocomplete
|
# autocomplete
|
||||||
if cls.has_autocomplete:
|
if cls.has_autocomplete:
|
||||||
config.add_route(f'{route_prefix}.autocomplete',
|
config.add_route(f'{route_prefix}.autocomplete',
|
||||||
|
@ -2138,3 +2287,16 @@ class MasterView(View):
|
||||||
config.add_wutta_permission(permission_prefix,
|
config.add_wutta_permission(permission_prefix,
|
||||||
f'{permission_prefix}.configure',
|
f'{permission_prefix}.configure',
|
||||||
f"Configure {model_title_plural}")
|
f"Configure {model_title_plural}")
|
||||||
|
|
||||||
|
# view
|
||||||
|
# nb. always register this one last, so it does not take
|
||||||
|
# priority over model-wide action routes, e.g. delete_bulk
|
||||||
|
if cls.viewable:
|
||||||
|
instance_url_prefix = cls.get_instance_url_prefix()
|
||||||
|
config.add_route(f'{route_prefix}.view', instance_url_prefix)
|
||||||
|
config.add_view(cls, attr='view',
|
||||||
|
route_name=f'{route_prefix}.view',
|
||||||
|
permission=f'{permission_prefix}.view')
|
||||||
|
config.add_wutta_permission(permission_prefix,
|
||||||
|
f'{permission_prefix}.view',
|
||||||
|
f"View {model_title}")
|
||||||
|
|
|
@ -202,6 +202,7 @@ class SettingView(MasterView):
|
||||||
"""
|
"""
|
||||||
model_class = Setting
|
model_class = Setting
|
||||||
model_title = "Raw Setting"
|
model_title = "Raw Setting"
|
||||||
|
deletable_bulk = True
|
||||||
filter_defaults = {
|
filter_defaults = {
|
||||||
'name': {'active': True},
|
'name': {'active': True},
|
||||||
}
|
}
|
||||||
|
|
|
@ -254,6 +254,42 @@ class TestGrid(WebTestCase):
|
||||||
self.assertEqual(len(grid.actions), 1)
|
self.assertEqual(len(grid.actions), 1)
|
||||||
self.assertIsInstance(grid.actions[0], mod.GridAction)
|
self.assertIsInstance(grid.actions[0], mod.GridAction)
|
||||||
|
|
||||||
|
def test_set_tools(self):
|
||||||
|
grid = self.make_grid()
|
||||||
|
self.assertEqual(grid.tools, {})
|
||||||
|
|
||||||
|
# null
|
||||||
|
grid.set_tools(None)
|
||||||
|
self.assertEqual(grid.tools, {})
|
||||||
|
|
||||||
|
# empty
|
||||||
|
grid.set_tools({})
|
||||||
|
self.assertEqual(grid.tools, {})
|
||||||
|
|
||||||
|
# full dict is replaced
|
||||||
|
grid.tools = {'foo': 'bar'}
|
||||||
|
self.assertEqual(grid.tools, {'foo': 'bar'})
|
||||||
|
grid.set_tools({'bar': 'baz'})
|
||||||
|
self.assertEqual(grid.tools, {'bar': 'baz'})
|
||||||
|
|
||||||
|
# can specify as list of html elements
|
||||||
|
grid.set_tools(['foo', 'bar'])
|
||||||
|
self.assertEqual(len(grid.tools), 2)
|
||||||
|
self.assertEqual(list(grid.tools.values()), ['foo', 'bar'])
|
||||||
|
|
||||||
|
def test_add_tool(self):
|
||||||
|
grid = self.make_grid()
|
||||||
|
self.assertEqual(grid.tools, {})
|
||||||
|
|
||||||
|
# with key
|
||||||
|
grid.add_tool('foo', key='foo')
|
||||||
|
self.assertEqual(grid.tools, {'foo': 'foo'})
|
||||||
|
|
||||||
|
# without key
|
||||||
|
grid.add_tool('bar')
|
||||||
|
self.assertEqual(len(grid.tools), 2)
|
||||||
|
self.assertEqual(list(grid.tools.values()), ['foo', 'bar'])
|
||||||
|
|
||||||
def test_get_pagesize_options(self):
|
def test_get_pagesize_options(self):
|
||||||
grid = self.make_grid()
|
grid = self.make_grid()
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ class TestMasterView(WebTestCase):
|
||||||
with patch.multiple(mod.MasterView, create=True,
|
with patch.multiple(mod.MasterView, create=True,
|
||||||
model_name='Widget',
|
model_name='Widget',
|
||||||
model_key='uuid',
|
model_key='uuid',
|
||||||
|
deletable_bulk=True,
|
||||||
has_autocomplete=True,
|
has_autocomplete=True,
|
||||||
configurable=True):
|
configurable=True):
|
||||||
mod.MasterView.defaults(self.pyramid_config)
|
mod.MasterView.defaults(self.pyramid_config)
|
||||||
|
@ -400,6 +401,33 @@ class TestMasterView(WebTestCase):
|
||||||
self.assertTrue(view.has_any_perm('list', 'view'))
|
self.assertTrue(view.has_any_perm('list', 'view'))
|
||||||
self.assertTrue(self.request.has_any_perm('settings.list', 'settings.view'))
|
self.assertTrue(self.request.has_any_perm('settings.list', 'settings.view'))
|
||||||
|
|
||||||
|
def test_make_button(self):
|
||||||
|
view = self.make_view()
|
||||||
|
|
||||||
|
# normal
|
||||||
|
html = view.make_button('click me')
|
||||||
|
self.assertIn('<b-button ', html)
|
||||||
|
self.assertIn('click me', html)
|
||||||
|
self.assertNotIn('is-primary', html)
|
||||||
|
|
||||||
|
# primary as primary
|
||||||
|
html = view.make_button('click me', primary=True)
|
||||||
|
self.assertIn('<b-button ', html)
|
||||||
|
self.assertIn('click me', html)
|
||||||
|
self.assertIn('is-primary', html)
|
||||||
|
|
||||||
|
# primary as variant
|
||||||
|
html = view.make_button('click me', variant='is-primary')
|
||||||
|
self.assertIn('<b-button ', html)
|
||||||
|
self.assertIn('click me', html)
|
||||||
|
self.assertIn('is-primary', html)
|
||||||
|
|
||||||
|
# primary as type
|
||||||
|
html = view.make_button('click me', type='is-primary')
|
||||||
|
self.assertIn('<b-button ', html)
|
||||||
|
self.assertIn('click me', html)
|
||||||
|
self.assertIn('is-primary', html)
|
||||||
|
|
||||||
def test_render_to_response(self):
|
def test_render_to_response(self):
|
||||||
self.pyramid_config.include('wuttaweb.views.common')
|
self.pyramid_config.include('wuttaweb.views.common')
|
||||||
self.pyramid_config.include('wuttaweb.views.auth')
|
self.pyramid_config.include('wuttaweb.views.auth')
|
||||||
|
@ -473,6 +501,7 @@ class TestMasterView(WebTestCase):
|
||||||
self.assertEqual(grid.labels, {'name': "SETTING NAME"})
|
self.assertEqual(grid.labels, {'name': "SETTING NAME"})
|
||||||
|
|
||||||
def test_make_model_grid(self):
|
def test_make_model_grid(self):
|
||||||
|
self.pyramid_config.add_route('settings.delete_bulk', '/settings/delete-bulk')
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
|
||||||
# no model class
|
# no model class
|
||||||
|
@ -525,6 +554,20 @@ class TestMasterView(WebTestCase):
|
||||||
grid = view.make_model_grid(session=self.session)
|
grid = view.make_model_grid(session=self.session)
|
||||||
self.assertEqual(len(grid.actions), 3)
|
self.assertEqual(len(grid.actions), 3)
|
||||||
|
|
||||||
|
# no tools by default
|
||||||
|
with patch.multiple(mod.MasterView, create=True,
|
||||||
|
model_class=model.Setting):
|
||||||
|
grid = view.make_model_grid(session=self.session)
|
||||||
|
self.assertEqual(grid.tools, {})
|
||||||
|
|
||||||
|
# delete-results tool added if master/perms allow
|
||||||
|
with patch.multiple(mod.MasterView, create=True,
|
||||||
|
model_class=model.Setting,
|
||||||
|
deletable_bulk=True):
|
||||||
|
with patch.object(self.request, 'is_root', new=True):
|
||||||
|
grid = view.make_model_grid(session=self.session)
|
||||||
|
self.assertIn('delete-results', grid.tools)
|
||||||
|
|
||||||
def test_get_grid_data(self):
|
def test_get_grid_data(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
self.app.save_setting(self.session, 'foo', 'bar')
|
self.app.save_setting(self.session, 'foo', 'bar')
|
||||||
|
@ -1053,6 +1096,81 @@ class TestMasterView(WebTestCase):
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
self.assertEqual(self.session.query(model.Setting).count(), 0)
|
self.assertEqual(self.session.query(model.Setting).count(), 0)
|
||||||
|
|
||||||
|
def test_delete_bulk(self):
|
||||||
|
self.pyramid_config.add_route('settings', '/settings/')
|
||||||
|
model = self.app.model
|
||||||
|
sample_data = [
|
||||||
|
{'name': 'foo1', 'value': 'ONE'},
|
||||||
|
{'name': 'foo2', 'value': 'two'},
|
||||||
|
{'name': 'foo3', 'value': 'three'},
|
||||||
|
{'name': 'foo4', 'value': 'four'},
|
||||||
|
{'name': 'foo5', 'value': 'five'},
|
||||||
|
{'name': 'foo6', 'value': 'six'},
|
||||||
|
{'name': 'foo7', 'value': 'seven'},
|
||||||
|
{'name': 'foo8', 'value': 'eight'},
|
||||||
|
{'name': 'foo9', 'value': 'nine'},
|
||||||
|
]
|
||||||
|
for setting in sample_data:
|
||||||
|
self.app.save_setting(self.session, setting['name'], setting['value'])
|
||||||
|
self.session.commit()
|
||||||
|
sample_query = self.session.query(model.Setting)
|
||||||
|
|
||||||
|
with patch.multiple(mod.MasterView, create=True,
|
||||||
|
model_class=model.Setting):
|
||||||
|
view = self.make_view()
|
||||||
|
|
||||||
|
# sanity check on sample data
|
||||||
|
grid = view.make_model_grid(session=self.session)
|
||||||
|
data = grid.get_visible_data()
|
||||||
|
self.assertEqual(len(data), 9)
|
||||||
|
|
||||||
|
# and then let's filter it a little
|
||||||
|
self.request.GET = {'value': 's', 'value.verb': 'contains'}
|
||||||
|
grid = view.make_model_grid(session=self.session)
|
||||||
|
self.assertEqual(len(grid.filters), 2)
|
||||||
|
self.assertEqual(len(grid.active_filters), 1)
|
||||||
|
data = grid.get_visible_data()
|
||||||
|
self.assertEqual(len(data), 2)
|
||||||
|
|
||||||
|
# okay now let's delete those (gets redirected)
|
||||||
|
with patch.object(view, 'make_model_grid', return_value=grid):
|
||||||
|
response = view.delete_bulk(session=self.session)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(self.session.query(model.Setting).count(), 7)
|
||||||
|
|
||||||
|
def test_delete_bulk_data(self):
|
||||||
|
self.pyramid_config.add_route('settings', '/settings/')
|
||||||
|
model = self.app.model
|
||||||
|
sample_data = [
|
||||||
|
{'name': 'foo1', 'value': 'ONE'},
|
||||||
|
{'name': 'foo2', 'value': 'two'},
|
||||||
|
{'name': 'foo3', 'value': 'three'},
|
||||||
|
{'name': 'foo4', 'value': 'four'},
|
||||||
|
{'name': 'foo5', 'value': 'five'},
|
||||||
|
{'name': 'foo6', 'value': 'six'},
|
||||||
|
{'name': 'foo7', 'value': 'seven'},
|
||||||
|
{'name': 'foo8', 'value': 'eight'},
|
||||||
|
{'name': 'foo9', 'value': 'nine'},
|
||||||
|
]
|
||||||
|
for setting in sample_data:
|
||||||
|
self.app.save_setting(self.session, setting['name'], setting['value'])
|
||||||
|
self.session.commit()
|
||||||
|
sample_query = self.session.query(model.Setting)
|
||||||
|
|
||||||
|
with patch.multiple(mod.MasterView, create=True,
|
||||||
|
model_class=model.Setting):
|
||||||
|
view = self.make_view()
|
||||||
|
|
||||||
|
# basic bulk delete
|
||||||
|
self.assertEqual(self.session.query(model.Setting).count(), 9)
|
||||||
|
settings = self.session.query(model.Setting)\
|
||||||
|
.filter(model.Setting.value.ilike('%s%'))\
|
||||||
|
.all()
|
||||||
|
self.assertEqual(len(settings), 2)
|
||||||
|
view.delete_bulk_data(settings, session=self.session)
|
||||||
|
self.session.commit()
|
||||||
|
self.assertEqual(self.session.query(model.Setting).count(), 7)
|
||||||
|
|
||||||
def test_autocomplete(self):
|
def test_autocomplete(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue