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 logging
|
||||
import warnings
|
||||
from collections import namedtuple
|
||||
from collections import namedtuple, OrderedDict
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import orm
|
||||
|
@ -339,6 +339,16 @@ class Grid:
|
|||
sorting.
|
||||
|
||||
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__(
|
||||
|
@ -369,6 +379,7 @@ class Grid:
|
|||
filters=None,
|
||||
filter_defaults=None,
|
||||
joiners=None,
|
||||
tools=None,
|
||||
):
|
||||
self.request = request
|
||||
self.vue_tagname = vue_tagname
|
||||
|
@ -386,6 +397,7 @@ class Grid:
|
|||
self.app = self.config.get_app()
|
||||
|
||||
self.set_columns(columns or self.get_columns())
|
||||
self.set_tools(tools)
|
||||
|
||||
# sorting
|
||||
self.sortable = sortable
|
||||
|
@ -658,6 +670,33 @@ class Grid:
|
|||
"""
|
||||
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
|
||||
##############################
|
||||
|
|
|
@ -90,6 +90,19 @@
|
|||
</form>
|
||||
% 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>
|
||||
|
||||
<${b}-table :data="data"
|
||||
|
@ -290,6 +303,14 @@
|
|||
template: '#${grid.vue_tagname}-template',
|
||||
computed: {
|
||||
|
||||
recordCount() {
|
||||
% if grid.paginated:
|
||||
return this.pagerStats.item_count
|
||||
% else:
|
||||
return this.data.length
|
||||
% endif
|
||||
},
|
||||
|
||||
directLink() {
|
||||
const params = new URLSearchParams(this.getAllParams())
|
||||
return `${request.path_url}?${'$'}{params}`
|
||||
|
|
|
@ -23,6 +23,41 @@
|
|||
% endif
|
||||
</%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()">
|
||||
${parent.make_vue_components()}
|
||||
% if grid is not Undefined:
|
||||
|
|
|
@ -154,6 +154,7 @@ class CommonView(View):
|
|||
'settings.view',
|
||||
'settings.edit',
|
||||
'settings.delete',
|
||||
'settings.delete_bulk',
|
||||
'upgrades.list',
|
||||
'upgrades.create',
|
||||
'upgrades.view',
|
||||
|
|
|
@ -24,6 +24,8 @@
|
|||
Base Logic for Master Views
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import orm
|
||||
|
||||
|
@ -31,11 +33,14 @@ from pyramid.renderers import render_to_response
|
|||
from webhelpers2.html import HTML
|
||||
|
||||
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 wuttjamaican.util import get_class_hierarchy
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MasterView(View):
|
||||
"""
|
||||
Base class for "master" views.
|
||||
|
@ -284,6 +289,12 @@ class MasterView(View):
|
|||
|
||||
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
|
||||
|
||||
List of fields for the model form.
|
||||
|
@ -321,6 +332,7 @@ class MasterView(View):
|
|||
viewable = True
|
||||
editable = True
|
||||
deletable = True
|
||||
deletable_bulk = False
|
||||
has_autocomplete = False
|
||||
configurable = False
|
||||
|
||||
|
@ -622,6 +634,76 @@ class MasterView(View):
|
|||
session = self.app.get_session(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
|
||||
##############################
|
||||
|
@ -1168,6 +1250,64 @@ class MasterView(View):
|
|||
return True
|
||||
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):
|
||||
"""
|
||||
Locate and render an appropriate template, with the given
|
||||
|
@ -1378,6 +1518,14 @@ class MasterView(View):
|
|||
|
||||
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'):
|
||||
kwargs.setdefault('row_class', self.grid_row_class)
|
||||
kwargs.setdefault('filterable', self.filterable)
|
||||
|
@ -2084,17 +2232,6 @@ class MasterView(View):
|
|||
f'{permission_prefix}.create',
|
||||
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
|
||||
if cls.editable:
|
||||
instance_url_prefix = cls.get_instance_url_prefix()
|
||||
|
@ -2119,6 +2256,18 @@ class MasterView(View):
|
|||
f'{permission_prefix}.delete',
|
||||
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
|
||||
if cls.has_autocomplete:
|
||||
config.add_route(f'{route_prefix}.autocomplete',
|
||||
|
@ -2138,3 +2287,16 @@ class MasterView(View):
|
|||
config.add_wutta_permission(permission_prefix,
|
||||
f'{permission_prefix}.configure',
|
||||
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_title = "Raw Setting"
|
||||
deletable_bulk = True
|
||||
filter_defaults = {
|
||||
'name': {'active': True},
|
||||
}
|
||||
|
|
|
@ -254,6 +254,42 @@ class TestGrid(WebTestCase):
|
|||
self.assertEqual(len(grid.actions), 1)
|
||||
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):
|
||||
grid = self.make_grid()
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ class TestMasterView(WebTestCase):
|
|||
with patch.multiple(mod.MasterView, create=True,
|
||||
model_name='Widget',
|
||||
model_key='uuid',
|
||||
deletable_bulk=True,
|
||||
has_autocomplete=True,
|
||||
configurable=True):
|
||||
mod.MasterView.defaults(self.pyramid_config)
|
||||
|
@ -400,6 +401,33 @@ class TestMasterView(WebTestCase):
|
|||
self.assertTrue(view.has_any_perm('list', '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):
|
||||
self.pyramid_config.include('wuttaweb.views.common')
|
||||
self.pyramid_config.include('wuttaweb.views.auth')
|
||||
|
@ -473,6 +501,7 @@ class TestMasterView(WebTestCase):
|
|||
self.assertEqual(grid.labels, {'name': "SETTING NAME"})
|
||||
|
||||
def test_make_model_grid(self):
|
||||
self.pyramid_config.add_route('settings.delete_bulk', '/settings/delete-bulk')
|
||||
model = self.app.model
|
||||
|
||||
# no model class
|
||||
|
@ -525,6 +554,20 @@ class TestMasterView(WebTestCase):
|
|||
grid = view.make_model_grid(session=self.session)
|
||||
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):
|
||||
model = self.app.model
|
||||
self.app.save_setting(self.session, 'foo', 'bar')
|
||||
|
@ -1053,6 +1096,81 @@ class TestMasterView(WebTestCase):
|
|||
self.session.commit()
|
||||
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):
|
||||
model = self.app.model
|
||||
|
||||
|
|
Loading…
Reference in a new issue